mirror of
https://github.com/Ryder-Sun/Meldwork.git
synced 2026-11-07 06:56:03 -10:00
feat: refresh the app UI and surface what needs the user
The chat page (header, messages, composer, run status and the animated empty
state) and the modal dialogs are deliberately left as they were before this
refresh: those files are byte-identical to 17c57f5.
What changed
- Design tokens: an 8-step type scale (readable text no smaller than 12px
outside badges), semantic success/warning/danger/info colors for both themes,
AA-contrast muted and accent text, a focus-ring token, elevation, layer and
motion tokens, and a dark backdrop that is darker than the workspace card.
- Startup is no longer blocked by Agent detection. The full-screen WebGL overlay
and the physics loader are removed (three is dropped from the dependencies);
the sidebar, home page and composer show "Detecting local Agents" instead, and
an empty Agent list is not reported as a setup problem while detection runs.
- Needs your decision: conversations waiting on a permission or decision are
marked in the sidebar (rows, Agent icons, collapsed switcher) and listed on the
home page. Group runs that finish while you are elsewhere get the same finished
mark direct chats already had. Collapsed sidebar sections show a count.
- Settings: a new General tab (language, appearance: System/Light/Dark). The
theme now follows the OS until one is chosen; it used to freeze on first
launch. Agent cards explain why an installed Agent is unavailable (installed
version, verified range, failed check) with a Re-detect action. Cloud
connections ask before deleting.
- Gemini: a detected Gemini CLI gets its own logo. It stays out of the built-in
list so nobody sees a "Not installed" card for an Agent Meldwork cannot install.
- Toasts: the duration grows with the message length and pauses on hover; errors
use the alert role.
- Onboarding: a Skip button and arrow-key navigation.
- Unsent text is kept per conversation while the app is open.
- Copy: engineering terms (circuit breaker, hard budget, delivery owner) are
gone from user-facing status text.
- Status colors, focus ring and the run-details waiting state use the new tokens.
Why
Small text and low-contrast grey carried the most important state (Agent
availability, time, hints); the brand color meant ready, done, running and
warning at once; and the app could not be used until local Agent detection
finished although history is local. This makes states legible and separable and
shows the ones that need the user.
Checks
- npm --prefix frontend test: 43 files, 357 tests passed
- npm --prefix frontend run build and build:desktop: passed
- Not verified in a running Electron window; the changed screens need a visual
check in light and dark, Chinese and English.
This commit is contained in:
1 parent
ef9933d571
commit
ce995cb93d
51 files changed
+2097
-1046
No files matched your search
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: meldwork-media:; media-src 'self' meldwork-media:; font-src 'self' data:; connect-src 'self'; worker-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'"
|
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: meldwork-media:; media-src 'self' meldwork-media:; font-src 'self' data:; connect-src 'self'; worker-src 'self'; object-src 'none'; frame-src 'none'; base-uri 'none'; form-action 'none'"
|
||||||
/>
|
/>
|
||||||
<meta name="theme-color" content="#f6f3ed" />
|
<meta name="theme-color" content="#f3f6f8" />
|
||||||
<title>Meldwork</title>
|
<title>Meldwork</title>
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="./logos/meldwork-favicon-16.png" />
|
<link rel="icon" type="image/png" sizes="16x16" href="./logos/meldwork-favicon-16.png" />
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="./logos/meldwork-favicon-32.png" />
|
<link rel="icon" type="image/png" sizes="32x32" href="./logos/meldwork-favicon-32.png" />
|
||||||
|
|||||||
Generated
-7
@@ -12,7 +12,6 @@
|
|||||||
"@vicons/ionicons5": "^0.12.0",
|
"@vicons/ionicons5": "^0.12.0",
|
||||||
"dompurify": "^3.4.13",
|
"dompurify": "^3.4.13",
|
||||||
"marked": "^9.0.0",
|
"marked": "^9.0.0",
|
||||||
"three": "^0.185.1",
|
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2241,12 +2240,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/three": {
|
|
||||||
"version": "0.185.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz",
|
|
||||||
"integrity": "sha512-5aojFCXKwnjBRZvUnt3WFfEcvUJgkN5LlijRFN95hMy8WVkG4I0QNcJE+OuWvuJ0bOdStrbfXn0pkd6/QyiAlg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
"version": "2.9.0",
|
"version": "2.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"@vicons/ionicons5": "^0.12.0",
|
"@vicons/ionicons5": "^0.12.0",
|
||||||
"dompurify": "^3.4.13",
|
"dompurify": "^3.4.13",
|
||||||
"marked": "^9.0.0",
|
"marked": "^9.0.0",
|
||||||
"three": "^0.185.1",
|
|
||||||
"vue": "^3.4.0"
|
"vue": "^3.4.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
+50
-33
@@ -12,7 +12,7 @@
|
|||||||
<section v-if="booting" class="boot-state" aria-live="polite">
|
<section v-if="booting" class="boot-state" aria-live="polite">
|
||||||
<img class="boot-logo" :src="productAppIcon" alt="Meldwork" />
|
<img class="boot-logo" :src="productAppIcon" alt="Meldwork" />
|
||||||
<p>{{ t('app.loading') }}</p>
|
<p>{{ t('app.loading') }}</p>
|
||||||
<WavePhysicsLoader :theme="theme" />
|
<span class="boot-progress" aria-hidden="true"><span /></span>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-else-if="bridgeMissing" class="bridge-state">
|
<section v-else-if="bridgeMissing" class="bridge-state">
|
||||||
@@ -26,33 +26,6 @@
|
|||||||
:t="t" :theme="theme" :active-view="activeView" :blocked="contentInteractionBlocked"
|
:t="t" :theme="theme" :active-view="activeView" :blocked="contentInteractionBlocked"
|
||||||
:open-system-settings="openSystemSettings" :toggle-locale="toggleLocale" :toggle-theme="toggleTheme"
|
:open-system-settings="openSystemSettings" :toggle-locale="toggleLocale" :toggle-theme="toggleTheme"
|
||||||
:shortcut-definitions="shortcutDefinitions" />
|
: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-progress" aria-hidden="true">
|
|
||||||
<span />
|
|
||||||
</div>
|
|
||||||
<div class="agent-discovery-status">
|
|
||||||
<span class="agent-discovery-status-dot" aria-hidden="true" />
|
|
||||||
<span>{{ t('agentDiscovery.status') }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</transition>
|
|
||||||
|
|
||||||
<WorkspaceSidebar :controller="workspaceSidebarController" />
|
<WorkspaceSidebar :controller="workspaceSidebarController" />
|
||||||
|
|
||||||
<section
|
<section
|
||||||
@@ -68,9 +41,12 @@
|
|||||||
<HomeDashboard
|
<HomeDashboard
|
||||||
v-else-if="activeView === 'home'"
|
v-else-if="activeView === 'home'"
|
||||||
:agent-description="agentDescription"
|
:agent-description="agentDescription"
|
||||||
|
:awaiting-decision-gates="awaitingDecisionGates"
|
||||||
|
:discovering="agentDiscoveryPending"
|
||||||
:group-name="groupName"
|
:group-name="groupName"
|
||||||
:groups="snapshot.groups"
|
:groups="snapshot.groups"
|
||||||
:installed-count="installedCount"
|
:installed-count="installedCount"
|
||||||
|
:is-group-awaiting-decision="isGroupAwaitingDecision"
|
||||||
:is-group-running="isGroupRunning"
|
:is-group-running="isGroupRunning"
|
||||||
:onboarding-completed="onboardingCompleted"
|
:onboarding-completed="onboardingCompleted"
|
||||||
:ready-agents="readyAgents"
|
:ready-agents="readyAgents"
|
||||||
@@ -157,8 +133,19 @@
|
|||||||
</transition>
|
</transition>
|
||||||
|
|
||||||
<transition name="toast">
|
<transition name="toast">
|
||||||
<div v-if="toastMessage" class="toast-message" role="status" aria-live="polite">
|
<div
|
||||||
<WarningOutline />
|
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>
|
<span>{{ toastMessage }}</span>
|
||||||
<button
|
<button
|
||||||
class="toast-dismiss-button"
|
class="toast-dismiss-button"
|
||||||
@@ -184,6 +171,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
AlertCircleOutline,
|
||||||
CheckmarkCircleOutline,
|
CheckmarkCircleOutline,
|
||||||
CloseOutline,
|
CloseOutline,
|
||||||
WarningOutline,
|
WarningOutline,
|
||||||
@@ -194,8 +182,6 @@ import ConversationHeader from './components/ConversationHeader.vue'
|
|||||||
import ConversationTimelineView from './components/ConversationTimelineView.vue'
|
import ConversationTimelineView from './components/ConversationTimelineView.vue'
|
||||||
import HomeDashboard from './components/HomeDashboard.vue'
|
import HomeDashboard from './components/HomeDashboard.vue'
|
||||||
import OnboardingDialog from './components/OnboardingDialog.vue'
|
import OnboardingDialog from './components/OnboardingDialog.vue'
|
||||||
import PixelBlast from './components/PixelBlast.vue'
|
|
||||||
import WavePhysicsLoader from './components/WavePhysicsLoader.vue'
|
|
||||||
import RunTracePanel from './components/RunTracePanel.vue'
|
import RunTracePanel from './components/RunTracePanel.vue'
|
||||||
import SystemSettingsView from './components/SystemSettingsView.vue'
|
import SystemSettingsView from './components/SystemSettingsView.vue'
|
||||||
import WorkspaceModalContent from './components/WorkspaceModalContent.vue'
|
import WorkspaceModalContent from './components/WorkspaceModalContent.vue'
|
||||||
@@ -281,9 +267,12 @@ const {
|
|||||||
copyToastMessage,
|
copyToastMessage,
|
||||||
dismissToast,
|
dismissToast,
|
||||||
notify,
|
notify,
|
||||||
|
pauseToast,
|
||||||
|
resumeToast,
|
||||||
showCopyToast,
|
showCopyToast,
|
||||||
showError,
|
showError,
|
||||||
toastMessage,
|
toastMessage,
|
||||||
|
toastTone,
|
||||||
} = useAppNotifications()
|
} = useAppNotifications()
|
||||||
const collapsedGroupMenuController = useCollapsedGroupMenu({ sidebarCollapsed, shortcutMenuOpen })
|
const collapsedGroupMenuController = useCollapsedGroupMenu({ sidebarCollapsed, shortcutMenuOpen })
|
||||||
const {
|
const {
|
||||||
@@ -340,7 +329,9 @@ const {
|
|||||||
productAppIcon,
|
productAppIcon,
|
||||||
productMark,
|
productMark,
|
||||||
productWordmark,
|
productWordmark,
|
||||||
|
setThemePreference,
|
||||||
theme,
|
theme,
|
||||||
|
themePreference,
|
||||||
toggleLocale,
|
toggleLocale,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
} = useAppPreferences()
|
} = useAppPreferences()
|
||||||
@@ -507,6 +498,16 @@ const {
|
|||||||
const pendingHumanGates = computed(() => (
|
const pendingHumanGates = computed(() => (
|
||||||
snapshot.value.humanGates.filter(gate => gate.status === 'pending')
|
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(() => (
|
const activeRunHumanGates = computed(() => (
|
||||||
activeRun.value
|
activeRun.value
|
||||||
? pendingHumanGates.value.filter(gate => gate.runId === activeRun.value.runId)
|
? pendingHumanGates.value.filter(gate => gate.runId === activeRun.value.runId)
|
||||||
@@ -584,8 +585,10 @@ const {
|
|||||||
snapshot,
|
snapshot,
|
||||||
})
|
})
|
||||||
const { messageDeleteArmedId } = messageActions
|
const { messageDeleteArmedId } = messageActions
|
||||||
|
// Agent discovery no longer blocks the window: history is local and usable at
|
||||||
|
// once, and the sidebar, home and composer each show that detection is running.
|
||||||
const contentInteractionBlocked = computed(() => (
|
const contentInteractionBlocked = computed(() => (
|
||||||
blockingOverlayOpen.value || traceDrawerBlocking.value || agentDiscoveryPending.value
|
blockingOverlayOpen.value || traceDrawerBlocking.value
|
||||||
))
|
))
|
||||||
const composerContext = useComposerContext({
|
const composerContext = useComposerContext({
|
||||||
activeGroup,
|
activeGroup,
|
||||||
@@ -625,11 +628,14 @@ const {
|
|||||||
discussionMode,
|
discussionMode,
|
||||||
disposeComposerContext,
|
disposeComposerContext,
|
||||||
draft,
|
draft,
|
||||||
|
pruneComposerDrafts,
|
||||||
resetComposerContext,
|
resetComposerContext,
|
||||||
restoreComposerContext,
|
restoreComposerContext,
|
||||||
|
restoreComposerDraft,
|
||||||
scheduleComposerResize,
|
scheduleComposerResize,
|
||||||
serializeComposerContext,
|
serializeComposerContext,
|
||||||
skillMenuOpen,
|
skillMenuOpen,
|
||||||
|
stashComposerDraft,
|
||||||
} = composerContext
|
} = composerContext
|
||||||
const composerTargetsReady = computed(() => (
|
const composerTargetsReady = computed(() => (
|
||||||
composerTargetKinds.value.length > 0
|
composerTargetKinds.value.length > 0
|
||||||
@@ -851,6 +857,7 @@ const {
|
|||||||
confirmUnlimitedRounds,
|
confirmUnlimitedRounds,
|
||||||
contentInteractionBlocked,
|
contentInteractionBlocked,
|
||||||
customAgentDeleteArmed,
|
customAgentDeleteArmed,
|
||||||
|
discoveringAgents: agentDiscoveryPending,
|
||||||
focusedAgentKind,
|
focusedAgentKind,
|
||||||
formError,
|
formError,
|
||||||
goHome,
|
goHome,
|
||||||
@@ -858,6 +865,7 @@ const {
|
|||||||
handleSidebarAgentMain,
|
handleSidebarAgentMain,
|
||||||
installConfirmKind,
|
installConfirmKind,
|
||||||
installerState,
|
installerState,
|
||||||
|
isGroupAwaitingDecision,
|
||||||
isGroupRunning,
|
isGroupRunning,
|
||||||
modal,
|
modal,
|
||||||
openAgentManager,
|
openAgentManager,
|
||||||
@@ -868,6 +876,7 @@ const {
|
|||||||
selectGroup,
|
selectGroup,
|
||||||
selectedGroupId,
|
selectedGroupId,
|
||||||
selectSystemSettingsSection,
|
selectSystemSettingsSection,
|
||||||
|
setThemePreference,
|
||||||
sidebarAgentsCollapsed,
|
sidebarAgentsCollapsed,
|
||||||
sidebarGroupsCollapsed,
|
sidebarGroupsCollapsed,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
@@ -876,6 +885,7 @@ const {
|
|||||||
systemSettingsSection,
|
systemSettingsSection,
|
||||||
t,
|
t,
|
||||||
theme,
|
theme,
|
||||||
|
themePreference,
|
||||||
toggleLocale,
|
toggleLocale,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
},
|
},
|
||||||
@@ -1056,10 +1066,17 @@ watch(activeGroupMemberSignature, (value, previous) => {
|
|||||||
messageDeleteArmedId.value = ''
|
messageDeleteArmedId.value = ''
|
||||||
clearActiveTurn()
|
clearActiveTurn()
|
||||||
roundSettingsOpen.value = false
|
roundSettingsOpen.value = false
|
||||||
|
stashComposerDraft(previousGroupId)
|
||||||
resetComposerContext(group)
|
resetComposerContext(group)
|
||||||
|
restoreComposerDraft(currentGroupId)
|
||||||
|
pruneComposerDrafts(snapshot.value.groups.map(item => item.id))
|
||||||
composerAttachments.value = []
|
composerAttachments.value = []
|
||||||
void discardAttachments(abandonedAttachments)
|
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) => {
|
watch(activeRun, (value) => {
|
||||||
if (!value) return
|
if (!value) return
|
||||||
messageDeleteArmedId.value = ''
|
messageDeleteArmedId.value = ''
|
||||||
|
|||||||
@@ -315,7 +315,9 @@ describe('Meldwork workbench', () => {
|
|||||||
|
|
||||||
await wrapper.get('[aria-label="Attach files"]').trigger('click')
|
await wrapper.get('[aria-label="Attach files"]').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.get('.toast-message').attributes('role')).toBe('status')
|
// A failure is announced assertively; plain notices stay polite.
|
||||||
|
expect(wrapper.get('.toast-message').attributes('role')).toBe('alert')
|
||||||
|
expect(wrapper.get('.toast-message').attributes('data-tone')).toBe('error')
|
||||||
expect(wrapper.get('.toast-dismiss-button').attributes('aria-label')).toBe('Dismiss')
|
expect(wrapper.get('.toast-dismiss-button').attributes('aria-label')).toBe('Dismiss')
|
||||||
await wrapper.get('.toast-dismiss-button').trigger('click')
|
await wrapper.get('.toast-dismiss-button').trigger('click')
|
||||||
expect(wrapper.find('.toast-message').exists()).toBe(false)
|
expect(wrapper.find('.toast-message').exists()).toBe(false)
|
||||||
|
|||||||
@@ -303,7 +303,8 @@ describe('Meldwork workbench', () => {
|
|||||||
await wrapper.get('.run-agent-row').trigger('click')
|
await wrapper.get('.run-agent-row').trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.get('.trace-waiting-state').text()).toBe('Waiting')
|
// The system is waiting on the user, so the state says so instead of rotating "thinking" words.
|
||||||
|
expect(wrapper.get('.trace-waiting-state').text()).toBe('Waiting for your decision')
|
||||||
expect(wrapper.get('.trace-human-gate-section').text())
|
expect(wrapper.get('.trace-human-gate-section').text())
|
||||||
.toContain('workspace-write synthesis attempt may have produced side effects')
|
.toContain('workspace-write synthesis attempt may have produced side effects')
|
||||||
expect(wrapper.find('.trace-budget-section').exists()).toBe(false)
|
expect(wrapper.find('.trace-budget-section').exists()).toBe(false)
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ describe('Meldwork workbench', () => {
|
|||||||
|
|
||||||
expect(wrapper.get('.onboarding-slide img').attributes('src')).toContain('agent-collaboration.png')
|
expect(wrapper.get('.onboarding-slide img').attributes('src')).toContain('agent-collaboration.png')
|
||||||
expect(wrapper.get('.onboarding-primary').attributes()).not.toHaveProperty('disabled')
|
expect(wrapper.get('.onboarding-primary').attributes()).not.toHaveProperty('disabled')
|
||||||
expect(wrapper.get('.onboarding-primary').text()).toContain('Start using')
|
expect(wrapper.get('.onboarding-primary').text()).toContain('Get started')
|
||||||
|
|
||||||
await vi.advanceTimersByTimeAsync(3_200)
|
await vi.advanceTimersByTimeAsync(3_200)
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
@@ -45,26 +45,35 @@ afterEach(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('Meldwork workbench', () => {
|
describe('Meldwork workbench', () => {
|
||||||
it('shows a calm full-screen Agent discovery state until the first refresh completes', async () => {
|
it('keeps the workspace usable while the first Agent detection runs', async () => {
|
||||||
const fixture = createBridge()
|
const fixture = createBridge()
|
||||||
const pendingRefresh = deferred()
|
const pendingRefresh = deferred()
|
||||||
|
fixture.state.agents = []
|
||||||
fixture.bridge.localWorkspace.refreshAgents.mockReturnValueOnce(pendingRefresh.promise)
|
fixture.bridge.localWorkspace.refreshAgents.mockReturnValueOnce(pendingRefresh.promise)
|
||||||
window.meldworkDesktop = fixture.bridge
|
window.meldworkDesktop = fixture.bridge
|
||||||
|
|
||||||
const wrapper = mount(App, { attachTo: document.body })
|
const wrapper = mount(App, { attachTo: document.body })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('.agent-discovery-overlay').exists()).toBe(true)
|
// No blocking overlay: local history stays reachable and nothing is inert.
|
||||||
expect(wrapper.get('.agent-discovery-overlay').attributes('role')).toBe('status')
|
expect(wrapper.find('.agent-discovery-overlay').exists()).toBe(false)
|
||||||
expect(wrapper.find('.pixel-blast-container').exists()).toBe(true)
|
expect(wrapper.get('.workspace-pane').attributes()).not.toHaveProperty('inert')
|
||||||
expect(wrapper.find('.pixel-blast-container canvas').exists()).toBe(true)
|
expect(wrapper.get('.sidebar').attributes()).not.toHaveProperty('inert')
|
||||||
expect(wrapper.find('.agent-discovery-orbit').exists()).toBe(false)
|
expect(wrapper.get('.sidebar-discovery').attributes('role')).toBe('status')
|
||||||
expect(wrapper.get('.agent-discovery-copy h1').text()).toBe('Finding your local Agents')
|
expect(wrapper.get('.sidebar-discovery').text()).toContain('Detecting local Agents')
|
||||||
|
// An empty Agent list is not a setup problem yet.
|
||||||
|
expect(wrapper.get('.home-workspace-state').text()).toBe('Detecting local Agents')
|
||||||
|
expect(wrapper.get('.home-workspace-state').classes()).not.toContain('attention')
|
||||||
|
expect(wrapper.find('.setup-guide').exists()).toBe(false)
|
||||||
|
|
||||||
pendingRefresh.resolve(structuredClone(fixture.state))
|
pendingRefresh.resolve(structuredClone({
|
||||||
|
...fixture.state,
|
||||||
|
agents: [{ kind: 'codex', installed: true, available: true, credentialState: 'ready', version: '1.0.0' }],
|
||||||
|
}))
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.find('.agent-discovery-overlay').exists()).toBe(false)
|
expect(wrapper.find('.sidebar-discovery').exists()).toBe(false)
|
||||||
|
expect(wrapper.get('.home-workspace-state').text()).not.toBe('Detecting local Agents')
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -522,7 +531,9 @@ describe('Meldwork workbench', () => {
|
|||||||
await wrapper.get('.sidebar-toggle').trigger('click')
|
await wrapper.get('.sidebar-toggle').trigger('click')
|
||||||
const switcher = wrapper.get('.collapsed-group-switcher-button')
|
const switcher = wrapper.get('.collapsed-group-switcher-button')
|
||||||
expect(switcher.attributes('aria-label')).toBe('Switch group chat')
|
expect(switcher.attributes('aria-label')).toBe('Switch group chat')
|
||||||
expect(switcher.text()).toContain('10')
|
// A total count read as "10 unread"; the switcher only shows a dot when a group needs the user.
|
||||||
|
expect(switcher.text()).toBe('')
|
||||||
|
expect(switcher.find('.attention-dot').exists()).toBe(false)
|
||||||
|
|
||||||
await switcher.trigger('click')
|
await switcher.trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
@@ -810,7 +821,8 @@ describe('Meldwork workbench', () => {
|
|||||||
expect(appSource).toContain(':data-platform="desktopPlatform"')
|
expect(appSource).toContain(':data-platform="desktopPlatform"')
|
||||||
expect(appSource).toContain("const desktopPlatform = computed(() => api.value?.platform || installCatalog.value.platform || '')")
|
expect(appSource).toContain("const desktopPlatform = computed(() => api.value?.platform || installCatalog.value.platform || '')")
|
||||||
expect(source).toMatch(/--shell-gradient:\s*linear-gradient\(145deg,\s*#f1f8f7 0%,\s*#e2efef 48%,\s*#c9dde0 100%\);/)
|
expect(source).toMatch(/--shell-gradient:\s*linear-gradient\(145deg,\s*#f1f8f7 0%,\s*#e2efef 48%,\s*#c9dde0 100%\);/)
|
||||||
expect(source).toMatch(/:root\[data-theme="dark"\]\s*\{[^}]*--shell-gradient:\s*linear-gradient\(145deg,\s*#2d3a3b 0%,\s*#222e32 48%,\s*#172226 100%\);/s)
|
// The dark backdrop stays darker than the workspace card so the card reads as raised.
|
||||||
|
expect(source).toMatch(/:root\[data-theme="dark"\]\s*\{[^}]*--shell-gradient:\s*linear-gradient\(145deg,\s*#1f292b 0%,\s*#182124 48%,\s*#111a1d 100%\);[^}]*--workspace-surface:\s*#1c2124;/s)
|
||||||
expect(source).toMatch(/--onboarding-frame-gradient:\s*linear-gradient\(145deg,\s*#dcefeb 0%,\s*#9fc2ca 48%,\s*#ddb5aa 100%\);/)
|
expect(source).toMatch(/--onboarding-frame-gradient:\s*linear-gradient\(145deg,\s*#dcefeb 0%,\s*#9fc2ca 48%,\s*#ddb5aa 100%\);/)
|
||||||
expect(source).toMatch(/:root\[data-theme="dark"\]\s*\{[^}]*--onboarding-frame-gradient:\s*linear-gradient\(145deg,\s*#4a696b 0%,\s*#2e4751 52%,\s*#775047 100%\);/s)
|
expect(source).toMatch(/:root\[data-theme="dark"\]\s*\{[^}]*--onboarding-frame-gradient:\s*linear-gradient\(145deg,\s*#4a696b 0%,\s*#2e4751 52%,\s*#775047 100%\);/s)
|
||||||
expect(source).toMatch(/\.onboarding-dialog\s*\{[^}]*border:\s*1px solid transparent;[^}]*background:\s*linear-gradient\(var\(--surface\), var\(--surface\)\) padding-box,\s*var\(--onboarding-frame-gradient\) border-box;/s)
|
expect(source).toMatch(/\.onboarding-dialog\s*\{[^}]*border:\s*1px solid transparent;[^}]*background:\s*linear-gradient\(var\(--surface\), var\(--surface\)\) padding-box,\s*var\(--onboarding-frame-gradient\) border-box;/s)
|
||||||
@@ -824,38 +836,24 @@ describe('Meldwork workbench', () => {
|
|||||||
expect(source).toMatch(/\.composer-zone\s*\{[^}]*background:\s*transparent;/s)
|
expect(source).toMatch(/\.composer-zone\s*\{[^}]*background:\s*transparent;/s)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses compact gradient transitions for discovery and first-run onboarding', () => {
|
it('keeps the first-run onboarding transition and loads no WebGL at startup', () => {
|
||||||
const source = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8')
|
const source = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8')
|
||||||
const appSource = readFileSync(resolve(process.cwd(), 'src/App.vue'), 'utf8')
|
const appSource = readFileSync(resolve(process.cwd(), 'src/App.vue'), 'utf8')
|
||||||
const pixelBlastSource = readFileSync(resolve(process.cwd(), 'src/components/PixelBlast.vue'), 'utf8')
|
const packageSource = readFileSync(resolve(process.cwd(), 'package.json'), 'utf8')
|
||||||
|
|
||||||
expect(appSource).toContain('<transition name="onboarding" appear>')
|
expect(appSource).toContain('<transition name="onboarding" appear>')
|
||||||
expect(appSource).toContain('<PixelBlast :theme="theme" />')
|
// Startup used to stack a physics loader and a full-screen WebGL field; it is one small bar now.
|
||||||
expect(source).toContain('--pixel-blast-primary: #d92f24;')
|
expect(appSource).not.toContain('PixelBlast')
|
||||||
expect(source).toContain('--pixel-blast-secondary: #007d91;')
|
expect(appSource).not.toContain('WavePhysicsLoader')
|
||||||
expect(source).toContain('--pixel-blast-tertiary: #ed512f;')
|
expect(appSource).not.toContain('agent-discovery-overlay')
|
||||||
expect(source).toContain('--pixel-blast-primary: #ff836d;')
|
expect(appSource).toContain('<span class="boot-progress" aria-hidden="true"><span /></span>')
|
||||||
expect(source).toContain('--pixel-blast-secondary: #54f5ff;')
|
expect(packageSource).not.toContain('"three"')
|
||||||
expect(source).toContain('--pixel-blast-tertiary: #ffa36f;')
|
expect(source).not.toContain('--pixel-blast-')
|
||||||
expect(pixelBlastSource).toContain('uPixelSize: { value: 3.7 * renderer.getPixelRatio() }')
|
expect(source).toMatch(/@keyframes boot-progress\s*\{[^}]*transform:\s*translateX/s)
|
||||||
expect(pixelBlastSource).toContain('vec2 pixelId = floor(fragCoord / uPixelSize);')
|
|
||||||
expect(pixelBlastSource).toContain('float cellPixelSize = 8.0 * uPixelSize;')
|
|
||||||
expect(pixelBlastSource).toContain('float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;')
|
|
||||||
expect(pixelBlastSource).toContain('float motionTime = uTime * (1.0 + 0.12 * sin(uTime * 0.17));')
|
|
||||||
expect(pixelBlastSource).toContain('vec2 drift = vec2(sin(uTime * 0.41), cos(uTime * 0.33)) * 0.045;')
|
|
||||||
expect(pixelBlastSource).toContain('uDensity: { value: 1.26 }')
|
|
||||||
expect(pixelBlastSource).toContain('uPixelJitter: { value: 0.32 }')
|
|
||||||
expect(pixelBlastSource).toContain('uOpacity: { value: 0.42 }')
|
|
||||||
expect(pixelBlastSource).toContain('Math.min(32, now - previousFrame) * 0.00036')
|
|
||||||
expect(pixelBlastSource).not.toContain('forceContextLoss()')
|
|
||||||
expect(source).toMatch(/\.agent-discovery-leave-active\s*\{[^}]*transition:\s*opacity 0\.28s ease;/s)
|
|
||||||
expect(source).toMatch(/\.onboarding-leave-active\s*\{[^}]*transition:\s*opacity 0\.42s ease;/s)
|
expect(source).toMatch(/\.onboarding-leave-active\s*\{[^}]*transition:\s*opacity 0\.42s ease;/s)
|
||||||
expect(source).toMatch(/\.onboarding-leave-active \.onboarding-backdrop\s*\{[^}]*transition:\s*background-color 0\.42s ease;/s)
|
expect(source).toMatch(/\.onboarding-leave-active \.onboarding-backdrop\s*\{[^}]*transition:\s*background-color 0\.42s ease;/s)
|
||||||
expect(source).toMatch(/\.onboarding-leave-to\s*\{[^}]*opacity:\s*0;/s)
|
expect(source).toMatch(/\.onboarding-leave-to\s*\{[^}]*opacity:\s*0;/s)
|
||||||
expect(source).toMatch(/\.onboarding-leave-to \.onboarding-dialog\s*\{[^}]*opacity:\s*0;[^}]*transform:\s*translateY\(14px\) scale\(0\.975\);/s)
|
expect(source).toMatch(/\.onboarding-leave-to \.onboarding-dialog\s*\{[^}]*opacity:\s*0;[^}]*transform:\s*translateY\(14px\) scale\(0\.975\);/s)
|
||||||
expect(source).not.toContain('@keyframes agent-discovery-spin')
|
|
||||||
expect(source).not.toContain('.agent-discovery-orbit-ring')
|
|
||||||
expect(source).not.toContain('.agent-discovery-pixel-field')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses one settings entry and a borderless Agent mention menu', () => {
|
it('uses one settings entry and a borderless Agent mention menu', () => {
|
||||||
@@ -881,17 +879,20 @@ describe('Meldwork workbench', () => {
|
|||||||
expect(source).toMatch(/\.direct-session-open\s*\{[^}]*grid-column:\s*1;/s)
|
expect(source).toMatch(/\.direct-session-open\s*\{[^}]*grid-column:\s*1;/s)
|
||||||
expect(source).toMatch(/\.direct-session-action\s*\{[^}]*opacity:\s*0;[^}]*pointer-events:\s*none;/s)
|
expect(source).toMatch(/\.direct-session-action\s*\{[^}]*opacity:\s*0;[^}]*pointer-events:\s*none;/s)
|
||||||
expect(source).toMatch(/\.direct-session-row:hover \.direct-session-action,[^{]+\.direct-session-row:focus-within \.direct-session-action\s*\{[^}]*opacity:\s*0\.62;[^}]*pointer-events:\s*auto;/s)
|
expect(source).toMatch(/\.direct-session-row:hover \.direct-session-action,[^{]+\.direct-session-row:focus-within \.direct-session-action\s*\{[^}]*opacity:\s*0\.62;[^}]*pointer-events:\s*auto;/s)
|
||||||
expect(source).toMatch(/\.direct-session-row > \.run-mark,[^{]+\.direct-session-row > \.run-finished-mark\s*\{[^}]*grid-column:\s*4;[^}]*grid-row:\s*1;[^}]*justify-self:\s*end;/s)
|
expect(source).toMatch(/\.direct-session-row > \.run-mark,[^{]+\.direct-session-row > \.run-finished-mark,[^{]+\.direct-session-row > \.attention-mark\s*\{[^}]*grid-column:\s*4;[^}]*grid-row:\s*1;[^}]*justify-self:\s*end;/s)
|
||||||
expect(source).toMatch(/\.run-agent-row\s*\{[^}]*grid-template-columns:\s*32px 20px;[^}]*grid-template-areas:\s*"avatar state"/s)
|
expect(source).toMatch(/\.run-agent-row\s*\{[^}]*grid-template-columns:\s*32px 20px;[^}]*grid-template-areas:\s*"avatar state"/s)
|
||||||
expect(source).toMatch(/\.run-agent-state\s*\{[^}]*justify-content:\s*flex-end;/s)
|
expect(source).toMatch(/\.run-agent-state\s*\{[^}]*justify-content:\s*flex-end;/s)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('hides all visible scrollbars while preserving scroll behavior', () => {
|
it('hides scrollbars on the shell, chat page and dialogs but shows thin ones on long lists and panels', () => {
|
||||||
const source = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8')
|
const source = readFileSync(resolve(process.cwd(), 'src/styles.css'), 'utf8')
|
||||||
|
|
||||||
expect(source).toMatch(/html,\s*body,\s*#app\s*\{[^}]*scrollbar-width:\s*none;[^}]*-ms-overflow-style:\s*none;/s)
|
expect(source).toMatch(/html,\s*body,\s*#app\s*\{[^}]*scrollbar-width:\s*none;[^}]*-ms-overflow-style:\s*none;/s)
|
||||||
expect(source).toMatch(/html::-webkit-scrollbar,[^}]+body::-webkit-scrollbar,[^}]+#app::-webkit-scrollbar,[^}]+\*::\-webkit-scrollbar\s*\{[^}]*width:\s*0;[^}]*height:\s*0;/s)
|
expect(source).toMatch(/html::-webkit-scrollbar,[^}]+body::-webkit-scrollbar,[^}]+#app::-webkit-scrollbar,[^}]+\*::\-webkit-scrollbar\s*\{[^}]*width:\s*0;[^}]*height:\s*0;/s)
|
||||||
expect(source).not.toMatch(/scrollbar-width:\s*thin;/)
|
// Long sidebar lists, run details and settings show where you are; the chat page and dialogs stay clean.
|
||||||
|
expect(source).toMatch(/\.conversation-nav,[^{]*\.trace-panel-scroll,[^{]*\.settings-panel,[^{]*\{[^}]*scrollbar-width:\s*thin;[^}]*scrollbar-color:\s*var\(--scrollbar-thumb\) transparent;/s)
|
||||||
|
expect(source).not.toMatch(/\.message-scroll,[^{]*\{[^}]*scrollbar-width:\s*thin;/s)
|
||||||
|
expect(source).not.toMatch(/\.modal-body,[^{]*\{[^}]*scrollbar-width:\s*thin;/s)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses a distinct Meldwork palette and separates composer controls', () => {
|
it('uses a distinct Meldwork palette and separates composer controls', () => {
|
||||||
@@ -917,7 +918,9 @@ describe('Meldwork workbench', () => {
|
|||||||
expect(source).toMatch(/\.message-row\.agent \.message-body:hover \.message-meta-actions > button,[^{]+\.message-row\.agent \.message-body:hover \.message-footer-actions > button/s)
|
expect(source).toMatch(/\.message-row\.agent \.message-body:hover \.message-meta-actions > button,[^{]+\.message-row\.agent \.message-body:hover \.message-footer-actions > button/s)
|
||||||
expect(source).toMatch(/\.message-meta-actions > button::after,[^{]+\.message-footer-actions > button::after,[^{]+\.code-copy-button::after\s*\{[^}]*bottom:\s*calc\(100% \+ 6px\);[^}]*background:\s*var\(--surface-raised\);/s)
|
expect(source).toMatch(/\.message-meta-actions > button::after,[^{]+\.message-footer-actions > button::after,[^{]+\.code-copy-button::after\s*\{[^}]*bottom:\s*calc\(100% \+ 6px\);[^}]*background:\s*var\(--surface-raised\);/s)
|
||||||
expect(source).toMatch(/\.message-footer-actions > button::after\s*\{[^}]*top:\s*calc\(100% \+ 6px\);[^}]*bottom:\s*auto;/s)
|
expect(source).toMatch(/\.message-footer-actions > button::after\s*\{[^}]*top:\s*calc\(100% \+ 6px\);[^}]*bottom:\s*auto;/s)
|
||||||
expect(source).toMatch(/\.conversation-link\s*\{[^}]*grid-template-columns:\s*26px minmax\(0, 1fr\) 58px;/s)
|
// Group names use the full row; row actions float over the end with a fade instead of reserving a column.
|
||||||
|
expect(source).toMatch(/\.conversation-link\s*\{[^}]*grid-template-columns:\s*26px minmax\(0, 1fr\) auto;/s)
|
||||||
|
expect(source).toMatch(/\.group-conversation-actions\s*\{[^}]*position:\s*absolute;[^}]*background:\s*linear-gradient/s)
|
||||||
expect(source).toMatch(/\.conversation-empty-wordmark\s*\{[^}]*width:\s*min\(276px, 60vw\);/s)
|
expect(source).toMatch(/\.conversation-empty-wordmark\s*\{[^}]*width:\s*min\(276px, 60vw\);/s)
|
||||||
expect(source).toMatch(/\.conversation-empty-copy\s*\{[^}]*width:\s*min\(920px, 100%\);[^}]*min-height:\s*60px;/s)
|
expect(source).toMatch(/\.conversation-empty-copy\s*\{[^}]*width:\s*min\(920px, 100%\);[^}]*min-height:\s*60px;/s)
|
||||||
expect(source).not.toContain('img[src$="/hermes.svg"],\n:root[data-theme="dark"] img[src$="/opencode.svg"]')
|
expect(source).not.toContain('img[src$="/hermes.svg"],\n:root[data-theme="dark"] img[src$="/opencode.svg"]')
|
||||||
|
|||||||
@@ -91,6 +91,55 @@ describe('Agent compatibility state', () => {
|
|||||||
expect(agentLogo('cloud-0123456789abcdef01234567')).toContain('agent-logos/codex.svg')
|
expect(agentLogo('cloud-0123456789abcdef01234567')).toContain('agent-logos/codex.svg')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('gives Gemini its own logo and name instead of the generic custom-Agent icon', () => {
|
||||||
|
setLocale('en')
|
||||||
|
const cloudAgent = {
|
||||||
|
kind: 'cloud-fedcba987654321012345678',
|
||||||
|
sourceKind: 'gemini',
|
||||||
|
label: 'Gemini @ Server',
|
||||||
|
custom: false,
|
||||||
|
connector: true,
|
||||||
|
cloud: true,
|
||||||
|
installed: true,
|
||||||
|
available: true,
|
||||||
|
credentialState: 'ready',
|
||||||
|
showInSidebar: true,
|
||||||
|
}
|
||||||
|
const catalog = useAgentCatalog({
|
||||||
|
activeGroup: ref(null),
|
||||||
|
directGroupsFor: () => [],
|
||||||
|
installCatalog: ref({ agents: [{ kind: 'gemini', name: 'Gemini CLI', installed: true }, cloudAgent] }),
|
||||||
|
snapshot: ref({ agents: [
|
||||||
|
{ kind: 'gemini', name: 'Gemini CLI', installed: true, available: true, credentialState: 'ready' },
|
||||||
|
cloudAgent,
|
||||||
|
] }),
|
||||||
|
t,
|
||||||
|
theme: ref('light'),
|
||||||
|
})
|
||||||
|
|
||||||
|
const gemini = catalog.mergedCatalog.value.find(agent => agent.kind === 'gemini')
|
||||||
|
expect(gemini.label).toBe('Gemini CLI')
|
||||||
|
expect(gemini.logo).toContain('agent-logos/gemini.svg')
|
||||||
|
expect(catalog.agentCatalogGroups.value.find(group => group.id === 'official').agents.map(agent => agent.kind))
|
||||||
|
.toContain('gemini')
|
||||||
|
expect(catalog.mergedCatalog.value.find(agent => agent.kind === cloudAgent.kind).logo)
|
||||||
|
.toContain('agent-logos/gemini.svg')
|
||||||
|
setCloudAgentProfiles([cloudAgent])
|
||||||
|
expect(agentLogo(cloudAgent.kind)).toContain('agent-logos/gemini.svg')
|
||||||
|
expect(agentLogo('gemini', 'dark')).toContain('agent-logos/gemini.svg')
|
||||||
|
setCloudAgentProfiles([])
|
||||||
|
|
||||||
|
const withoutGemini = useAgentCatalog({
|
||||||
|
activeGroup: ref(null),
|
||||||
|
directGroupsFor: () => [],
|
||||||
|
installCatalog: ref({ agents: [] }),
|
||||||
|
snapshot: ref({ agents: [] }),
|
||||||
|
t,
|
||||||
|
theme: ref('light'),
|
||||||
|
})
|
||||||
|
expect(withoutGemini.mergedCatalog.value.map(agent => agent.kind)).toEqual(AGENTS.map(agent => agent.kind))
|
||||||
|
})
|
||||||
|
|
||||||
it('keeps an installed cloud Agent that needs login in management but out of chat choices', () => {
|
it('keeps an installed cloud Agent that needs login in management but out of chat choices', () => {
|
||||||
setLocale('en')
|
setLocale('en')
|
||||||
const cloudAgent = {
|
const cloudAgent = {
|
||||||
@@ -167,4 +216,65 @@ describe('Agent compatibility state', () => {
|
|||||||
})
|
})
|
||||||
expect(state.label).toBe('Detection timed out; retry')
|
expect(state.label).toBe('Detection timed out; retry')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('explaining why an installed Agent is unavailable', () => {
|
||||||
|
function noteFor(agent) {
|
||||||
|
setLocale('en')
|
||||||
|
const catalog = useAgentCatalog({
|
||||||
|
activeGroup: ref(null),
|
||||||
|
directGroupsFor: () => [],
|
||||||
|
installCatalog: ref({ agents: [] }),
|
||||||
|
snapshot: ref({ agents: [agent] }),
|
||||||
|
t,
|
||||||
|
theme: ref('light'),
|
||||||
|
})
|
||||||
|
return catalog.agentStateNote(catalog.mergedCatalog.value.find(item => item.kind === agent.kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
it('names the installed version, the verified range, and why the CLI failed its startup check', () => {
|
||||||
|
const note = noteFor({
|
||||||
|
kind: 'workbuddy', installed: true, available: false,
|
||||||
|
resolvedVersion: '2.161.0', supportedVersionRange: '2.115.0..2.132.0',
|
||||||
|
compatibilityState: 'incompatible', incompatibilityReason: 'LOCAL_AGENT_REQUIRED_CAPABILITY_MISSING',
|
||||||
|
})
|
||||||
|
expect(note).toContain('2.161.0')
|
||||||
|
expect(note).toContain('did not pass the startup check')
|
||||||
|
expect(note).toContain('Verified versions: 2.115.0..2.132.0.')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('distinguishes an unsupported version, a missing sign-in, and a detection timeout', () => {
|
||||||
|
expect(noteFor({
|
||||||
|
kind: 'codex', installed: true, available: false, resolvedVersion: '9.0.0',
|
||||||
|
supportedVersionRange: '0.137.0..0.146.0',
|
||||||
|
compatibilityState: 'incompatible', incompatibilityReason: 'LOCAL_AGENT_VERSION_UNSUPPORTED',
|
||||||
|
})).toContain('outside the range Meldwork has verified')
|
||||||
|
expect(noteFor({
|
||||||
|
kind: 'codex', installed: true, available: false, credentialState: 'missing',
|
||||||
|
})).toContain('finish signing in')
|
||||||
|
expect(noteFor({
|
||||||
|
kind: 'codex', installed: true, available: false,
|
||||||
|
incompatibilityReason: 'LOCAL_AGENT_CAPABILITY_PROBE_TIMEOUT',
|
||||||
|
})).toContain('Detection timed out')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sends a cloud Agent to its server to sign in, not to a local terminal', () => {
|
||||||
|
const note = noteFor({
|
||||||
|
kind: 'cloud-0123456789abcdef01234567', sourceKind: 'claude', cloud: true,
|
||||||
|
installed: true, available: false, credentialState: 'missing',
|
||||||
|
})
|
||||||
|
expect(note).toContain('cloud Agent')
|
||||||
|
expect(note).toContain('on its server')
|
||||||
|
expect(note).not.toContain('terminal')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('stays silent when there is nothing to explain', () => {
|
||||||
|
expect(noteFor({ kind: 'codex', installed: true, available: true })).toBe('')
|
||||||
|
expect(noteFor({ kind: 'codex', installed: false, available: false })).toBe('')
|
||||||
|
// Without a verified range it still explains, just without the range sentence.
|
||||||
|
expect(noteFor({
|
||||||
|
kind: 'codex', installed: true, available: false,
|
||||||
|
compatibilityState: 'incompatible', incompatibilityReason: 'LOCAL_AGENT_VERSION_UNSUPPORTED',
|
||||||
|
})).not.toContain('Verified versions')
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { defineComponent, h } from 'vue'
|
||||||
|
import { useAppNotifications } from '../../composables/useAppNotifications.js'
|
||||||
|
import { setLocale } from '../../i18n.js'
|
||||||
|
|
||||||
|
function mountNotifications() {
|
||||||
|
let api = null
|
||||||
|
const wrapper = mount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
api = useAppNotifications()
|
||||||
|
return () => h('div')
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return { api, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||||
|
setLocale('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers()
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('app notifications', () => {
|
||||||
|
it('keeps short notices for four seconds and longer ones long enough to read', () => {
|
||||||
|
const { api, wrapper } = mountNotifications()
|
||||||
|
|
||||||
|
api.notify('Saved.')
|
||||||
|
vi.advanceTimersByTime(3900)
|
||||||
|
expect(api.toastMessage.value).toBe('Saved.')
|
||||||
|
vi.advanceTimersByTime(600)
|
||||||
|
expect(api.toastMessage.value).toBe('')
|
||||||
|
|
||||||
|
const long = 'x'.repeat(80)
|
||||||
|
api.notify(long)
|
||||||
|
// 4 s + 80 × 60 ms = 8.8 s.
|
||||||
|
vi.advanceTimersByTime(8700)
|
||||||
|
expect(api.toastMessage.value).toBe(long)
|
||||||
|
vi.advanceTimersByTime(200)
|
||||||
|
expect(api.toastMessage.value).toBe('')
|
||||||
|
|
||||||
|
api.notify('y'.repeat(1000))
|
||||||
|
vi.advanceTimersByTime(10_100)
|
||||||
|
expect(api.toastMessage.value).toBe('')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('pauses while hovered and restarts the countdown when the pointer leaves', () => {
|
||||||
|
const { api, wrapper } = mountNotifications()
|
||||||
|
|
||||||
|
api.notify('Check the folder permissions.')
|
||||||
|
api.pauseToast()
|
||||||
|
vi.advanceTimersByTime(20_000)
|
||||||
|
expect(api.toastMessage.value).toBe('Check the folder permissions.')
|
||||||
|
|
||||||
|
api.resumeToast()
|
||||||
|
vi.advanceTimersByTime(3000)
|
||||||
|
expect(api.toastMessage.value).toBe('Check the folder permissions.')
|
||||||
|
vi.advanceTimersByTime(3000)
|
||||||
|
expect(api.toastMessage.value).toBe('')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks failures as errors and plain notices as warnings', () => {
|
||||||
|
const { api, wrapper } = mountNotifications()
|
||||||
|
|
||||||
|
api.notify('Select at least one Agent.')
|
||||||
|
expect(api.toastTone.value).toBe('warning')
|
||||||
|
|
||||||
|
api.showError(new Error('LOCAL_AGENT_TIMEOUT'))
|
||||||
|
expect(api.toastTone.value).toBe('error')
|
||||||
|
expect(api.toastMessage.value).toContain('took too long to respond')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import { flushPromises } from '@vue/test-utils'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { setLocale } from '../../i18n.js'
|
||||||
|
import { mountApp } from './app-test-harness.js'
|
||||||
|
|
||||||
|
const gateId = `human-gate-${'e'.repeat(64)}`
|
||||||
|
|
||||||
|
function addPendingDecision(state) {
|
||||||
|
state.groups.push(
|
||||||
|
{
|
||||||
|
id: 'group-needs-you',
|
||||||
|
conversationType: 'group',
|
||||||
|
name: 'Release review',
|
||||||
|
topic: '',
|
||||||
|
agentKinds: ['codex', 'hermes'],
|
||||||
|
workdir: '/tmp/meldwork-workspace',
|
||||||
|
allowWrite: false,
|
||||||
|
createdAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group-quiet',
|
||||||
|
conversationType: 'group',
|
||||||
|
name: 'Quiet group',
|
||||||
|
topic: '',
|
||||||
|
agentKinds: ['codex', 'hermes'],
|
||||||
|
workdir: '/tmp/meldwork-workspace',
|
||||||
|
allowWrite: false,
|
||||||
|
createdAt: '2026-08-04T07:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-04T07:00:00.000Z',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
state.messages.push({
|
||||||
|
id: 'needs-you-root',
|
||||||
|
groupId: 'group-needs-you',
|
||||||
|
role: 'user',
|
||||||
|
content: 'Check the release',
|
||||||
|
targetKinds: ['codex', 'hermes'],
|
||||||
|
createdAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
})
|
||||||
|
state.runningGroupIds = ['group-needs-you']
|
||||||
|
state.runs = [{
|
||||||
|
runId: 'run-needs-you',
|
||||||
|
groupId: 'group-needs-you',
|
||||||
|
threadRootId: 'needs-you-root',
|
||||||
|
targetKinds: ['codex', 'hermes'],
|
||||||
|
currentKind: 'codex',
|
||||||
|
waitingGateIds: [gateId],
|
||||||
|
agentRuns: [{
|
||||||
|
agentRunId: 'agent-needs-you',
|
||||||
|
kind: 'codex',
|
||||||
|
round: 1,
|
||||||
|
status: 'waiting',
|
||||||
|
output: '',
|
||||||
|
events: [],
|
||||||
|
}],
|
||||||
|
}]
|
||||||
|
state.humanGates = [{
|
||||||
|
gateId,
|
||||||
|
type: 'permission',
|
||||||
|
runId: 'run-needs-you',
|
||||||
|
agentRunId: 'agent-needs-you',
|
||||||
|
agentKind: 'codex',
|
||||||
|
summary: 'Agent requests permission to continue a tool action.',
|
||||||
|
options: [
|
||||||
|
{ optionId: 'allow-once', name: 'Allow once', kind: 'allow_once' },
|
||||||
|
{ optionId: 'reject-once', name: 'Reject', kind: 'reject_once' },
|
||||||
|
],
|
||||||
|
status: 'pending',
|
||||||
|
createdAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
localStorage.setItem('meldwork-theme', 'light')
|
||||||
|
localStorage.setItem('meldwork-onboarding-seen-v1', '1')
|
||||||
|
setLocale('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete window.meldworkDesktop
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('decisions waiting on the user', () => {
|
||||||
|
it('marks only the waiting conversation in the sidebar and lists it on the home page', async () => {
|
||||||
|
const { wrapper } = await mountApp(({ state }) => addPendingDecision(state))
|
||||||
|
|
||||||
|
const rows = wrapper.findAll('.group-conversation-row')
|
||||||
|
const waitingRow = rows.find(row => row.text().includes('Release review'))
|
||||||
|
const quietRow = rows.find(row => row.text().includes('Quiet group'))
|
||||||
|
// The decision mark replaces the running bars, so the row reads "needs you", not "busy".
|
||||||
|
expect(waitingRow.find('.attention-mark').attributes('aria-label')).toBe('Waiting for your decision')
|
||||||
|
expect(waitingRow.find('.run-mark').exists()).toBe(false)
|
||||||
|
expect(quietRow.find('.attention-mark').exists()).toBe(false)
|
||||||
|
|
||||||
|
const item = wrapper.get('.home-attention-item')
|
||||||
|
expect(wrapper.get('.home-attention-header').text()).toContain('Needs your attention')
|
||||||
|
expect(item.text()).toContain('Codex is waiting for you: Permission')
|
||||||
|
expect(item.text()).toContain('Release review')
|
||||||
|
|
||||||
|
await item.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.get('.conversation-header h1').text()).toBe('Release review')
|
||||||
|
// Once the conversation is open, its own row no longer needs the mark.
|
||||||
|
expect(wrapper.findAll('.group-conversation-row')
|
||||||
|
.find(row => row.text().includes('Release review'))
|
||||||
|
.find('.attention-mark').exists()).toBe(false)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('shows a dot on the collapsed group switcher instead of a total count', async () => {
|
||||||
|
const { wrapper } = await mountApp(({ state }) => addPendingDecision(state))
|
||||||
|
|
||||||
|
await wrapper.get('.sidebar-toggle').trigger('click')
|
||||||
|
const switcher = wrapper.get('.collapsed-group-switcher-button')
|
||||||
|
expect(switcher.text()).toBe('')
|
||||||
|
expect(switcher.find('.attention-dot').exists()).toBe(true)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps the home page free of the attention list when nothing is waiting', async () => {
|
||||||
|
const { wrapper } = await mountApp()
|
||||||
|
|
||||||
|
expect(wrapper.find('.home-attention').exists()).toBe(false)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('marks a group discussion that finished while you were elsewhere, then clears it when opened', async () => {
|
||||||
|
const { wrapper, state, emitRunFinished } = await mountApp(({ state: fixture }) => {
|
||||||
|
fixture.groups.push(
|
||||||
|
{
|
||||||
|
id: 'group-done',
|
||||||
|
conversationType: 'group',
|
||||||
|
name: 'Finished discussion',
|
||||||
|
topic: '',
|
||||||
|
agentKinds: ['codex', 'hermes'],
|
||||||
|
workdir: '/tmp/meldwork-workspace',
|
||||||
|
allowWrite: false,
|
||||||
|
createdAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'group-other',
|
||||||
|
conversationType: 'group',
|
||||||
|
name: 'Where I am now',
|
||||||
|
topic: '',
|
||||||
|
agentKinds: ['codex', 'hermes'],
|
||||||
|
workdir: '/tmp/meldwork-workspace',
|
||||||
|
allowWrite: false,
|
||||||
|
createdAt: '2026-08-04T07:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-04T07:00:00.000Z',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
fixture.messages.push({
|
||||||
|
id: 'done-root',
|
||||||
|
groupId: 'group-done',
|
||||||
|
role: 'user',
|
||||||
|
content: 'Discuss the plan',
|
||||||
|
targetKinds: ['codex', 'hermes'],
|
||||||
|
createdAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
void state
|
||||||
|
wrapper.vm.selectGroup('group-other')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
// A failed or stopped run is not a "finished" cue.
|
||||||
|
emitRunFinished({ groupId: 'group-done', threadRootId: 'done-root', status: 'failed' })
|
||||||
|
await flushPromises()
|
||||||
|
const doneRow = () => wrapper.findAll('.group-conversation-row').find(row => row.text().includes('Finished discussion'))
|
||||||
|
expect(doneRow().find('.run-finished-mark').exists()).toBe(false)
|
||||||
|
|
||||||
|
emitRunFinished({ groupId: 'group-done', threadRootId: 'done-root', status: 'completed' })
|
||||||
|
await flushPromises()
|
||||||
|
expect(doneRow().get('.run-finished-mark').attributes('aria-label')).toBe('Run finished')
|
||||||
|
|
||||||
|
wrapper.vm.selectGroup('group-done')
|
||||||
|
await flushPromises()
|
||||||
|
wrapper.vm.selectGroup('group-other')
|
||||||
|
await flushPromises()
|
||||||
|
expect(doneRow().find('.run-finished-mark').exists()).toBe(false)
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
import CloudAgentBridgePanel from '../../components/CloudAgentBridgePanel.vue'
|
||||||
import { useCloudAgentBridges } from '../../composables/useCloudAgentBridges.js'
|
import { useCloudAgentBridges } from '../../composables/useCloudAgentBridges.js'
|
||||||
|
import { setLocale } from '../../i18n.js'
|
||||||
|
|
||||||
describe('Cloud Agent bridges', () => {
|
describe('Cloud Agent bridges', () => {
|
||||||
it('refreshes the local Agent snapshot after connecting a server', async () => {
|
it('refreshes the local Agent snapshot after connecting a server', async () => {
|
||||||
@@ -17,4 +20,37 @@ describe('Cloud Agent bridges', () => {
|
|||||||
expect(bridgeApi.value.connect).toHaveBeenCalledWith({ address: '10.1.132.21' })
|
expect(bridgeApi.value.connect).toHaveBeenCalledWith({ address: '10.1.132.21' })
|
||||||
expect(refreshAgents).toHaveBeenCalledTimes(1)
|
expect(refreshAgents).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('asks before deleting a connection and deletes only after confirming', async () => {
|
||||||
|
setLocale('en')
|
||||||
|
const remove = vi.fn(async () => {})
|
||||||
|
const wrapper = mount(CloudAgentBridgePanel, {
|
||||||
|
props: {
|
||||||
|
controller: {
|
||||||
|
address: ref(''),
|
||||||
|
bridges: ref([{ bridgeId: 'bridge-1', label: 'Lab server', address: '203.0.113.10', available: true, transport: 'ssh', agents: [] }]),
|
||||||
|
connect: vi.fn(),
|
||||||
|
error: ref(''),
|
||||||
|
label: ref(''),
|
||||||
|
loading: ref(false),
|
||||||
|
refresh: vi.fn(),
|
||||||
|
remove,
|
||||||
|
saving: ref(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.get('[aria-label="Delete connection"]').trigger('click')
|
||||||
|
expect(remove).not.toHaveBeenCalled()
|
||||||
|
expect(wrapper.get('.cloud-agent-bridge-confirm').text()).toContain('Delete this connection?')
|
||||||
|
|
||||||
|
await wrapper.get('.cloud-agent-bridge-confirm .secondary-button').trigger('click')
|
||||||
|
expect(wrapper.find('.cloud-agent-bridge-confirm').exists()).toBe(false)
|
||||||
|
expect(remove).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
await wrapper.get('[aria-label="Delete connection"]').trigger('click')
|
||||||
|
await wrapper.get('.cloud-agent-bridge-confirm .danger-button').trigger('click')
|
||||||
|
expect(remove).toHaveBeenCalledWith('bridge-1')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import { flushPromises } from '@vue/test-utils'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||||
|
import { setLocale } from '../../i18n.js'
|
||||||
|
import { mountApp } from './app-test-harness.js'
|
||||||
|
|
||||||
|
function addGroups(state) {
|
||||||
|
for (const [id, name] of [['group-a', 'Alpha review'], ['group-b', 'Beta review']]) {
|
||||||
|
state.groups.push({
|
||||||
|
id,
|
||||||
|
conversationType: 'group',
|
||||||
|
name,
|
||||||
|
topic: '',
|
||||||
|
agentKinds: ['codex', 'hermes'],
|
||||||
|
workdir: '/tmp/meldwork-workspace',
|
||||||
|
allowWrite: false,
|
||||||
|
createdAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-04T08:00:00.000Z',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
localStorage.setItem('meldwork-theme', 'light')
|
||||||
|
localStorage.setItem('meldwork-onboarding-seen-v1', '1')
|
||||||
|
setLocale('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete window.meldworkDesktop
|
||||||
|
document.body.className = ''
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
async function open(wrapper, groupId) {
|
||||||
|
wrapper.vm.selectGroup(groupId)
|
||||||
|
await flushPromises()
|
||||||
|
return wrapper.get('.composer-box textarea')
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('composer drafts', () => {
|
||||||
|
it('keeps unsent text for each conversation when switching between them', async () => {
|
||||||
|
const { wrapper } = await mountApp(({ state }) => addGroups(state))
|
||||||
|
|
||||||
|
let input = await open(wrapper, 'group-a')
|
||||||
|
await input.setValue('Compare the two migration plans')
|
||||||
|
input = await open(wrapper, 'group-b')
|
||||||
|
// Another conversation starts clean...
|
||||||
|
expect(input.element.value).toBe('')
|
||||||
|
await input.setValue('Summarise the open risks')
|
||||||
|
|
||||||
|
// ...and each one gets its own text back.
|
||||||
|
input = await open(wrapper, 'group-a')
|
||||||
|
expect(input.element.value).toBe('Compare the two migration plans')
|
||||||
|
input = await open(wrapper, 'group-b')
|
||||||
|
expect(input.element.value).toBe('Summarise the open risks')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('forgets a draft once it has been sent', async () => {
|
||||||
|
const { wrapper, bridge } = await mountApp(({ state }) => addGroups(state))
|
||||||
|
|
||||||
|
let input = await open(wrapper, 'group-a')
|
||||||
|
await input.setValue('Review the release notes')
|
||||||
|
await wrapper.get('.send-button').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(bridge.localWorkspace.send).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
await open(wrapper, 'group-b')
|
||||||
|
input = await open(wrapper, 'group-a')
|
||||||
|
expect(input.element.value).toBe('')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not bring back text typed into a conversation that was deleted', async () => {
|
||||||
|
const { wrapper, state, emitWorkspaceChanged } = await mountApp(({ state: fixture }) => addGroups(fixture))
|
||||||
|
|
||||||
|
const input = await open(wrapper, 'group-a')
|
||||||
|
await input.setValue('This conversation is about to disappear')
|
||||||
|
await open(wrapper, 'group-b')
|
||||||
|
|
||||||
|
state.groups = state.groups.filter(group => group.id !== 'group-a')
|
||||||
|
emitWorkspaceChanged()
|
||||||
|
await flushPromises()
|
||||||
|
state.groups.push({
|
||||||
|
id: 'group-a',
|
||||||
|
conversationType: 'group',
|
||||||
|
name: 'Alpha review',
|
||||||
|
topic: '',
|
||||||
|
agentKinds: ['codex', 'hermes'],
|
||||||
|
workdir: '/tmp/meldwork-workspace',
|
||||||
|
allowWrite: false,
|
||||||
|
createdAt: '2026-08-04T09:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-04T09:00:00.000Z',
|
||||||
|
})
|
||||||
|
emitWorkspaceChanged()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect((await open(wrapper, 'group-a')).element.value).toBe('')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -44,8 +44,6 @@ describe('Meldwork i18n', () => {
|
|||||||
it('localizes Human Gates, Agent controls, and budget metadata', () => {
|
it('localizes Human Gates, Agent controls, and budget metadata', () => {
|
||||||
setLocale('en')
|
setLocale('en')
|
||||||
expect(t('humanGate.waiting')).toBe('Waiting for your decision')
|
expect(t('humanGate.waiting')).toBe('Waiting for your decision')
|
||||||
expect(t('humanGate.waitingWordOne')).toBe('Waiting')
|
|
||||||
expect(t('humanGate.waitingWordFour')).toBe('Reviewing')
|
|
||||||
expect(t('humanGate.option.allowOnce')).toBe('Allow once')
|
expect(t('humanGate.option.allowOnce')).toBe('Allow once')
|
||||||
expect(t('humanGate.summary.decision')).toBe('This run requires your decision.')
|
expect(t('humanGate.summary.decision')).toBe('This run requires your decision.')
|
||||||
expect(t('humanGate.summary.retry')).toContain('may already have changed')
|
expect(t('humanGate.summary.retry')).toContain('may already have changed')
|
||||||
@@ -68,13 +66,12 @@ describe('Meldwork i18n', () => {
|
|||||||
expect(t('run.phase.reconcile')).toBe('Reconciling results')
|
expect(t('run.phase.reconcile')).toBe('Reconciling results')
|
||||||
expect(t('run.phase.commit')).toBe('Committing delivery')
|
expect(t('run.phase.commit')).toBe('Committing delivery')
|
||||||
expect(t('run.phase.failed')).toBe('Failed')
|
expect(t('run.phase.failed')).toBe('Failed')
|
||||||
expect(t('run.status.circuitBreaker')).toBe('Circuit breaker stopped')
|
// Status text says what happened instead of naming the mechanism.
|
||||||
expect(t('system.agentBudgetExhausted', { agent: 'Codex' })).toContain('hard run budget')
|
expect(t('run.status.circuitBreaker')).toBe('Stopped after repeated failures')
|
||||||
|
expect(t('system.agentBudgetExhausted', { agent: 'Codex' })).toContain('usage limit')
|
||||||
|
|
||||||
setLocale('zh')
|
setLocale('zh')
|
||||||
expect(t('humanGate.waiting')).toBe('等待你的决定')
|
expect(t('humanGate.waiting')).toBe('等待你的决定')
|
||||||
expect(t('humanGate.waitingWordOne')).toBe('等待中')
|
|
||||||
expect(t('humanGate.waitingWordFour')).toBe('整理中')
|
|
||||||
expect(t('humanGate.option.allowOnce')).toBe('允许一次')
|
expect(t('humanGate.option.allowOnce')).toBe('允许一次')
|
||||||
expect(t('humanGate.summary.decision')).toBe('该运行需要你做出决定。')
|
expect(t('humanGate.summary.decision')).toBe('该运行需要你做出决定。')
|
||||||
expect(t('humanGate.summary.retry')).toContain('可能已经修改')
|
expect(t('humanGate.summary.retry')).toContain('可能已经修改')
|
||||||
@@ -97,8 +94,9 @@ describe('Meldwork i18n', () => {
|
|||||||
expect(t('run.phase.dispatch')).toBe('分发 Agent')
|
expect(t('run.phase.dispatch')).toBe('分发 Agent')
|
||||||
expect(t('run.phase.running')).toBe('运行中')
|
expect(t('run.phase.running')).toBe('运行中')
|
||||||
expect(t('run.phase.failed')).toBe('失败')
|
expect(t('run.phase.failed')).toBe('失败')
|
||||||
expect(t('run.status.circuitBreaker')).toBe('已触发熔断器')
|
// User-facing status avoids engineering terms like 熔断器 and 硬预算.
|
||||||
expect(t('system.agentBudgetExhausted', { agent: 'Codex' })).toContain('硬预算上限')
|
expect(t('run.status.circuitBreaker')).toBe('失败过多,已自动停止')
|
||||||
|
expect(t('system.agentBudgetExhausted', { agent: 'Codex' })).toContain('用量上限')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('maps every stable desktop error code from direct and wrapped errors', () => {
|
it('maps every stable desktop error code from direct and wrapped errors', () => {
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { flushPromises } from '@vue/test-utils'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { setLocale } from '../../i18n.js'
|
||||||
|
import { deferred, mountApp } from './app-test-harness.js'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear()
|
||||||
|
localStorage.setItem('meldwork-theme', 'light')
|
||||||
|
setLocale('en')
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks()
|
||||||
|
delete window.meldworkDesktop
|
||||||
|
document.body.className = ''
|
||||||
|
document.body.innerHTML = ''
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('first-run onboarding', () => {
|
||||||
|
it('can be skipped with a visible button while Agent detection is still running', async () => {
|
||||||
|
vi.spyOn(window.history, 'back').mockImplementation(() => {})
|
||||||
|
const pendingDetection = deferred()
|
||||||
|
const { wrapper } = await mountApp(({ bridge }) => {
|
||||||
|
bridge.localWorkspace.refreshAgents.mockReturnValueOnce(pendingDetection.promise)
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.get('.onboarding-primary').attributes()).toHaveProperty('disabled')
|
||||||
|
const skip = wrapper.get('.onboarding-skip')
|
||||||
|
expect(skip.text()).toBe('Skip')
|
||||||
|
await skip.trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(wrapper.find('.onboarding-dialog').exists()).toBe(false)
|
||||||
|
expect(localStorage.getItem('meldwork-onboarding-seen-v1')).toBe('1')
|
||||||
|
// Detection carries on in the background and the window is usable.
|
||||||
|
expect(wrapper.get('.sidebar').attributes()).not.toHaveProperty('inert')
|
||||||
|
pendingDetection.resolve(undefined)
|
||||||
|
await flushPromises()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('moves between slides with the arrow keys', async () => {
|
||||||
|
const { wrapper } = await mountApp()
|
||||||
|
const dialog = wrapper.get('.onboarding-dialog')
|
||||||
|
|
||||||
|
await dialog.trigger('keydown', { key: 'ArrowRight' })
|
||||||
|
expect(wrapper.findAll('.onboarding-dot')[1].attributes('aria-current')).toBe('step')
|
||||||
|
await dialog.trigger('keydown', { key: 'ArrowLeft' })
|
||||||
|
expect(wrapper.findAll('.onboarding-dot')[0].attributes('aria-current')).toBe('step')
|
||||||
|
// The first slide is the floor.
|
||||||
|
await dialog.trigger('keydown', { key: 'ArrowLeft' })
|
||||||
|
expect(wrapper.findAll('.onboarding-dot')[0].attributes('aria-current')).toBe('step')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { mount } from '@vue/test-utils'
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { defineComponent, h, nextTick } from 'vue'
|
||||||
|
import { useAppPreferences } from '../../composables/useAppPreferences.js'
|
||||||
|
|
||||||
|
function fakeColorScheme(initialDark) {
|
||||||
|
const listeners = new Set()
|
||||||
|
const query = {
|
||||||
|
matches: initialDark,
|
||||||
|
addEventListener: vi.fn((_type, listener) => listeners.add(listener)),
|
||||||
|
removeEventListener: vi.fn((_type, listener) => listeners.delete(listener)),
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
query,
|
||||||
|
matchMedia: vi.fn(() => query),
|
||||||
|
setDark(value) {
|
||||||
|
query.matches = value
|
||||||
|
for (const listener of listeners) listener({ matches: value })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mountPreferences() {
|
||||||
|
let api = null
|
||||||
|
const wrapper = mount(defineComponent({
|
||||||
|
setup() {
|
||||||
|
api = useAppPreferences()
|
||||||
|
return () => h('div')
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
return { api, wrapper }
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => localStorage.clear())
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllGlobals()
|
||||||
|
delete document.documentElement.dataset.theme
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('theme preference', () => {
|
||||||
|
it('follows the system on first launch without saving a fixed theme', async () => {
|
||||||
|
const scheme = fakeColorScheme(false)
|
||||||
|
vi.stubGlobal('matchMedia', scheme.matchMedia)
|
||||||
|
const { api, wrapper } = mountPreferences()
|
||||||
|
|
||||||
|
expect(api.themePreference.value).toBe('system')
|
||||||
|
expect(api.theme.value).toBe('light')
|
||||||
|
expect(localStorage.getItem('meldwork-theme')).toBeNull()
|
||||||
|
|
||||||
|
scheme.setDark(true)
|
||||||
|
await nextTick()
|
||||||
|
expect(api.theme.value).toBe('dark')
|
||||||
|
expect(document.documentElement.dataset.theme).toBe('dark')
|
||||||
|
expect(localStorage.getItem('meldwork-theme')).toBeNull()
|
||||||
|
wrapper.unmount()
|
||||||
|
expect(scheme.query.removeEventListener).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps an explicit choice and stops following the system until System is chosen again', async () => {
|
||||||
|
const scheme = fakeColorScheme(false)
|
||||||
|
vi.stubGlobal('matchMedia', scheme.matchMedia)
|
||||||
|
const { api, wrapper } = mountPreferences()
|
||||||
|
|
||||||
|
api.toggleTheme()
|
||||||
|
await nextTick()
|
||||||
|
expect(api.theme.value).toBe('dark')
|
||||||
|
expect(localStorage.getItem('meldwork-theme')).toBe('dark')
|
||||||
|
|
||||||
|
scheme.setDark(false)
|
||||||
|
await nextTick()
|
||||||
|
expect(api.theme.value).toBe('dark')
|
||||||
|
|
||||||
|
api.setThemePreference('system')
|
||||||
|
await nextTick()
|
||||||
|
expect(api.theme.value).toBe('light')
|
||||||
|
expect(localStorage.getItem('meldwork-theme')).toBe('system')
|
||||||
|
scheme.setDark(true)
|
||||||
|
await nextTick()
|
||||||
|
expect(api.theme.value).toBe('dark')
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('restores a saved explicit theme without listening to the system', () => {
|
||||||
|
localStorage.setItem('meldwork-theme', 'dark')
|
||||||
|
const scheme = fakeColorScheme(false)
|
||||||
|
vi.stubGlobal('matchMedia', scheme.matchMedia)
|
||||||
|
const { api, wrapper } = mountPreferences()
|
||||||
|
|
||||||
|
expect(api.theme.value).toBe('dark')
|
||||||
|
expect(scheme.query.addEventListener).not.toHaveBeenCalled()
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -23,7 +23,12 @@ export const AGENTS = Object.freeze([
|
|||||||
{ kind: 'qwen', label: 'Qwen Code', logo: publicAsset('agent-logos/qwen.svg'), imageLimit: 0, attachmentTypes: ['file'] },
|
{ kind: 'qwen', label: 'Qwen Code', logo: publicAsset('agent-logos/qwen.svg'), imageLimit: 0, attachmentTypes: ['file'] },
|
||||||
])
|
])
|
||||||
|
|
||||||
const BY_KIND = new Map(AGENTS.map(agent => [agent.kind, agent]))
|
// Detected and runnable, but not in AGENTS: listing it there would show a "Not installed" card to everyone.
|
||||||
|
const DETECTED_ONLY_AGENTS = Object.freeze([
|
||||||
|
{ kind: 'gemini', label: 'Gemini CLI', logo: publicAsset('agent-logos/gemini.svg') },
|
||||||
|
])
|
||||||
|
|
||||||
|
const BY_KIND = new Map([...AGENTS, ...DETECTED_ONLY_AGENTS].map(agent => [agent.kind, agent]))
|
||||||
const CUSTOM_BY_KIND = new Map()
|
const CUSTOM_BY_KIND = new Map()
|
||||||
const CLOUD_BY_KIND = new Map()
|
const CLOUD_BY_KIND = new Map()
|
||||||
const CUSTOM_AGENT_KIND = /^custom-[a-f0-9]{16}$/
|
const CUSTOM_AGENT_KIND = /^custom-[a-f0-9]{16}$/
|
||||||
|
|||||||
@@ -57,6 +57,12 @@
|
|||||||
<div v-if="installerState.kind === agent.kind && installerState.phase !== 'idle'" class="install-progress">
|
<div v-if="installerState.kind === agent.kind && installerState.phase !== 'idle'" class="install-progress">
|
||||||
<span>{{ installerPhaseLabel }}</span>
|
<span>{{ installerPhaseLabel }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-if="agentStateNote(agent)" class="agent-card-note" :data-tone="agentState(agent).tone">
|
||||||
|
<span>{{ agentStateNote(agent) }}</span>
|
||||||
|
<button type="button" :disabled="refreshing" @click.stop="$emit('refresh-agents')">
|
||||||
|
{{ t('home.refresh') }}
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
<div class="agent-card-meta-row">
|
<div class="agent-card-meta-row">
|
||||||
<span
|
<span
|
||||||
v-if="agent.version"
|
v-if="agent.version"
|
||||||
@@ -125,6 +131,7 @@ defineProps({
|
|||||||
agentImageLabel: { type: Function, required: true },
|
agentImageLabel: { type: Function, required: true },
|
||||||
agentSkillLabel: { type: Function, required: true },
|
agentSkillLabel: { type: Function, required: true },
|
||||||
agentState: { type: Function, required: true },
|
agentState: { type: Function, required: true },
|
||||||
|
agentStateNote: { type: Function, default: () => '' },
|
||||||
focusedAgentKind: { type: String, required: true },
|
focusedAgentKind: { type: String, required: true },
|
||||||
installConfirmKind: { type: String, required: true },
|
installConfirmKind: { type: String, required: true },
|
||||||
installedCount: { type: Number, required: true },
|
installedCount: { type: Number, required: true },
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@click.self="close"
|
@click.self="close"
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
|
ref="dialog"
|
||||||
class="attachment-media-preview-dialog"
|
class="attachment-media-preview-dialog"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
:title="closeLabel"
|
:title="closeLabel"
|
||||||
@click="close"
|
@click="close"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true">×</span>
|
<CloseOutline aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
<img
|
<img
|
||||||
v-if="type === 'image'"
|
v-if="type === 'image'"
|
||||||
@@ -36,6 +37,7 @@
|
|||||||
playsinline
|
playsinline
|
||||||
preload="auto"
|
preload="auto"
|
||||||
/>
|
/>
|
||||||
|
<p class="attachment-media-preview-caption" :title="attachment.name">{{ attachment.name }}</p>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</Teleport>
|
</Teleport>
|
||||||
@@ -43,6 +45,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { CloseOutline } from '@vicons/ionicons5'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
attachment: { type: Object, required: true },
|
attachment: { type: Object, required: true },
|
||||||
@@ -54,6 +57,7 @@ const emit = defineEmits(['close'])
|
|||||||
|
|
||||||
const backdrop = ref(null)
|
const backdrop = ref(null)
|
||||||
const closeButton = ref(null)
|
const closeButton = ref(null)
|
||||||
|
const dialog = ref(null)
|
||||||
let returnFocus = null
|
let returnFocus = null
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -67,8 +71,15 @@ function handleKeydown(event) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (event.key !== 'Tab') return
|
if (event.key !== 'Tab') return
|
||||||
|
// Keep focus inside the preview, but let it reach the video controls.
|
||||||
|
const focusable = [...(dialog.value?.querySelectorAll('button, video[controls]') || [])]
|
||||||
|
if (!focusable.length) return
|
||||||
|
const index = focusable.indexOf(document.activeElement)
|
||||||
|
const next = event.shiftKey
|
||||||
|
? focusable[(index - 1 + focusable.length) % focusable.length]
|
||||||
|
: focusable[(index + 1) % focusable.length]
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
closeButton.value?.focus()
|
next.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -95,12 +106,25 @@ body.media-preview-open {
|
|||||||
|
|
||||||
.attachment-media-preview-backdrop {
|
.attachment-media-preview-backdrop {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 1000;
|
z-index: var(--z-lightbox);
|
||||||
inset: 0;
|
inset: 0;
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
place-items: center;
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
background: rgb(10 14 20 / 78%);
|
background: rgb(10 14 20 / 78%);
|
||||||
|
animation: media-preview-fade var(--dur-base) var(--ease-out) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-media-preview-dialog {
|
||||||
|
animation: media-preview-zoom var(--dur-slow) var(--ease-out) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes media-preview-fade {
|
||||||
|
from { opacity: 0; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes media-preview-zoom {
|
||||||
|
from { opacity: 0; transform: scale(0.96); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-media-preview-dialog {
|
.attachment-media-preview-dialog {
|
||||||
@@ -115,7 +139,8 @@ body.media-preview-open {
|
|||||||
.attachment-media-preview-dialog video {
|
.attachment-media-preview-dialog video {
|
||||||
display: block;
|
display: block;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: calc(100vh - 48px);
|
max-height: calc(100vh - 88px);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
background: #080b10;
|
background: #080b10;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
}
|
}
|
||||||
@@ -136,13 +161,30 @@ body.media-preview-open {
|
|||||||
right: 10px;
|
right: 10px;
|
||||||
width: 34px;
|
width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: rgb(0 0 0 / 62%);
|
background: rgb(0 0 0 / 62%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-size: 28px;
|
}
|
||||||
line-height: 28px;
|
|
||||||
|
.attachment-media-preview-close svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachment-media-preview-caption {
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 10px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
|
color: rgb(255 255 255 / 82%);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
text-align: center;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.attachment-media-preview-close:hover,
|
.attachment-media-preview-close:hover,
|
||||||
|
|||||||
@@ -58,13 +58,23 @@
|
|||||||
</p>
|
</p>
|
||||||
<small v-if="!bridge.available && bridge.lastError">{{ translateError(bridge.lastError) }}</small>
|
<small v-if="!bridge.available && bridge.lastError">{{ translateError(bridge.lastError) }}</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="confirmingBridgeId === bridge.bridgeId" class="cloud-agent-bridge-confirm" role="group" :aria-label="t('cloudAgents.deletePrompt')">
|
||||||
|
<span>{{ t('cloudAgents.deletePrompt') }}</span>
|
||||||
|
<button class="secondary-button compact" type="button" :disabled="saving" @click="confirmingBridgeId = ''">
|
||||||
|
{{ t('common.cancel') }}
|
||||||
|
</button>
|
||||||
|
<button class="danger-button compact" type="button" :disabled="saving" @click="confirmRemove(bridge.bridgeId)">
|
||||||
|
{{ t('cloudAgents.delete') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
|
v-else
|
||||||
class="icon-button"
|
class="icon-button"
|
||||||
type="button"
|
type="button"
|
||||||
:title="t('cloudAgents.delete')"
|
:title="t('cloudAgents.delete')"
|
||||||
:aria-label="t('cloudAgents.delete')"
|
:aria-label="t('cloudAgents.delete')"
|
||||||
:disabled="saving"
|
:disabled="saving"
|
||||||
@click="remove(bridge.bridgeId)"
|
@click="confirmingBridgeId = bridge.bridgeId"
|
||||||
>
|
>
|
||||||
<TrashOutline />
|
<TrashOutline />
|
||||||
</button>
|
</button>
|
||||||
@@ -75,6 +85,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
import { CloudOutline, RefreshOutline, TrashOutline } from '@vicons/ionicons5'
|
import { CloudOutline, RefreshOutline, TrashOutline } from '@vicons/ionicons5'
|
||||||
import { t, translateError } from '../i18n.js'
|
import { t, translateError } from '../i18n.js'
|
||||||
|
|
||||||
@@ -94,6 +105,13 @@ const {
|
|||||||
saving,
|
saving,
|
||||||
} = props.controller
|
} = props.controller
|
||||||
|
|
||||||
|
const confirmingBridgeId = ref('')
|
||||||
|
|
||||||
|
async function confirmRemove(bridgeId) {
|
||||||
|
confirmingBridgeId.value = ''
|
||||||
|
await remove(bridgeId)
|
||||||
|
}
|
||||||
|
|
||||||
function transportLabel(transport) {
|
function transportLabel(transport) {
|
||||||
return t(`cloudAgents.transport.${transport || 'bridge'}`)
|
return t(`cloudAgents.transport.${transport || 'bridge'}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<template>
|
||||||
|
<section class="settings-panel general-settings-panel">
|
||||||
|
<div class="general-settings-row">
|
||||||
|
<div class="general-settings-copy">
|
||||||
|
<h2 id="general-settings-language">{{ t('generalSettings.languageTitle') }}</h2>
|
||||||
|
<p>{{ t('generalSettings.languageHint') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="choice-segmented" role="radiogroup" aria-labelledby="general-settings-language">
|
||||||
|
<button
|
||||||
|
v-for="option in languageOptions"
|
||||||
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
:aria-checked="String(locale === option.value)"
|
||||||
|
:class="{ active: locale === option.value }"
|
||||||
|
:lang="option.lang"
|
||||||
|
@click="setLocale(option.value)"
|
||||||
|
>
|
||||||
|
{{ option.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="general-settings-row">
|
||||||
|
<div class="general-settings-copy">
|
||||||
|
<h2 id="general-settings-theme">{{ t('generalSettings.themeTitle') }}</h2>
|
||||||
|
<p>{{ t('generalSettings.themeHint') }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="choice-segmented" role="radiogroup" aria-labelledby="general-settings-theme">
|
||||||
|
<button
|
||||||
|
v-for="option in themeOptions"
|
||||||
|
:key="option.value"
|
||||||
|
type="button"
|
||||||
|
role="radio"
|
||||||
|
:aria-checked="String(themePreference === option.value)"
|
||||||
|
:class="{ active: themePreference === option.value }"
|
||||||
|
@click="setThemePreference(option.value)"
|
||||||
|
>
|
||||||
|
<component :is="option.icon" aria-hidden="true" />
|
||||||
|
{{ t(option.labelKey) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, unref } from 'vue'
|
||||||
|
import { DesktopOutline, MoonOutline, SunnyOutline } from '@vicons/ionicons5'
|
||||||
|
import { locale as currentLocale, setLocale, t } from '../i18n.js'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
setThemePreference: { type: Function, required: true },
|
||||||
|
themePreference: { type: [String, Object], required: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
// Language names are shown in their own language, whatever the UI locale is.
|
||||||
|
const languageOptions = [
|
||||||
|
{ value: 'zh', label: '中文', lang: 'zh-CN' },
|
||||||
|
{ value: 'en', label: 'English', lang: 'en' },
|
||||||
|
]
|
||||||
|
const themeOptions = [
|
||||||
|
{ value: 'system', labelKey: 'generalSettings.themeSystem', icon: DesktopOutline },
|
||||||
|
{ value: 'light', labelKey: 'generalSettings.themeLight', icon: SunnyOutline },
|
||||||
|
{ value: 'dark', labelKey: 'generalSettings.themeDark', icon: MoonOutline },
|
||||||
|
]
|
||||||
|
|
||||||
|
const locale = currentLocale
|
||||||
|
const themePreference = computed(() => unref(props.themePreference))
|
||||||
|
|
||||||
|
function setThemePreference(value) {
|
||||||
|
props.setThemePreference(value)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -5,13 +5,49 @@
|
|||||||
<h1>{{ t('home.dashboardTitle') }}</h1>
|
<h1>{{ t('home.dashboardTitle') }}</h1>
|
||||||
<p>{{ t('home.dashboardSubtitle') }}</p>
|
<p>{{ t('home.dashboardSubtitle') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="home-workspace-state" :class="{ attention: homeNeedsAttention }" role="status">
|
<div
|
||||||
<WarningOutline v-if="homeNeedsAttention" />
|
class="home-workspace-state"
|
||||||
|
:class="{ attention: homeNeedsAttention, discovering }"
|
||||||
|
role="status"
|
||||||
|
>
|
||||||
|
<RefreshOutline v-if="discovering" class="spinning" />
|
||||||
|
<WarningOutline v-else-if="homeNeedsAttention" />
|
||||||
<CheckmarkCircleOutline v-else />
|
<CheckmarkCircleOutline v-else />
|
||||||
<span>{{ homeWorkspaceSummary }}</span>
|
<span>{{ homeWorkspaceSummary }}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section
|
||||||
|
v-if="awaitingDecisionGates.length"
|
||||||
|
class="home-attention"
|
||||||
|
aria-labelledby="home-attention-title"
|
||||||
|
>
|
||||||
|
<header class="home-attention-header">
|
||||||
|
<span class="attention-dot" aria-hidden="true" />
|
||||||
|
<h2 id="home-attention-title">{{ t('home.attentionTitle') }}</h2>
|
||||||
|
<small>{{ awaitingDecisionGates.length }}</small>
|
||||||
|
</header>
|
||||||
|
<div class="home-attention-list">
|
||||||
|
<button
|
||||||
|
v-for="gate in awaitingDecisionGates"
|
||||||
|
:key="gate.gateId"
|
||||||
|
class="home-attention-item"
|
||||||
|
type="button"
|
||||||
|
@click="$emit('select-group', gate.groupId)"
|
||||||
|
>
|
||||||
|
<img :src="agentLogo(gate.agentKind, theme)" alt="" />
|
||||||
|
<span class="home-attention-copy">
|
||||||
|
<strong>{{ t('home.attentionItem', { agent: agentLabel(gate.agentKind), type: t(`humanGate.type.${gate.type}`) }) }}</strong>
|
||||||
|
<small>{{ gate.groupName }}</small>
|
||||||
|
</span>
|
||||||
|
<span class="home-attention-action">
|
||||||
|
{{ t('home.attentionOpen') }}
|
||||||
|
<ChevronForwardOutline aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section v-if="showRecoveryGuide" class="home-recovery-notice" aria-live="polite">
|
<section v-if="showRecoveryGuide" class="home-recovery-notice" aria-live="polite">
|
||||||
<WarningOutline />
|
<WarningOutline />
|
||||||
<div>
|
<div>
|
||||||
@@ -48,8 +84,17 @@
|
|||||||
<strong>{{ groupName(group) }}</strong>
|
<strong>{{ groupName(group) }}</strong>
|
||||||
<small>{{ recentGroupMeta(group) }}</small>
|
<small>{{ recentGroupMeta(group) }}</small>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="isGroupRunning(group.id)" class="run-mark" :title="t('conversation.runningGeneric')">
|
<span
|
||||||
<span class="run-pulse" />
|
v-if="isGroupAwaitingDecision(group.id)"
|
||||||
|
class="attention-mark"
|
||||||
|
role="img"
|
||||||
|
:title="t('humanGate.waiting')"
|
||||||
|
:aria-label="t('humanGate.waiting')"
|
||||||
|
>
|
||||||
|
<span class="attention-dot" />
|
||||||
|
</span>
|
||||||
|
<span v-else-if="isGroupRunning(group.id)" class="run-mark" :title="t('conversation.runningGeneric')">
|
||||||
|
<span class="run-agent-bars" aria-hidden="true"><i /><i /><i /></span>
|
||||||
</span>
|
</span>
|
||||||
<ChevronForwardOutline />
|
<ChevronForwardOutline />
|
||||||
</button>
|
</button>
|
||||||
@@ -61,7 +106,8 @@
|
|||||||
v-if="homeMode === 'first-task'"
|
v-if="homeMode === 'first-task'"
|
||||||
class="primary-button compact"
|
class="primary-button compact"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="readyCount < 2"
|
:disabled="readyCount < 1"
|
||||||
|
:title="readyCount < 1 ? t('home.noReadyAgents') : undefined"
|
||||||
@click="$emit('open-new-group')"
|
@click="$emit('open-new-group')"
|
||||||
>
|
>
|
||||||
<AddOutline />
|
<AddOutline />
|
||||||
@@ -80,7 +126,8 @@
|
|||||||
v-if="homeMode !== 'first-task'"
|
v-if="homeMode !== 'first-task'"
|
||||||
class="secondary-button compact home-new-group-button"
|
class="secondary-button compact home-new-group-button"
|
||||||
type="button"
|
type="button"
|
||||||
:disabled="readyCount < 2"
|
:disabled="readyCount < 1"
|
||||||
|
:title="readyCount < 1 ? t('home.noReadyAgents') : undefined"
|
||||||
@click="$emit('open-new-group')"
|
@click="$emit('open-new-group')"
|
||||||
>
|
>
|
||||||
<AddOutline />
|
<AddOutline />
|
||||||
@@ -104,8 +151,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="home-panel-empty">
|
<div v-else class="home-panel-empty">
|
||||||
<TerminalOutline />
|
<RefreshOutline v-if="discovering" class="spinning" />
|
||||||
<p>{{ t('home.noReadyAgents') }}</p>
|
<TerminalOutline v-else />
|
||||||
|
<p>{{ discovering ? t('onboarding.detecting') : t('home.noReadyAgents') }}</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -161,14 +209,17 @@ import {
|
|||||||
TerminalOutline,
|
TerminalOutline,
|
||||||
WarningOutline,
|
WarningOutline,
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import { agentLogo } from '../catalog.js'
|
import { agentLabel, agentLogo } from '../catalog.js'
|
||||||
import { t } from '../i18n.js'
|
import { t } from '../i18n.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
agentDescription: { type: Function, required: true },
|
agentDescription: { type: Function, required: true },
|
||||||
|
awaitingDecisionGates: { type: Array, default: () => [] },
|
||||||
|
discovering: { type: Boolean, default: false },
|
||||||
groupName: { type: Function, required: true },
|
groupName: { type: Function, required: true },
|
||||||
groups: { type: Array, required: true },
|
groups: { type: Array, required: true },
|
||||||
installedCount: { type: Number, required: true },
|
installedCount: { type: Number, required: true },
|
||||||
|
isGroupAwaitingDecision: { type: Function, default: () => false },
|
||||||
isGroupRunning: { type: Function, required: true },
|
isGroupRunning: { type: Function, required: true },
|
||||||
onboardingCompleted: { type: Boolean, required: true },
|
onboardingCompleted: { type: Boolean, required: true },
|
||||||
readyAgents: { type: Array, required: true },
|
readyAgents: { type: Array, required: true },
|
||||||
@@ -195,13 +246,19 @@ const homeMode = computed(() => {
|
|||||||
if (props.groups.length) return 'workspace'
|
if (props.groups.length) return 'workspace'
|
||||||
return readyCount.value > 0 ? 'first-task' : 'setup'
|
return readyCount.value > 0 ? 'first-task' : 'setup'
|
||||||
})
|
})
|
||||||
const homeNeedsAttention = computed(() => readyCount.value === 0)
|
// While the first detection runs there is no Agent data yet; that is not a
|
||||||
const showSetupGuide = computed(() => props.onboardingCompleted && homeMode.value === 'setup')
|
// setup problem, so no warning or setup guide until it finishes.
|
||||||
|
const homeNeedsAttention = computed(() => !props.discovering && readyCount.value === 0)
|
||||||
|
const showSetupGuide = computed(() => (
|
||||||
|
props.onboardingCompleted && homeMode.value === 'setup' && !props.discovering
|
||||||
|
))
|
||||||
const showRecoveryGuide = computed(() => (
|
const showRecoveryGuide = computed(() => (
|
||||||
props.onboardingCompleted && homeMode.value === 'workspace' && homeNeedsAttention.value
|
props.onboardingCompleted && homeMode.value === 'workspace' && homeNeedsAttention.value
|
||||||
))
|
))
|
||||||
const homeWorkspaceSummary = computed(() => (
|
const homeWorkspaceSummary = computed(() => (
|
||||||
homeNeedsAttention.value
|
props.discovering
|
||||||
|
? t('onboarding.detecting')
|
||||||
|
: homeNeedsAttention.value
|
||||||
? t('home.setupNeeded')
|
? t('home.setupNeeded')
|
||||||
: t('home.workspaceSummary', {
|
: t('home.workspaceSummary', {
|
||||||
agents: readyCount.value,
|
agents: readyCount.value,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="onboarding-title"
|
aria-labelledby="onboarding-title"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
|
@keydown.left="selectOnboardingSlide(onboardingIndex - 1)"
|
||||||
|
@keydown.right="selectOnboardingSlide(onboardingIndex + 1)"
|
||||||
>
|
>
|
||||||
<div class="onboarding-slide-viewport">
|
<div class="onboarding-slide-viewport">
|
||||||
<transition name="onboarding-slide">
|
<transition name="onboarding-slide">
|
||||||
@@ -32,6 +34,10 @@
|
|||||||
@click="selectOnboardingSlide(index)"
|
@click="selectOnboardingSlide(index)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="onboarding-actions">
|
||||||
|
<button class="onboarding-skip" type="button" @click="completeOnboarding">
|
||||||
|
{{ t('onboarding.skip') }}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
class="primary-button onboarding-primary"
|
class="primary-button onboarding-primary"
|
||||||
type="button"
|
type="button"
|
||||||
@@ -43,6 +49,7 @@
|
|||||||
<CheckmarkCircleOutline v-else />
|
<CheckmarkCircleOutline v-else />
|
||||||
{{ onboardingReady ? t('onboarding.start') : onboardingLoadingLabel }}
|
{{ onboardingReady ? t('onboarding.start') : onboardingLoadingLabel }}
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,273 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="container"
|
|
||||||
class="pixel-blast-container"
|
|
||||||
:class="{ 'pixel-blast-ready': rendererReady }"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<div class="pixel-blast-fallback" />
|
|
||||||
<canvas ref="canvas" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
theme: { type: String, default: 'light' },
|
|
||||||
})
|
|
||||||
|
|
||||||
const container = ref(null)
|
|
||||||
const canvas = ref(null)
|
|
||||||
const rendererReady = ref(false)
|
|
||||||
let runtime = null
|
|
||||||
let disposed = false
|
|
||||||
|
|
||||||
const VERTEX_SHADER = `
|
|
||||||
void main() {
|
|
||||||
gl_Position = vec4(position, 1.0);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
// Adapted from the React Bits PixelBlast shader (MIT).
|
|
||||||
const FRAGMENT_SHADER = `
|
|
||||||
precision highp float;
|
|
||||||
|
|
||||||
uniform vec2 uResolution;
|
|
||||||
uniform float uTime;
|
|
||||||
uniform float uPixelSize;
|
|
||||||
uniform float uScale;
|
|
||||||
uniform float uDensity;
|
|
||||||
uniform float uPixelJitter;
|
|
||||||
uniform float uEdgeFade;
|
|
||||||
uniform float uOpacity;
|
|
||||||
uniform vec3 uPrimaryColor;
|
|
||||||
uniform vec3 uSecondaryColor;
|
|
||||||
uniform vec3 uTertiaryColor;
|
|
||||||
|
|
||||||
out vec4 fragColor;
|
|
||||||
|
|
||||||
float Bayer2(vec2 a) {
|
|
||||||
a = floor(a);
|
|
||||||
return fract(a.x / 2.0 + a.y * a.y * 0.75);
|
|
||||||
}
|
|
||||||
|
|
||||||
#define Bayer4(a) (Bayer2(0.5 * (a)) * 0.25 + Bayer2(a))
|
|
||||||
#define Bayer8(a) (Bayer4(0.5 * (a)) * 0.25 + Bayer2(a))
|
|
||||||
|
|
||||||
float hash11(float n) {
|
|
||||||
return fract(sin(n) * 43758.5453);
|
|
||||||
}
|
|
||||||
|
|
||||||
float vnoise(vec3 p) {
|
|
||||||
vec3 ip = floor(p);
|
|
||||||
vec3 fp = fract(p);
|
|
||||||
float n000 = hash11(dot(ip + vec3(0.0, 0.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
float n100 = hash11(dot(ip + vec3(1.0, 0.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
float n010 = hash11(dot(ip + vec3(0.0, 1.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
float n110 = hash11(dot(ip + vec3(1.0, 1.0, 0.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
float n001 = hash11(dot(ip + vec3(0.0, 0.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
float n101 = hash11(dot(ip + vec3(1.0, 0.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
float n011 = hash11(dot(ip + vec3(0.0, 1.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
float n111 = hash11(dot(ip + vec3(1.0, 1.0, 1.0), vec3(1.0, 57.0, 113.0)));
|
|
||||||
vec3 weight = fp * fp * fp * (fp * (fp * 6.0 - 15.0) + 10.0);
|
|
||||||
float x00 = mix(n000, n100, weight.x);
|
|
||||||
float x10 = mix(n010, n110, weight.x);
|
|
||||||
float x01 = mix(n001, n101, weight.x);
|
|
||||||
float x11 = mix(n011, n111, weight.x);
|
|
||||||
return mix(mix(x00, x10, weight.y), mix(x01, x11, weight.y), weight.z) * 2.0 - 1.0;
|
|
||||||
}
|
|
||||||
|
|
||||||
float fbm(vec2 uv, float time) {
|
|
||||||
vec3 point = vec3(uv * uScale, time);
|
|
||||||
float amplitude = 1.0;
|
|
||||||
float frequency = 1.0;
|
|
||||||
float sum = 1.0;
|
|
||||||
for (int index = 0; index < 5; ++index) {
|
|
||||||
sum += amplitude * vnoise(point * frequency);
|
|
||||||
frequency *= 1.25;
|
|
||||||
}
|
|
||||||
return sum * 0.5 + 0.5;
|
|
||||||
}
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
vec2 fragCoord = gl_FragCoord.xy - uResolution * 0.5;
|
|
||||||
float aspectRatio = uResolution.x / uResolution.y;
|
|
||||||
vec2 pixelId = floor(fragCoord / uPixelSize);
|
|
||||||
vec2 pixelUv = fract(fragCoord / uPixelSize);
|
|
||||||
float cellPixelSize = 8.0 * uPixelSize;
|
|
||||||
vec2 cellCoord = floor(fragCoord / cellPixelSize) * cellPixelSize;
|
|
||||||
vec2 fieldUv = cellCoord / uResolution * vec2(aspectRatio, 1.0);
|
|
||||||
|
|
||||||
float motionTime = uTime * (1.0 + 0.12 * sin(uTime * 0.17));
|
|
||||||
vec2 drift = vec2(sin(uTime * 0.41), cos(uTime * 0.33)) * 0.045;
|
|
||||||
float field = fbm(fieldUv + drift, motionTime * 0.05) * 0.5 - 0.65;
|
|
||||||
float feed = field + (uDensity - 0.5) * 0.3;
|
|
||||||
float bayer = Bayer8(fragCoord / uPixelSize) - 0.5;
|
|
||||||
float enabled = step(0.5, feed + bayer);
|
|
||||||
float hash = fract(sin(dot(pixelId, vec2(127.1, 311.7))) * 43758.5453);
|
|
||||||
float jitter = 1.0 + (hash - 0.5) * uPixelJitter;
|
|
||||||
float inset = step(0.08, pixelUv.x) * step(0.08, pixelUv.y)
|
|
||||||
* step(pixelUv.x, 0.92) * step(pixelUv.y, 0.92);
|
|
||||||
float mask = enabled * jitter * inset;
|
|
||||||
|
|
||||||
vec2 normalized = gl_FragCoord.xy / uResolution;
|
|
||||||
float edge = min(min(normalized.x, normalized.y), min(1.0 - normalized.x, 1.0 - normalized.y));
|
|
||||||
mask *= smoothstep(0.0, uEdgeFade, edge);
|
|
||||||
float centerDistance = length((normalized - 0.5) * vec2(aspectRatio, 1.0));
|
|
||||||
// Keep the message area visually calm while preserving the animated field
|
|
||||||
// around the edges of the discovery screen.
|
|
||||||
mask *= mix(0.1, 1.0, smoothstep(0.14, 0.46, centerDistance));
|
|
||||||
|
|
||||||
float colorNoise = clamp(0.5 + 0.5 * vnoise(vec3((fieldUv + drift * 1.8) * 1.35, motionTime * 0.035)), 0.0, 1.0);
|
|
||||||
vec3 color = mix(uPrimaryColor, uSecondaryColor, colorNoise);
|
|
||||||
color = mix(color, uTertiaryColor, smoothstep(0.56, 1.0, normalized.y + colorNoise * 0.18));
|
|
||||||
fragColor = vec4(color, mask * uOpacity);
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
function cssColor(name, fallback) {
|
|
||||||
const value = getComputedStyle(document.documentElement).getPropertyValue(name).trim()
|
|
||||||
return value || fallback
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateThemeColors() {
|
|
||||||
if (!runtime) return
|
|
||||||
runtime.uniforms.uPrimaryColor.value.set(cssColor('--pixel-blast-primary', '#d92f24'))
|
|
||||||
runtime.uniforms.uSecondaryColor.value.set(cssColor('--pixel-blast-secondary', '#007d91'))
|
|
||||||
runtime.uniforms.uTertiaryColor.value.set(cssColor('--pixel-blast-tertiary', '#ed512f'))
|
|
||||||
}
|
|
||||||
|
|
||||||
function disposeRuntime() {
|
|
||||||
if (!runtime) return
|
|
||||||
cancelAnimationFrame(runtime.animationFrame)
|
|
||||||
runtime.resizeObserver.disconnect()
|
|
||||||
runtime.geometry.dispose()
|
|
||||||
runtime.material.dispose()
|
|
||||||
runtime.renderer.dispose()
|
|
||||||
runtime = null
|
|
||||||
rendererReady.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initializeRenderer() {
|
|
||||||
if (!container.value || !canvas.value || typeof window.WebGL2RenderingContext === 'undefined') return
|
|
||||||
try {
|
|
||||||
const THREE = await import('three')
|
|
||||||
if (disposed || !container.value || !canvas.value) return
|
|
||||||
const renderer = new THREE.WebGLRenderer({
|
|
||||||
canvas: canvas.value,
|
|
||||||
alpha: true,
|
|
||||||
antialias: false,
|
|
||||||
powerPreference: 'high-performance',
|
|
||||||
})
|
|
||||||
renderer.setClearAlpha(0)
|
|
||||||
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 1.5))
|
|
||||||
const uniforms = {
|
|
||||||
uResolution: { value: new THREE.Vector2(1, 1) },
|
|
||||||
uTime: { value: Math.random() * 1000 },
|
|
||||||
uPixelSize: { value: 3.7 * renderer.getPixelRatio() },
|
|
||||||
uScale: { value: 2.2 },
|
|
||||||
uDensity: { value: 1.26 },
|
|
||||||
uPixelJitter: { value: 0.32 },
|
|
||||||
uEdgeFade: { value: 0.12 },
|
|
||||||
uOpacity: { value: 0.42 },
|
|
||||||
uPrimaryColor: { value: new THREE.Color() },
|
|
||||||
uSecondaryColor: { value: new THREE.Color() },
|
|
||||||
uTertiaryColor: { value: new THREE.Color() },
|
|
||||||
}
|
|
||||||
const scene = new THREE.Scene()
|
|
||||||
const camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1)
|
|
||||||
const material = new THREE.ShaderMaterial({
|
|
||||||
vertexShader: VERTEX_SHADER,
|
|
||||||
fragmentShader: FRAGMENT_SHADER,
|
|
||||||
uniforms,
|
|
||||||
transparent: true,
|
|
||||||
depthTest: false,
|
|
||||||
depthWrite: false,
|
|
||||||
glslVersion: THREE.GLSL3,
|
|
||||||
})
|
|
||||||
const geometry = new THREE.PlaneGeometry(2, 2)
|
|
||||||
scene.add(new THREE.Mesh(geometry, material))
|
|
||||||
|
|
||||||
const resize = () => {
|
|
||||||
const width = Math.max(1, container.value?.clientWidth || 1)
|
|
||||||
const height = Math.max(1, container.value?.clientHeight || 1)
|
|
||||||
renderer.setSize(width, height, false)
|
|
||||||
uniforms.uResolution.value.set(renderer.domElement.width, renderer.domElement.height)
|
|
||||||
uniforms.uPixelSize.value = 3.7 * renderer.getPixelRatio()
|
|
||||||
}
|
|
||||||
const resizeObserver = new ResizeObserver(resize)
|
|
||||||
resizeObserver.observe(container.value)
|
|
||||||
runtime = {
|
|
||||||
animationFrame: 0,
|
|
||||||
geometry,
|
|
||||||
material,
|
|
||||||
renderer,
|
|
||||||
resizeObserver,
|
|
||||||
uniforms,
|
|
||||||
}
|
|
||||||
resize()
|
|
||||||
updateThemeColors()
|
|
||||||
rendererReady.value = true
|
|
||||||
|
|
||||||
const reducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
||||||
let previousFrame = performance.now()
|
|
||||||
const animate = (now) => {
|
|
||||||
if (!runtime) return
|
|
||||||
runtime.uniforms.uTime.value += Math.min(32, now - previousFrame) * 0.00036
|
|
||||||
previousFrame = now
|
|
||||||
renderer.render(scene, camera)
|
|
||||||
if (!reducedMotion) runtime.animationFrame = requestAnimationFrame(animate)
|
|
||||||
}
|
|
||||||
runtime.animationFrame = requestAnimationFrame(animate)
|
|
||||||
} catch {
|
|
||||||
disposeRuntime()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(() => props.theme, () => requestAnimationFrame(updateThemeColors))
|
|
||||||
onMounted(() => { void initializeRenderer() })
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
disposed = true
|
|
||||||
disposeRuntime()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.pixel-blast-container,
|
|
||||||
.pixel-blast-container canvas,
|
|
||||||
.pixel-blast-fallback {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-blast-container {
|
|
||||||
overflow: hidden;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-blast-container canvas {
|
|
||||||
display: block;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.22s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-blast-ready canvas {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-blast-fallback {
|
|
||||||
background-image:
|
|
||||||
repeating-linear-gradient(90deg, transparent 0 5px, color-mix(in srgb, var(--pixel-blast-secondary) 38%, transparent) 5px 8px),
|
|
||||||
repeating-linear-gradient(0deg, transparent 0 5px, color-mix(in srgb, var(--pixel-blast-primary) 34%, transparent) 5px 8px);
|
|
||||||
opacity: 0.32;
|
|
||||||
transition: opacity 0.22s ease;
|
|
||||||
mask-image: linear-gradient(135deg, transparent 2%, black 34%, black 66%, transparent 98%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pixel-blast-ready .pixel-blast-fallback {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -148,7 +148,10 @@
|
|||||||
<span>{{ roundLabel(selectedItem) }}</span>
|
<span>{{ roundLabel(selectedItem) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="trace-summary-statuses">
|
<span class="trace-summary-statuses">
|
||||||
<TextMorph v-if="waiting" class="trace-waiting-state" :words="waitingWords" />
|
<span v-if="waiting" class="trace-waiting-state">
|
||||||
|
<span class="attention-dot" aria-hidden="true" />
|
||||||
|
{{ t('humanGate.waiting') }}
|
||||||
|
</span>
|
||||||
<span class="trace-status" :data-status="statusTone(selectedItem.status)">
|
<span class="trace-status" :data-status="statusTone(selectedItem.status)">
|
||||||
{{ statusLabel(selectedItem.status) }}
|
{{ statusLabel(selectedItem.status) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -186,9 +189,12 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
<div v-if="selectedItem.context && hasTraceContext(selectedItem.context)" class="trace-context-disclosure">
|
<details v-if="selectedItem.context && hasTraceContext(selectedItem.context)" class="trace-context-disclosure">
|
||||||
<div class="trace-context-stats" data-context-section="attempt">
|
<summary>
|
||||||
<span>{{ t('trace.attemptInjection') }}</span>
|
<span>{{ t('trace.attemptInjection') }}</span>
|
||||||
|
<ChevronDownOutline aria-hidden="true" />
|
||||||
|
</summary>
|
||||||
|
<div class="trace-context-stats" data-context-section="attempt">
|
||||||
<span>{{ t('trace.contextIncluded', { count: selectedItem.context.includedCount }) }}</span>
|
<span>{{ t('trace.contextIncluded', { count: selectedItem.context.includedCount }) }}</span>
|
||||||
<span>{{ t('trace.contextOmitted', { count: selectedItem.context.omittedCount }) }}</span>
|
<span>{{ t('trace.contextOmitted', { count: selectedItem.context.omittedCount }) }}</span>
|
||||||
<span>{{ t('trace.contextChars', { count: selectedItem.context.charCount }) }}</span>
|
<span>{{ t('trace.contextChars', { count: selectedItem.context.charCount }) }}</span>
|
||||||
@@ -273,7 +279,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</details>
|
||||||
<p v-if="hasOnlyBareEvents(selectedItem)" class="trace-detail-unavailable">
|
<p v-if="hasOnlyBareEvents(selectedItem)" class="trace-detail-unavailable">
|
||||||
{{ t('trace.detailUnavailable') }}
|
{{ t('trace.detailUnavailable') }}
|
||||||
</p>
|
</p>
|
||||||
@@ -415,7 +421,6 @@ import {
|
|||||||
CloseOutline,
|
CloseOutline,
|
||||||
} from '@vicons/ionicons5'
|
} from '@vicons/ionicons5'
|
||||||
import { agentLabel, agentLogo } from '../catalog.js'
|
import { agentLabel, agentLogo } from '../catalog.js'
|
||||||
import TextMorph from './TextMorph.vue'
|
|
||||||
import { retainedTraceEvents } from '../conversationTimelineModel.js'
|
import { retainedTraceEvents } from '../conversationTimelineModel.js'
|
||||||
import { orchestrationPhase, orchestrationSlotCompleted } from '../desktop-normalization.js'
|
import { orchestrationPhase, orchestrationSlotCompleted } from '../desktop-normalization.js'
|
||||||
import { locale, t } from '../i18n.js'
|
import { locale, t } from '../i18n.js'
|
||||||
@@ -445,12 +450,6 @@ const roundSelectTrigger = ref(null)
|
|||||||
const openSelector = ref('')
|
const openSelector = ref('')
|
||||||
const humanGateResponses = reactive({})
|
const humanGateResponses = reactive({})
|
||||||
const selectedItem = computed(() => props.items.find(item => item.agentRunId === props.selectedAgentRunId) || props.items.at(-1) || null)
|
const selectedItem = computed(() => props.items.find(item => item.agentRunId === props.selectedAgentRunId) || props.items.at(-1) || null)
|
||||||
const waitingWords = computed(() => [
|
|
||||||
t('humanGate.waitingWordOne'),
|
|
||||||
t('humanGate.waitingWordTwo'),
|
|
||||||
t('humanGate.waitingWordThree'),
|
|
||||||
t('humanGate.waitingWordFour'),
|
|
||||||
])
|
|
||||||
const agentGroups = computed(() => {
|
const agentGroups = computed(() => {
|
||||||
const groups = new Map()
|
const groups = new Map()
|
||||||
for (const item of props.items) {
|
for (const item of props.items) {
|
||||||
@@ -641,8 +640,9 @@ function handleDocumentPointerDown(event) {
|
|||||||
onMounted(() => document.addEventListener('pointerdown', handleDocumentPointerDown))
|
onMounted(() => document.addEventListener('pointerdown', handleDocumentPointerDown))
|
||||||
onBeforeUnmount(() => document.removeEventListener('pointerdown', handleDocumentPointerDown))
|
onBeforeUnmount(() => document.removeEventListener('pointerdown', handleDocumentPointerDown))
|
||||||
|
|
||||||
|
// "Always allow" grants a lasting permission, so it is never the default-looking button.
|
||||||
function optionApprovesHumanGate(option) {
|
function optionApprovesHumanGate(option) {
|
||||||
return ['allow_once', 'allow_always', 'accept', 'respond'].includes(option?.kind)
|
return ['allow_once', 'accept', 'respond'].includes(option?.kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
function humanGateResponse(gateId) {
|
function humanGateResponse(gateId) {
|
||||||
@@ -880,6 +880,42 @@ defineExpose({ focus })
|
|||||||
.trace-context-disclosure {
|
.trace-context-disclosure {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-context-disclosure > summary {
|
||||||
|
min-height: 30px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-context-disclosure > summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-context-disclosure > summary:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-context-disclosure > summary svg {
|
||||||
|
width: 13px;
|
||||||
|
height: 13px;
|
||||||
|
transition: transform var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-context-disclosure[open] > summary svg {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-context-disclosure code {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--text-2xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-context-record code {
|
.trace-context-record code {
|
||||||
@@ -916,8 +952,8 @@ defineExpose({ focus })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.trace-phase-current strong {
|
.trace-phase-current strong {
|
||||||
color: var(--accent-hover);
|
color: var(--accent-text);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-phase-slot-list {
|
.trace-phase-slot-list {
|
||||||
@@ -936,7 +972,7 @@ defineExpose({ focus })
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 10px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-phase-slot-list li > span {
|
.trace-phase-slot-list li > span {
|
||||||
@@ -950,7 +986,7 @@ defineExpose({ focus })
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex: 1 1 142px;
|
flex: 1 1 142px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 9px;
|
font-size: var(--text-xs);
|
||||||
line-height: 1.35;
|
line-height: 1.35;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
@@ -958,9 +994,12 @@ defineExpose({ focus })
|
|||||||
}
|
}
|
||||||
|
|
||||||
.trace-waiting-state {
|
.trace-waiting-state {
|
||||||
color: var(--accent-hover);
|
display: inline-flex;
|
||||||
font-size: 10px;
|
align-items: center;
|
||||||
font-weight: 700;
|
gap: 6px;
|
||||||
|
color: var(--warning);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -973,10 +1012,10 @@ defineExpose({ focus })
|
|||||||
.trace-human-gate-card {
|
.trace-human-gate-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
padding: 10px;
|
padding: 10px 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid color-mix(in srgb, var(--warning) 34%, var(--border));
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius);
|
||||||
background: var(--surface-raised);
|
background: color-mix(in srgb, var(--warning-soft) 50%, var(--surface-raised));
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-human-gate-card header {
|
.trace-human-gate-card header {
|
||||||
@@ -997,21 +1036,21 @@ defineExpose({ focus })
|
|||||||
|
|
||||||
.trace-human-gate-card header strong {
|
.trace-human-gate-card header strong {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-human-gate-card header small {
|
.trace-human-gate-card header small {
|
||||||
color: var(--accent-hover);
|
color: var(--warning);
|
||||||
font-size: 9px;
|
font-size: var(--text-2xs);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.trace-human-gate-card p {
|
.trace-human-gate-card p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,16 @@
|
|||||||
<LibraryOutline />
|
<LibraryOutline />
|
||||||
{{ t('systemSettings.knowledgeBases') }}
|
{{ t('systemSettings.knowledgeBases') }}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
:aria-selected="systemSettingsSection === 'general'"
|
||||||
|
:class="{ active: systemSettingsSection === 'general' }"
|
||||||
|
@click="selectSystemSettingsSection('general')"
|
||||||
|
>
|
||||||
|
<OptionsOutline />
|
||||||
|
{{ t('systemSettings.general') }}
|
||||||
|
</button>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<AgentSettingsPanel
|
<AgentSettingsPanel
|
||||||
@@ -48,6 +58,7 @@
|
|||||||
:agent-image-label="agentImageLabel"
|
:agent-image-label="agentImageLabel"
|
||||||
:agent-skill-label="agentSkillLabel"
|
:agent-skill-label="agentSkillLabel"
|
||||||
:agent-state="agentState"
|
:agent-state="agentState"
|
||||||
|
:agent-state-note="agentStateNote"
|
||||||
:focused-agent-kind="focusedAgentKind"
|
:focused-agent-kind="focusedAgentKind"
|
||||||
:install-confirm-kind="installConfirmKind"
|
:install-confirm-kind="installConfirmKind"
|
||||||
:installed-count="installedCount"
|
:installed-count="installedCount"
|
||||||
@@ -85,14 +96,21 @@
|
|||||||
@primary-action="runKnowledgeBasePrimaryAction"
|
@primary-action="runKnowledgeBasePrimaryAction"
|
||||||
@refresh="loadKnowledgeBaseStatuses"
|
@refresh="loadKnowledgeBaseStatuses"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<GeneralSettingsPanel
|
||||||
|
v-else-if="systemSettingsSection === 'general'"
|
||||||
|
:set-theme-preference="setThemePreference"
|
||||||
|
:theme-preference="themePreference"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { CloudOutline, LibraryOutline, TerminalOutline } from '@vicons/ionicons5'
|
import { CloudOutline, LibraryOutline, OptionsOutline, TerminalOutline } from '@vicons/ionicons5'
|
||||||
import AgentSettingsPanel from './AgentSettingsPanel.vue'
|
import AgentSettingsPanel from './AgentSettingsPanel.vue'
|
||||||
import CloudAgentBridgePanel from './CloudAgentBridgePanel.vue'
|
import CloudAgentBridgePanel from './CloudAgentBridgePanel.vue'
|
||||||
|
import GeneralSettingsPanel from './GeneralSettingsPanel.vue'
|
||||||
import KnowledgeBaseSettingsPanel from './KnowledgeBaseSettingsPanel.vue'
|
import KnowledgeBaseSettingsPanel from './KnowledgeBaseSettingsPanel.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -105,6 +123,7 @@ const {
|
|||||||
agentImageLabel,
|
agentImageLabel,
|
||||||
agentSkillLabel,
|
agentSkillLabel,
|
||||||
agentState,
|
agentState,
|
||||||
|
agentStateNote,
|
||||||
cancelInstall,
|
cancelInstall,
|
||||||
cloudAgentBridges,
|
cloudAgentBridges,
|
||||||
focusedAgentKind,
|
focusedAgentKind,
|
||||||
@@ -134,7 +153,9 @@ const {
|
|||||||
requestInstall,
|
requestInstall,
|
||||||
runKnowledgeBasePrimaryAction,
|
runKnowledgeBasePrimaryAction,
|
||||||
selectSystemSettingsSection,
|
selectSystemSettingsSection,
|
||||||
|
setThemePreference,
|
||||||
systemSettingsSection,
|
systemSettingsSection,
|
||||||
t,
|
t,
|
||||||
|
themePreference,
|
||||||
} = props.controller
|
} = props.controller
|
||||||
</script>
|
</script>
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<template>
|
|
||||||
<span class="text-morph" aria-live="polite" aria-atomic="true">
|
|
||||||
<Transition name="text-morph-swap" mode="out-in">
|
|
||||||
<span :key="currentWord" class="text-morph-word">{{ currentWord }}</span>
|
|
||||||
</Transition>
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
intervalMs: { type: Number, default: 2200 },
|
|
||||||
words: { type: Array, default: () => [] },
|
|
||||||
})
|
|
||||||
|
|
||||||
const index = ref(0)
|
|
||||||
let timer = null
|
|
||||||
|
|
||||||
const normalizedWords = computed(() => (
|
|
||||||
Array.isArray(props.words)
|
|
||||||
? props.words.map(value => String(value || '').trim()).filter(Boolean)
|
|
||||||
: []
|
|
||||||
))
|
|
||||||
|
|
||||||
const currentWord = computed(() => {
|
|
||||||
const words = normalizedWords.value
|
|
||||||
if (!words.length) return ''
|
|
||||||
return words[index.value % words.length]
|
|
||||||
})
|
|
||||||
|
|
||||||
function clearTimer() {
|
|
||||||
if (timer) clearInterval(timer)
|
|
||||||
timer = null
|
|
||||||
}
|
|
||||||
|
|
||||||
function syncTimer() {
|
|
||||||
clearTimer()
|
|
||||||
index.value = 0
|
|
||||||
const words = normalizedWords.value
|
|
||||||
if (typeof window === 'undefined' || words.length < 2) return
|
|
||||||
if (typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
|
||||||
|
|
||||||
timer = window.setInterval(() => {
|
|
||||||
index.value = (index.value + 1) % words.length
|
|
||||||
}, Math.max(800, props.intervalMs))
|
|
||||||
}
|
|
||||||
|
|
||||||
watch([normalizedWords, () => props.intervalMs], syncTimer, { immediate: true })
|
|
||||||
|
|
||||||
onBeforeUnmount(clearTimer)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.text-morph {
|
|
||||||
position: relative;
|
|
||||||
display: inline-grid;
|
|
||||||
place-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
min-width: 7.5em;
|
|
||||||
height: 1.5em;
|
|
||||||
color: inherit;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-morph-word {
|
|
||||||
grid-area: 1 / 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-morph-swap-enter-active,
|
|
||||||
.text-morph-swap-leave-active {
|
|
||||||
transition: opacity 0.35s ease, transform 0.35s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-morph-swap-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-morph-swap-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-16px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="wave-physics-loader" aria-hidden="true">
|
|
||||||
<div class="wave-physics-track">
|
|
||||||
<span
|
|
||||||
v-for="bar in bars"
|
|
||||||
:key="bar.index"
|
|
||||||
class="wave-physics-bar"
|
|
||||||
:style="{ height: bar.height, backgroundColor: bar.color }"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="wave-physics-ball"
|
|
||||||
:style="{
|
|
||||||
transform: `translate3d(${ball.x}, ${ball.y}, 0) scaleX(${ball.scaleX}) scaleY(${ball.scaleY})`,
|
|
||||||
backgroundColor: ball.color,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
theme: { type: String, default: 'light' },
|
|
||||||
})
|
|
||||||
|
|
||||||
const numBars = 15
|
|
||||||
const barWidth = 12
|
|
||||||
const barGap = 8
|
|
||||||
const barTotalWidth = barWidth + barGap
|
|
||||||
const B = 4
|
|
||||||
const maxBounce = 60
|
|
||||||
const baseBarH = 16
|
|
||||||
const wavePeakH = 48
|
|
||||||
const duration = 4000
|
|
||||||
|
|
||||||
const progress = ref(0.5)
|
|
||||||
let animationFrame = 0
|
|
||||||
|
|
||||||
function interpolate(start, end, value) {
|
|
||||||
return Math.round(start + (end - start) * value)
|
|
||||||
}
|
|
||||||
|
|
||||||
function barColor(theme, waveValue) {
|
|
||||||
if (theme === 'dark') {
|
|
||||||
return `rgb(${interpolate(39, 228, waveValue)} ${interpolate(39, 228, waveValue)} ${interpolate(42, 231, waveValue)})`
|
|
||||||
}
|
|
||||||
return `rgb(${interpolate(228, 39, waveValue)} ${interpolate(228, 39, waveValue)} ${interpolate(231, 42, waveValue)})`
|
|
||||||
}
|
|
||||||
|
|
||||||
function frameAt(t) {
|
|
||||||
const xFrac = t < 0.5 ? t / 0.5 : (1 - t) / 0.5
|
|
||||||
const ballIndex = xFrac * (numBars - 1)
|
|
||||||
let bounceF = (xFrac * B) % 1
|
|
||||||
if (xFrac === 0 || xFrac === 1) bounceF = 0
|
|
||||||
|
|
||||||
const bounceH = 4 * bounceF * (1 - bounceF)
|
|
||||||
const heightFactor = Math.max(0, 1 - bounceH * 2)
|
|
||||||
const ballIndent = heightFactor * 20
|
|
||||||
const ballY = (baseBarH + wavePeakH - ballIndent) + bounceH * maxBounce
|
|
||||||
|
|
||||||
const bars = Array.from({ length: numBars }, (_unused, index) => {
|
|
||||||
const dist = Math.abs(index - ballIndex)
|
|
||||||
let waveValue = 0
|
|
||||||
if (dist < 3) waveValue = Math.cos((dist / 3) * (Math.PI / 2))
|
|
||||||
|
|
||||||
let indent = 0
|
|
||||||
if (dist < 1.5) {
|
|
||||||
const indentDist = Math.cos((dist / 1.5) * (Math.PI / 2))
|
|
||||||
indent = indentDist * heightFactor * 20
|
|
||||||
}
|
|
||||||
|
|
||||||
const barHeight = Math.max(4, baseBarH + waveValue * wavePeakH - indent)
|
|
||||||
|
|
||||||
return {
|
|
||||||
color: barColor(props.theme, waveValue),
|
|
||||||
height: `${barHeight}px`,
|
|
||||||
index,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
bars,
|
|
||||||
ball: {
|
|
||||||
color: props.theme === 'dark' ? 'rgb(244, 244, 245)' : 'rgb(24, 24, 27)',
|
|
||||||
scaleX: 1 + heightFactor * 0.25,
|
|
||||||
scaleY: 1 - heightFactor * 0.3,
|
|
||||||
x: `${ballIndex * barTotalWidth}px`,
|
|
||||||
y: `-${ballY}px`,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const frame = computed(() => frameAt(progress.value))
|
|
||||||
const bars = computed(() => frame.value.bars)
|
|
||||||
const ball = computed(() => frame.value.ball)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') return
|
|
||||||
if (typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches) return
|
|
||||||
|
|
||||||
const start = performance.now()
|
|
||||||
|
|
||||||
const tick = (now) => {
|
|
||||||
progress.value = ((now - start) % duration) / duration
|
|
||||||
animationFrame = window.requestAnimationFrame(tick)
|
|
||||||
}
|
|
||||||
|
|
||||||
animationFrame = window.requestAnimationFrame(tick)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (typeof window !== 'undefined' && typeof window.cancelAnimationFrame === 'function') {
|
|
||||||
window.cancelAnimationFrame(animationFrame)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.wave-physics-loader {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wave-physics-track {
|
|
||||||
position: relative;
|
|
||||||
width: 292px;
|
|
||||||
height: 192px;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: flex-start;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wave-physics-bar {
|
|
||||||
width: 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
transform-origin: bottom center;
|
|
||||||
will-change: height, background-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wave-physics-ball {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
bottom: 0;
|
|
||||||
width: 12px;
|
|
||||||
height: 12px;
|
|
||||||
border-radius: 999px;
|
|
||||||
transform-origin: bottom center;
|
|
||||||
box-shadow: 0 1px 3px color-mix(in srgb, currentColor 24%, transparent);
|
|
||||||
will-change: transform, background-color;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.wave-physics-track {
|
|
||||||
width: 260px;
|
|
||||||
transform: scale(0.9);
|
|
||||||
transform-origin: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
|
||||||
.wave-physics-bar,
|
|
||||||
.wave-physics-ball {
|
|
||||||
will-change: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -8,7 +8,11 @@
|
|||||||
<span class="preference-icon-frame"><LanguageOutline /></span>
|
<span class="preference-icon-frame"><LanguageOutline /></span>
|
||||||
</button>
|
</button>
|
||||||
<button class="icon-button" type="button" :title="theme === 'dark' ? t('common.themeLight') : t('common.themeDark')" :aria-label="theme === 'dark' ? t('common.themeLight') : t('common.themeDark')" @click="toggleTheme">
|
<button class="icon-button" type="button" :title="theme === 'dark' ? t('common.themeLight') : t('common.themeDark')" :aria-label="theme === 'dark' ? t('common.themeLight') : t('common.themeDark')" @click="toggleTheme">
|
||||||
<span class="preference-icon-frame"><SunnyOutline v-if="theme === 'dark'" /><MoonOutline v-else /></span>
|
<span class="preference-icon-frame">
|
||||||
|
<Transition name="preference-icon">
|
||||||
|
<span :key="theme" class="preference-icon"><SunnyOutline v-if="theme === 'dark'" /><MoonOutline v-else /></span>
|
||||||
|
</Transition>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -50,11 +50,21 @@
|
|||||||
@click="sidebarAgentsCollapsed = !sidebarAgentsCollapsed"
|
@click="sidebarAgentsCollapsed = !sidebarAgentsCollapsed"
|
||||||
>
|
>
|
||||||
<span>{{ t('nav.sidebarAgents') }}</span>
|
<span>{{ t('nav.sidebarAgents') }}</span>
|
||||||
|
<small v-if="sidebarAgentsCollapsed" class="nav-heading-count">{{ sidebarAgentCount }}</small>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!sidebarAgentsCollapsed" id="sidebar-agent-list">
|
<div v-if="!sidebarAgentsCollapsed" id="sidebar-agent-list">
|
||||||
|
<div v-if="discoveringAgents && !sidebarAgentGroups.length" class="sidebar-discovery" role="status">
|
||||||
|
<span v-for="index in 3" :key="index" class="sidebar-discovery-row" aria-hidden="true"><i /><b /></span>
|
||||||
|
<p>{{ t('onboarding.detecting') }}</p>
|
||||||
|
</div>
|
||||||
<div v-for="agentGroup in sidebarAgentGroups" :key="agentGroup.id" class="sidebar-agent-location-group">
|
<div v-for="agentGroup in sidebarAgentGroups" :key="agentGroup.id" class="sidebar-agent-location-group">
|
||||||
<p class="sidebar-agent-location-heading">{{ t(agentGroup.titleKey) }}</p>
|
<p class="sidebar-agent-location-heading">{{ t(agentGroup.titleKey) }}</p>
|
||||||
<article v-for="agent in agentGroup.agents" :key="agent.kind" class="sidebar-agent">
|
<article
|
||||||
|
v-for="agent in agentGroup.agents"
|
||||||
|
:key="agent.kind"
|
||||||
|
class="sidebar-agent"
|
||||||
|
:class="{ unavailable: !agent.ready }"
|
||||||
|
>
|
||||||
<header class="sidebar-agent-header">
|
<header class="sidebar-agent-header">
|
||||||
<button
|
<button
|
||||||
class="sidebar-agent-main"
|
class="sidebar-agent-main"
|
||||||
@@ -68,10 +78,15 @@
|
|||||||
<span class="agent-logo-cloud" :class="{ cloud: agent.cloud }">
|
<span class="agent-logo-cloud" :class="{ cloud: agent.cloud }">
|
||||||
<img :src="agent.logo" :alt="agent.label" />
|
<img :src="agent.logo" :alt="agent.label" />
|
||||||
<CloudOutline v-if="agent.cloud" aria-hidden="true" />
|
<CloudOutline v-if="agent.cloud" aria-hidden="true" />
|
||||||
|
<span
|
||||||
|
v-if="agentAwaitingDecision(agent.kind)"
|
||||||
|
class="attention-dot agent-attention-dot"
|
||||||
|
:title="t('humanGate.waiting')"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
<strong>{{ agent.label }}</strong>
|
<strong>{{ agent.label }}</strong>
|
||||||
<small>
|
<small :data-tone="agent.ready ? undefined : agentState(agent).tone">
|
||||||
{{ !agent.ready ? agentState(agent).label : directGroupsFor(agent.kind).length
|
{{ !agent.ready ? agentState(agent).label : directGroupsFor(agent.kind).length
|
||||||
? t('nav.agentSessions', { count: directGroupsFor(agent.kind).length })
|
? t('nav.agentSessions', { count: directGroupsFor(agent.kind).length })
|
||||||
: t('nav.noAgentSessions') }}
|
: t('nav.noAgentSessions') }}
|
||||||
@@ -110,7 +125,16 @@
|
|||||||
<span>{{ groupName(group) }}</span>
|
<span>{{ groupName(group) }}</span>
|
||||||
</button>
|
</button>
|
||||||
<span
|
<span
|
||||||
v-if="isGroupRunning(group.id) && !isSelectedConversation(group)"
|
v-if="isGroupAwaitingDecision(group.id) && !isSelectedConversation(group)"
|
||||||
|
class="attention-mark"
|
||||||
|
role="img"
|
||||||
|
:title="t('humanGate.waiting')"
|
||||||
|
:aria-label="t('humanGate.waiting')"
|
||||||
|
>
|
||||||
|
<span class="attention-dot" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="isGroupRunning(group.id) && !isSelectedConversation(group)"
|
||||||
class="run-mark"
|
class="run-mark"
|
||||||
:title="t('conversation.runningGeneric')"
|
:title="t('conversation.runningGeneric')"
|
||||||
>
|
>
|
||||||
@@ -169,7 +193,7 @@
|
|||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="!sidebarAgentGroups.length && !sidebarAgentsCollapsed" class="nav-empty">{{ t('nav.noSidebarAgents') }}</p>
|
<p v-if="!sidebarAgentGroups.length && !sidebarAgentsCollapsed && !discoveringAgents" class="nav-empty">{{ t('nav.noSidebarAgents') }}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="nav-section group-nav-section">
|
<section class="nav-section group-nav-section">
|
||||||
@@ -182,6 +206,7 @@
|
|||||||
@click="sidebarGroupsCollapsed = !sidebarGroupsCollapsed"
|
@click="sidebarGroupsCollapsed = !sidebarGroupsCollapsed"
|
||||||
>
|
>
|
||||||
<span>{{ t('nav.groups') }}</span>
|
<span>{{ t('nav.groups') }}</span>
|
||||||
|
<small v-if="sidebarGroupsCollapsed" class="nav-heading-count">{{ groupGroups.length }}</small>
|
||||||
</button>
|
</button>
|
||||||
<div v-if="!sidebarGroupsCollapsed" id="sidebar-group-list">
|
<div v-if="!sidebarGroupsCollapsed" id="sidebar-group-list">
|
||||||
<div v-if="groupGroups.length" class="group-conversation-list">
|
<div v-if="groupGroups.length" class="group-conversation-list">
|
||||||
@@ -203,12 +228,30 @@
|
|||||||
</span>
|
</span>
|
||||||
<span>{{ groupName(group) }}</span>
|
<span>{{ groupName(group) }}</span>
|
||||||
<span
|
<span
|
||||||
v-if="isGroupRunning(group.id) && !isSelectedConversation(group)"
|
v-if="isGroupAwaitingDecision(group.id) && !isSelectedConversation(group)"
|
||||||
|
class="attention-mark"
|
||||||
|
role="img"
|
||||||
|
:title="t('humanGate.waiting')"
|
||||||
|
:aria-label="t('humanGate.waiting')"
|
||||||
|
>
|
||||||
|
<span class="attention-dot" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="isGroupRunning(group.id) && !isSelectedConversation(group)"
|
||||||
class="run-mark"
|
class="run-mark"
|
||||||
:title="t('conversation.runningGeneric')"
|
:title="t('conversation.runningGeneric')"
|
||||||
>
|
>
|
||||||
<span class="run-agent-bars" aria-hidden="true"><i /><i /><i /></span>
|
<span class="run-agent-bars" aria-hidden="true"><i /><i /><i /></span>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="hasFinishedDirectRun(group.id) && !isSelectedConversation(group)"
|
||||||
|
class="run-finished-mark"
|
||||||
|
role="img"
|
||||||
|
:title="t('nav.runFinished')"
|
||||||
|
:aria-label="t('nav.runFinished')"
|
||||||
|
>
|
||||||
|
<CheckmarkCircleOutline aria-hidden="true" />
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<span v-if="!isGroupRunning(group.id) || isSelectedConversation(group)" class="group-conversation-actions">
|
<span v-if="!isGroupRunning(group.id) || isSelectedConversation(group)" class="group-conversation-actions">
|
||||||
<button
|
<button
|
||||||
@@ -270,9 +313,11 @@
|
|||||||
@click="toggleCollapsedGroupMenu"
|
@click="toggleCollapsedGroupMenu"
|
||||||
>
|
>
|
||||||
<ChatbubblesOutline />
|
<ChatbubblesOutline />
|
||||||
<span v-if="groupGroups.length" class="collapsed-group-count" aria-hidden="true">
|
<span
|
||||||
{{ groupGroups.length > 99 ? '99+' : groupGroups.length }}
|
v-if="groupGroups.some(group => isGroupAwaitingDecision(group.id))"
|
||||||
</span>
|
class="attention-dot collapsed-group-attention"
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -316,12 +361,30 @@
|
|||||||
</span>
|
</span>
|
||||||
<span class="collapsed-group-option-state">
|
<span class="collapsed-group-option-state">
|
||||||
<span
|
<span
|
||||||
v-if="isGroupRunning(group.id) && !isSelectedConversation(group)"
|
v-if="isGroupAwaitingDecision(group.id) && !isSelectedConversation(group)"
|
||||||
|
class="attention-mark"
|
||||||
|
role="img"
|
||||||
|
:title="t('humanGate.waiting')"
|
||||||
|
:aria-label="t('humanGate.waiting')"
|
||||||
|
>
|
||||||
|
<span class="attention-dot" />
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="isGroupRunning(group.id) && !isSelectedConversation(group)"
|
||||||
class="run-mark"
|
class="run-mark"
|
||||||
:title="t('conversation.runningGeneric')"
|
:title="t('conversation.runningGeneric')"
|
||||||
>
|
>
|
||||||
<span class="run-agent-bars" aria-hidden="true"><i /><i /><i /></span>
|
<span class="run-agent-bars" aria-hidden="true"><i /><i /><i /></span>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="hasFinishedDirectRun(group.id) && !isSelectedConversation(group)"
|
||||||
|
class="run-finished-mark"
|
||||||
|
role="img"
|
||||||
|
:title="t('nav.runFinished')"
|
||||||
|
:aria-label="t('nav.runFinished')"
|
||||||
|
>
|
||||||
|
<CheckmarkCircleOutline aria-hidden="true" />
|
||||||
|
</span>
|
||||||
<CheckmarkCircleOutline v-else-if="isSelectedConversation(group)" aria-hidden="true" />
|
<CheckmarkCircleOutline v-else-if="isSelectedConversation(group)" aria-hidden="true" />
|
||||||
<time v-else>{{ formatNavTime(group.updatedAt || group.createdAt) }}</time>
|
<time v-else>{{ formatNavTime(group.updatedAt || group.createdAt) }}</time>
|
||||||
</span>
|
</span>
|
||||||
@@ -355,7 +418,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { unref } from 'vue'
|
import { computed, unref } from 'vue'
|
||||||
import {
|
import {
|
||||||
AddOutline,
|
AddOutline,
|
||||||
ChatbubblesOutline,
|
ChatbubblesOutline,
|
||||||
@@ -384,6 +447,7 @@ const {
|
|||||||
createDirectSession,
|
createDirectSession,
|
||||||
deleteSidebarConversation,
|
deleteSidebarConversation,
|
||||||
directGroupsFor,
|
directGroupsFor,
|
||||||
|
discoveringAgents,
|
||||||
dismissSidebarDeleteConfirmation,
|
dismissSidebarDeleteConfirmation,
|
||||||
formatNavTime,
|
formatNavTime,
|
||||||
goHome,
|
goHome,
|
||||||
@@ -397,6 +461,7 @@ const {
|
|||||||
hasMoreGroupGroups,
|
hasMoreGroupGroups,
|
||||||
isDirectCreationPending,
|
isDirectCreationPending,
|
||||||
isDirectSessionListExpanded,
|
isDirectSessionListExpanded,
|
||||||
|
isGroupAwaitingDecision,
|
||||||
isGroupRunning,
|
isGroupRunning,
|
||||||
isSidebarAgentExpanded,
|
isSidebarAgentExpanded,
|
||||||
openConversationRename,
|
openConversationRename,
|
||||||
@@ -427,4 +492,12 @@ const {
|
|||||||
function isSelectedConversation(group) {
|
function isSelectedConversation(group) {
|
||||||
return unref(activeView) === 'conversation' && unref(selectedGroupId) === group.id
|
return unref(activeView) === 'conversation' && unref(selectedGroupId) === group.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const sidebarAgentCount = computed(() => (
|
||||||
|
unref(sidebarAgentGroups).reduce((total, group) => total + group.agents.length, 0)
|
||||||
|
))
|
||||||
|
|
||||||
|
function agentAwaitingDecision(kind) {
|
||||||
|
return directGroupsFor(kind).some(group => isGroupAwaitingDecision(group.id))
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -156,6 +156,33 @@ export function useAgentCatalog({
|
|||||||
return { label: t('agent.unverified'), tone: 'neutral', icon: WarningOutline }
|
return { label: t('agent.unverified'), tone: 'neutral', icon: WarningOutline }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// One plain sentence on why an installed Agent cannot be used and what to do
|
||||||
|
// next. Empty when there is nothing to explain (ready, or not installed).
|
||||||
|
function agentStateNote(agent) {
|
||||||
|
if (!agent || agent.ready || agent.custom || !agent.installed) return ''
|
||||||
|
const version = agent.resolvedVersion || agent.version || ''
|
||||||
|
const params = { version: version || t('agent.versionUnknown') }
|
||||||
|
const verified = agent.supportedVersionRange
|
||||||
|
? ` ${t('agent.note.verifiedRange', { range: agent.supportedVersionRange })}`
|
||||||
|
: ''
|
||||||
|
if (agent.incompatibilityReason === 'LOCAL_AGENT_CAPABILITY_PROBE_TIMEOUT') {
|
||||||
|
return t('agent.note.probeTimeout')
|
||||||
|
}
|
||||||
|
if (agent.compatibilityState === 'incompatible') {
|
||||||
|
const key = ({
|
||||||
|
LOCAL_AGENT_VERSION_UNSUPPORTED: 'agent.note.versionUnsupported',
|
||||||
|
LOCAL_AGENT_REQUIRED_CAPABILITY_MISSING: 'agent.note.cannotStart',
|
||||||
|
LOCAL_AGENT_PROTOCOL_UNAVAILABLE: 'agent.note.protocolUnavailable',
|
||||||
|
})[agent.incompatibilityReason] || 'agent.note.versionUnsupported'
|
||||||
|
return `${t(key, params)}${verified}`
|
||||||
|
}
|
||||||
|
if (agent.credentialState === 'missing') {
|
||||||
|
// A cloud Agent runs on the connected server, so that is where it signs in.
|
||||||
|
return t(agent.cloud ? 'agent.note.needsLoginCloud' : 'agent.note.needsLogin')
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeDirectAgent,
|
activeDirectAgent,
|
||||||
agentCatalogGroups,
|
agentCatalogGroups,
|
||||||
@@ -163,6 +190,7 @@ export function useAgentCatalog({
|
|||||||
agentImageLabel,
|
agentImageLabel,
|
||||||
agentSoul,
|
agentSoul,
|
||||||
agentState,
|
agentState,
|
||||||
|
agentStateNote,
|
||||||
installedCount,
|
installedCount,
|
||||||
mergedCatalog,
|
mergedCatalog,
|
||||||
readyAgentKinds,
|
readyAgentKinds,
|
||||||
|
|||||||
@@ -2,16 +2,42 @@ import { onBeforeUnmount, ref } from 'vue'
|
|||||||
import { errorCode } from '../desktop.js'
|
import { errorCode } from '../desktop.js'
|
||||||
import { t, translateError } from '../i18n.js'
|
import { t, translateError } from '../i18n.js'
|
||||||
|
|
||||||
|
// Long messages stay up long enough to read: 4 s plus about 60 ms per
|
||||||
|
// character, at most 10 s. Hovering the toast pauses the countdown.
|
||||||
|
const TOAST_MIN_MS = 4000
|
||||||
|
const TOAST_MAX_MS = 10000
|
||||||
|
const TOAST_MS_PER_CHARACTER = 60
|
||||||
|
|
||||||
|
function toastDuration(message) {
|
||||||
|
const length = String(message || '').length
|
||||||
|
return Math.min(TOAST_MAX_MS, TOAST_MIN_MS + length * TOAST_MS_PER_CHARACTER)
|
||||||
|
}
|
||||||
|
|
||||||
export function useAppNotifications() {
|
export function useAppNotifications() {
|
||||||
const toastMessage = ref('')
|
const toastMessage = ref('')
|
||||||
|
const toastTone = ref('warning')
|
||||||
const copyToastMessage = ref('')
|
const copyToastMessage = ref('')
|
||||||
let toastTimer = null
|
let toastTimer = null
|
||||||
let copyToastTimer = null
|
let copyToastTimer = null
|
||||||
|
|
||||||
function notify(message) {
|
function scheduleToastDismiss() {
|
||||||
toastMessage.value = message
|
|
||||||
clearTimeout(toastTimer)
|
clearTimeout(toastTimer)
|
||||||
toastTimer = setTimeout(() => { toastMessage.value = '' }, 3600)
|
toastTimer = setTimeout(() => { toastMessage.value = '' }, toastDuration(toastMessage.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
function notify(message, { tone = 'warning' } = {}) {
|
||||||
|
toastMessage.value = message
|
||||||
|
toastTone.value = tone === 'error' ? 'error' : 'warning'
|
||||||
|
scheduleToastDismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
function pauseToast() {
|
||||||
|
clearTimeout(toastTimer)
|
||||||
|
toastTimer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resumeToast() {
|
||||||
|
if (toastMessage.value) scheduleToastDismiss()
|
||||||
}
|
}
|
||||||
|
|
||||||
function showCopyToast() {
|
function showCopyToast() {
|
||||||
@@ -27,7 +53,7 @@ export function useAppNotifications() {
|
|||||||
|
|
||||||
function showError(error) {
|
function showError(error) {
|
||||||
console.error('[Meldwork]', errorCode(error))
|
console.error('[Meldwork]', errorCode(error))
|
||||||
notify(translateError(error))
|
notify(translateError(error), { tone: 'error' })
|
||||||
}
|
}
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
@@ -39,8 +65,11 @@ export function useAppNotifications() {
|
|||||||
copyToastMessage,
|
copyToastMessage,
|
||||||
dismissToast,
|
dismissToast,
|
||||||
notify,
|
notify,
|
||||||
|
pauseToast,
|
||||||
|
resumeToast,
|
||||||
showCopyToast,
|
showCopyToast,
|
||||||
showError,
|
showError,
|
||||||
toastMessage,
|
toastMessage,
|
||||||
|
toastTone,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||||
import { publicAsset } from '../catalog.js'
|
import { publicAsset } from '../catalog.js'
|
||||||
import { locale, setLocale } from '../i18n.js'
|
import { locale, setLocale } from '../i18n.js'
|
||||||
import { readProductPreference, writeProductPreference } from '../product-preferences.js'
|
import { readProductPreference, writeProductPreference } from '../product-preferences.js'
|
||||||
|
|
||||||
function initialTheme() {
|
const THEME_PREFERENCES = new Set(['system', 'light', 'dark'])
|
||||||
|
// Matches --bg in base-foundation.css for each theme.
|
||||||
|
const THEME_COLORS = { light: '#f3f6f8', dark: '#111618' }
|
||||||
|
|
||||||
|
function initialThemePreference() {
|
||||||
try {
|
try {
|
||||||
const saved = readProductPreference('theme')
|
const saved = readProductPreference('theme')
|
||||||
if (saved === 'light' || saved === 'dark') return saved
|
if (THEME_PREFERENCES.has(saved)) return saved
|
||||||
} catch { /* noop */ }
|
} catch { /* noop */ }
|
||||||
|
return 'system'
|
||||||
|
}
|
||||||
|
|
||||||
|
function systemTheme() {
|
||||||
return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
return typeof matchMedia === 'function' && matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAppPreferences() {
|
export function useAppPreferences() {
|
||||||
const theme = ref(initialTheme())
|
// 'system' follows the OS appearance; 'light' and 'dark' are explicit.
|
||||||
|
// Only an explicit choice by the user is written back, so a first launch
|
||||||
|
// keeps following the system instead of freezing whatever it was then.
|
||||||
|
const themePreference = ref(initialThemePreference())
|
||||||
|
const theme = ref(themePreference.value === 'system' ? systemTheme() : themePreference.value)
|
||||||
|
let colorSchemeQuery = null
|
||||||
const productMark = computed(() => publicAsset(
|
const productMark = computed(() => publicAsset(
|
||||||
theme.value === 'dark' ? 'logos/meldwork-mark-v3-dark.svg' : 'logos/meldwork-mark-v3.svg',
|
theme.value === 'dark' ? 'logos/meldwork-mark-v3-dark.svg' : 'logos/meldwork-mark-v3.svg',
|
||||||
))
|
))
|
||||||
@@ -24,12 +37,32 @@ export function useAppPreferences() {
|
|||||||
function applyTheme(value) {
|
function applyTheme(value) {
|
||||||
document.documentElement.dataset.theme = value
|
document.documentElement.dataset.theme = value
|
||||||
document.documentElement.style.colorScheme = value
|
document.documentElement.style.colorScheme = value
|
||||||
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', value === 'dark' ? '#0e171d' : '#f3f6f8')
|
document.querySelector('meta[name="theme-color"]')?.setAttribute('content', THEME_COLORS[value] || THEME_COLORS.light)
|
||||||
try { writeProductPreference('theme', value) } catch { /* noop */ }
|
}
|
||||||
|
|
||||||
|
function syncSystemTheme() {
|
||||||
|
if (themePreference.value === 'system') theme.value = systemTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
function followSystemTheme(enabled) {
|
||||||
|
if (enabled && !colorSchemeQuery && typeof matchMedia === 'function') {
|
||||||
|
colorSchemeQuery = matchMedia('(prefers-color-scheme: dark)')
|
||||||
|
colorSchemeQuery.addEventListener?.('change', syncSystemTheme)
|
||||||
|
} else if (!enabled && colorSchemeQuery) {
|
||||||
|
colorSchemeQuery.removeEventListener?.('change', syncSystemTheme)
|
||||||
|
colorSchemeQuery = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setThemePreference(value) {
|
||||||
|
const next = THEME_PREFERENCES.has(value) ? value : 'system'
|
||||||
|
themePreference.value = next
|
||||||
|
try { writeProductPreference('theme', next) } catch { /* noop */ }
|
||||||
|
theme.value = next === 'system' ? systemTheme() : next
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
theme.value = theme.value === 'dark' ? 'light' : 'dark'
|
setThemePreference(theme.value === 'dark' ? 'light' : 'dark')
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleLocale() {
|
function toggleLocale() {
|
||||||
@@ -37,13 +70,17 @@ export function useAppPreferences() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
watch(theme, applyTheme)
|
watch(theme, applyTheme)
|
||||||
|
watch(themePreference, value => followSystemTheme(value === 'system'), { immediate: true })
|
||||||
|
onBeforeUnmount(() => followSystemTheme(false))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
productAppIcon,
|
productAppIcon,
|
||||||
productMark,
|
productMark,
|
||||||
productWordmark,
|
productWordmark,
|
||||||
|
setThemePreference,
|
||||||
theme,
|
theme,
|
||||||
|
themePreference,
|
||||||
toggleLocale,
|
toggleLocale,
|
||||||
toggleTheme,
|
toggleTheme,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -248,6 +248,29 @@ export function useComposerContext({
|
|||||||
automaticTeamFormation.value = context?.automaticTeamFormation === true
|
automaticTeamFormation.value = context?.automaticTeamFormation === true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unsent text is kept per conversation while the app is open, so a quick look
|
||||||
|
// at another conversation (or saving this one's settings) does not cost what
|
||||||
|
// was typed. Attachments and mentions are still reset: they are tied to the
|
||||||
|
// conversation's members and to files staged for it.
|
||||||
|
const draftsByGroupId = new Map()
|
||||||
|
|
||||||
|
function stashComposerDraft(groupId) {
|
||||||
|
const id = String(groupId || '')
|
||||||
|
if (!id) return
|
||||||
|
if (draft.value.trim()) draftsByGroupId.set(id, draft.value)
|
||||||
|
else draftsByGroupId.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreComposerDraft(groupId) {
|
||||||
|
const text = draftsByGroupId.get(String(groupId || ''))
|
||||||
|
if (text) draft.value = text
|
||||||
|
}
|
||||||
|
|
||||||
|
function pruneComposerDrafts(groupIds) {
|
||||||
|
const live = new Set((Array.isArray(groupIds) ? groupIds : []).map(String))
|
||||||
|
for (const id of draftsByGroupId.keys()) if (!live.has(id)) draftsByGroupId.delete(id)
|
||||||
|
}
|
||||||
|
|
||||||
function resetComposerContext(group) {
|
function resetComposerContext(group) {
|
||||||
discussionMode.value = group?.conversationType === 'direct' ? 'manual' : 'auto'
|
discussionMode.value = group?.conversationType === 'direct' ? 'manual' : 'auto'
|
||||||
automaticTeamFormation.value = false
|
automaticTeamFormation.value = false
|
||||||
@@ -308,11 +331,13 @@ export function useComposerContext({
|
|||||||
isComposerTargetSelected,
|
isComposerTargetSelected,
|
||||||
manualModeLabel,
|
manualModeLabel,
|
||||||
openSkillMenu,
|
openSkillMenu,
|
||||||
|
pruneComposerDrafts,
|
||||||
removeAgentMention,
|
removeAgentMention,
|
||||||
removeKnowledgeBase,
|
removeKnowledgeBase,
|
||||||
removeSkill,
|
removeSkill,
|
||||||
resetComposerContext,
|
resetComposerContext,
|
||||||
restoreComposerContext,
|
restoreComposerContext,
|
||||||
|
restoreComposerDraft,
|
||||||
scheduleComposerResize,
|
scheduleComposerResize,
|
||||||
selectComposerMenuOption,
|
selectComposerMenuOption,
|
||||||
selectedAgentKinds,
|
selectedAgentKinds,
|
||||||
@@ -323,6 +348,7 @@ export function useComposerContext({
|
|||||||
skillActiveIndex,
|
skillActiveIndex,
|
||||||
skillMenuOpen,
|
skillMenuOpen,
|
||||||
skillsLoading,
|
skillsLoading,
|
||||||
|
stashComposerDraft,
|
||||||
targetKinds,
|
targetKinds,
|
||||||
toggleTarget,
|
toggleTarget,
|
||||||
toggleAutomaticTeamFormation,
|
toggleAutomaticTeamFormation,
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ export function useRunFinishedNotifications({
|
|||||||
pendingEvents.delete(key)
|
pendingEvents.delete(key)
|
||||||
}
|
}
|
||||||
clearFinishedRun(normalizedEvent)
|
clearFinishedRun(normalizedEvent)
|
||||||
if (group.conversationType !== 'direct') return
|
|
||||||
if (normalizedEvent.status !== 'completed') {
|
if (normalizedEvent.status !== 'completed') {
|
||||||
if (hasFinishedDirectRun(groupId)) setFinishedDirectRun(groupId, false)
|
if (hasFinishedDirectRun(groupId)) setFinishedDirectRun(groupId, false)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
function normalizedSection(section) {
|
function normalizedSection(section) {
|
||||||
if (section === 'cloud-agents') return 'cloud-agents'
|
if (section === 'cloud-agents') return 'cloud-agents'
|
||||||
if (section === 'knowledge-bases') return 'knowledge-bases'
|
if (section === 'knowledge-bases') return 'knowledge-bases'
|
||||||
|
if (section === 'general') return 'general'
|
||||||
return 'agents'
|
return 'agents'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-19
@@ -3,10 +3,6 @@ export default {
|
|||||||
'app.desktopRequired': 'Open Meldwork in the desktop app.',
|
'app.desktopRequired': 'Open Meldwork in the desktop app.',
|
||||||
'app.desktopRequiredDetail': 'The local Agent bridge is unavailable in this window.',
|
'app.desktopRequiredDetail': 'The local Agent bridge is unavailable in this window.',
|
||||||
'app.loading': 'Reading local Agents',
|
'app.loading': 'Reading local Agents',
|
||||||
'agentDiscovery.kicker': 'Local workspace startup',
|
|
||||||
'agentDiscovery.title': 'Finding your local Agents',
|
|
||||||
'agentDiscovery.body': 'Meldwork is checking the Agent CLIs available on this computer.',
|
|
||||||
'agentDiscovery.status': 'Preparing your workspace',
|
|
||||||
'onboarding.discoverTitle': 'Bring your local Agents together',
|
'onboarding.discoverTitle': 'Bring your local Agents together',
|
||||||
'onboarding.discoverBody': 'Meldwork finds the supported Agent CLIs already available on this computer.',
|
'onboarding.discoverBody': 'Meldwork finds the supported Agent CLIs already available on this computer.',
|
||||||
'onboarding.collaborationTitle': 'Share one task across Agents',
|
'onboarding.collaborationTitle': 'Share one task across Agents',
|
||||||
@@ -22,7 +18,8 @@ export default {
|
|||||||
'onboarding.detecting': 'Detecting local Agents',
|
'onboarding.detecting': 'Detecting local Agents',
|
||||||
'onboarding.loading': 'Preparing workspace',
|
'onboarding.loading': 'Preparing workspace',
|
||||||
'onboarding.continue': 'Next',
|
'onboarding.continue': 'Next',
|
||||||
'onboarding.start': 'Start using',
|
'onboarding.start': 'Get started',
|
||||||
|
'onboarding.skip': 'Skip',
|
||||||
'nav.newGroup': 'New group',
|
'nav.newGroup': 'New group',
|
||||||
'nav.collapseSidebar': 'Collapse sidebar',
|
'nav.collapseSidebar': 'Collapse sidebar',
|
||||||
'nav.expandSidebar': 'Expand sidebar',
|
'nav.expandSidebar': 'Expand sidebar',
|
||||||
@@ -74,6 +71,9 @@ export default {
|
|||||||
'home.recentTitle': 'Continue working',
|
'home.recentTitle': 'Continue working',
|
||||||
'home.recentSubtitle': 'Open a recent conversation and continue from its local context.',
|
'home.recentSubtitle': 'Open a recent conversation and continue from its local context.',
|
||||||
'home.recentEmpty': 'Create a group or direct chat to start building your local history.',
|
'home.recentEmpty': 'Create a group or direct chat to start building your local history.',
|
||||||
|
'home.attentionTitle': 'Needs your attention',
|
||||||
|
'home.attentionItem': '{agent} is waiting for you: {type}',
|
||||||
|
'home.attentionOpen': 'Review',
|
||||||
'home.firstTaskTitle': 'Start your first task',
|
'home.firstTaskTitle': 'Start your first task',
|
||||||
'home.firstTaskSubtitle': 'There is no local conversation history yet.',
|
'home.firstTaskSubtitle': 'There is no local conversation history yet.',
|
||||||
'home.agentStatusTitle': 'Start a new task',
|
'home.agentStatusTitle': 'Start a new task',
|
||||||
@@ -99,6 +99,14 @@ export default {
|
|||||||
'agent.incompatibleVersion': 'Unsupported version',
|
'agent.incompatibleVersion': 'Unsupported version',
|
||||||
'agent.incompatibleCapability': 'Required capability missing',
|
'agent.incompatibleCapability': 'Required capability missing',
|
||||||
'agent.incompatibleProtocol': 'Required protocol unavailable',
|
'agent.incompatibleProtocol': 'Required protocol unavailable',
|
||||||
|
'agent.versionUnknown': 'unknown',
|
||||||
|
'agent.note.versionUnsupported': 'The installed version ({version}) is outside the range Meldwork has verified, so it is unavailable for now.',
|
||||||
|
'agent.note.cannotStart': 'The installed version ({version}) did not pass the startup check: its command line cannot run, or it lacks a capability Meldwork needs.',
|
||||||
|
'agent.note.protocolUnavailable': 'The installed version ({version}) does not support a protocol Meldwork needs.',
|
||||||
|
'agent.note.verifiedRange': 'Verified versions: {range}.',
|
||||||
|
'agent.note.probeTimeout': 'Detection timed out. Make sure it starts normally in a terminal, then detect again.',
|
||||||
|
'agent.note.needsLogin': 'Run it in a terminal and finish signing in, then detect again.',
|
||||||
|
'agent.note.needsLoginCloud': 'This is a cloud Agent: sign in to it on its server, then detect again.',
|
||||||
'agent.detectedVersion': 'Version {version}',
|
'agent.detectedVersion': 'Version {version}',
|
||||||
'agent.localSkills': '{count} local skills',
|
'agent.localSkills': '{count} local skills',
|
||||||
'agent.cloudSkills': '{count} remote skills',
|
'agent.cloudSkills': '{count} remote skills',
|
||||||
@@ -218,9 +226,9 @@ export default {
|
|||||||
'run.status.interrupted': 'Interrupted',
|
'run.status.interrupted': 'Interrupted',
|
||||||
'run.status.notStarted': 'Not started',
|
'run.status.notStarted': 'Not started',
|
||||||
'run.status.roundLimit': 'Round limit reached',
|
'run.status.roundLimit': 'Round limit reached',
|
||||||
'run.status.budgetExhausted': 'Hard budget reached',
|
'run.status.budgetExhausted': 'Usage limit reached',
|
||||||
'run.status.circuitBreaker': 'Circuit breaker stopped',
|
'run.status.circuitBreaker': 'Stopped after repeated failures',
|
||||||
'run.status.unknown': 'Updated',
|
'run.status.unknown': 'Unknown',
|
||||||
'run.step.reasoning': 'Analyze task',
|
'run.step.reasoning': 'Analyze task',
|
||||||
'run.step.process': 'Run process',
|
'run.step.process': 'Run process',
|
||||||
'run.step.writeFile': 'Write file',
|
'run.step.writeFile': 'Write file',
|
||||||
@@ -255,7 +263,7 @@ export default {
|
|||||||
'trace.detailUnavailable': 'This earlier run retained tool names and statuses, but not detailed inputs or results.',
|
'trace.detailUnavailable': 'This earlier run retained tool names and statuses, but not detailed inputs or results.',
|
||||||
'trace.eventDetailUnavailable': 'Detailed input and result data was not captured for this event.',
|
'trace.eventDetailUnavailable': 'Detailed input and result data was not captured for this event.',
|
||||||
'trace.truncated': 'Some execution details were compacted to keep this conversation stable.',
|
'trace.truncated': 'Some execution details were compacted to keep this conversation stable.',
|
||||||
'trace.attemptInjection': 'Attempt injection',
|
'trace.attemptInjection': 'Context and diagnostics',
|
||||||
'trace.contextIncluded': '{count} messages injected for this attempt',
|
'trace.contextIncluded': '{count} messages injected for this attempt',
|
||||||
'trace.contextOmitted': '{count} messages compacted',
|
'trace.contextOmitted': '{count} messages compacted',
|
||||||
'trace.contextChars': '{count} context characters',
|
'trace.contextChars': '{count} context characters',
|
||||||
@@ -292,10 +300,6 @@ export default {
|
|||||||
'trace.sessionUnknownLegacyWarning': 'Legacy native Session history is unknown. The injected message IDs for this attempt are not the complete context.',
|
'trace.sessionUnknownLegacyWarning': 'Legacy native Session history is unknown. The injected message IDs for this attempt are not the complete context.',
|
||||||
'humanGate.pendingTitle': 'Action required',
|
'humanGate.pendingTitle': 'Action required',
|
||||||
'humanGate.waiting': 'Waiting for your decision',
|
'humanGate.waiting': 'Waiting for your decision',
|
||||||
'humanGate.waitingWordOne': 'Waiting',
|
|
||||||
'humanGate.waitingWordTwo': 'Thinking',
|
|
||||||
'humanGate.waitingWordThree': 'Preparing',
|
|
||||||
'humanGate.waitingWordFour': 'Reviewing',
|
|
||||||
'humanGate.type.permission': 'Permission',
|
'humanGate.type.permission': 'Permission',
|
||||||
'humanGate.type.budget': 'Budget',
|
'humanGate.type.budget': 'Budget',
|
||||||
'humanGate.type.decision': 'Review decision',
|
'humanGate.type.decision': 'Review decision',
|
||||||
@@ -411,9 +415,9 @@ export default {
|
|||||||
'composer.autoRounds': '{count} rounds',
|
'composer.autoRounds': '{count} rounds',
|
||||||
'composer.maxRounds': 'Maximum rounds',
|
'composer.maxRounds': 'Maximum rounds',
|
||||||
'composer.unlimitedRounds': 'No round limit',
|
'composer.unlimitedRounds': 'No round limit',
|
||||||
'composer.unlimitedHint': 'The discussion continues until collaboration ends or you stop it. Agents can discuss, divide work, collaborate, and review material gaps once concrete results exist. Agent timeouts and the mandatory circuit breaker still apply.',
|
'composer.unlimitedHint': 'The discussion continues until collaboration ends or you stop it. Agents can discuss, divide work, collaborate, and review material gaps once concrete results exist. A single Agent timing out, or too many failures in a row, still stops it automatically.',
|
||||||
'composer.unlimitedConfirmTitle': 'Continue without a round limit?',
|
'composer.unlimitedConfirmTitle': 'Continue without a round limit?',
|
||||||
'composer.unlimitedConfirm': 'The discussion can continue until collaboration ends or you stop it and may consume more resources. Agents can discuss, divide work, collaborate, and review material gaps once concrete results exist. Agent timeouts and the mandatory circuit breaker still apply. Continue?',
|
'composer.unlimitedConfirm': 'The discussion can continue until collaboration ends or you stop it and may use more resources. Agents can discuss, divide work, collaborate, and review material gaps once concrete results exist. A single Agent timing out, or too many failures in a row, still stops it automatically.',
|
||||||
'composer.useBoundedRounds': 'Use bounded rounds',
|
'composer.useBoundedRounds': 'Use bounded rounds',
|
||||||
'composer.addTarget': 'Include {agent}',
|
'composer.addTarget': 'Include {agent}',
|
||||||
'composer.removeTarget': 'Exclude {agent}',
|
'composer.removeTarget': 'Exclude {agent}',
|
||||||
@@ -489,6 +493,14 @@ export default {
|
|||||||
'systemSettings.agents': 'Agents',
|
'systemSettings.agents': 'Agents',
|
||||||
'systemSettings.cloudServers': 'Cloud servers',
|
'systemSettings.cloudServers': 'Cloud servers',
|
||||||
'systemSettings.knowledgeBases': 'Knowledge bases',
|
'systemSettings.knowledgeBases': 'Knowledge bases',
|
||||||
|
'systemSettings.general': 'General',
|
||||||
|
'generalSettings.languageTitle': 'Language',
|
||||||
|
'generalSettings.languageHint': 'The language used across the interface.',
|
||||||
|
'generalSettings.themeTitle': 'Appearance',
|
||||||
|
'generalSettings.themeHint': 'System follows your operating system’s light or dark appearance.',
|
||||||
|
'generalSettings.themeSystem': 'System',
|
||||||
|
'generalSettings.themeLight': 'Light',
|
||||||
|
'generalSettings.themeDark': 'Dark',
|
||||||
'systemSettings.openAgentDetail': 'View {agent} details',
|
'systemSettings.openAgentDetail': 'View {agent} details',
|
||||||
'systemSettings.openAgentDetailDefault': 'Agent details',
|
'systemSettings.openAgentDetailDefault': 'Agent details',
|
||||||
'systemSettings.officialAgents': 'Official Agents',
|
'systemSettings.officialAgents': 'Official Agents',
|
||||||
@@ -531,6 +543,7 @@ export default {
|
|||||||
'cloudAgents.connect': 'Connect and discover',
|
'cloudAgents.connect': 'Connect and discover',
|
||||||
'cloudAgents.refresh': 'Refresh connections',
|
'cloudAgents.refresh': 'Refresh connections',
|
||||||
'cloudAgents.delete': 'Delete connection',
|
'cloudAgents.delete': 'Delete connection',
|
||||||
|
'cloudAgents.deletePrompt': 'Delete this connection?',
|
||||||
'cloudAgents.online': 'Online',
|
'cloudAgents.online': 'Online',
|
||||||
'cloudAgents.offline': 'Unavailable',
|
'cloudAgents.offline': 'Unavailable',
|
||||||
'cloudAgents.agentCount': '{count} Agent CLIs',
|
'cloudAgents.agentCount': '{count} Agent CLIs',
|
||||||
@@ -642,7 +655,7 @@ export default {
|
|||||||
'agentDetail.location': 'Location',
|
'agentDetail.location': 'Location',
|
||||||
'agentDetail.cloudLocation': '{server} · Version {version}',
|
'agentDetail.cloudLocation': '{server} · Version {version}',
|
||||||
'agentDetail.images': 'Images',
|
'agentDetail.images': 'Images',
|
||||||
'agentDetail.soul': 'Soul',
|
'agentDetail.soul': 'About',
|
||||||
'agentDetail.localSkills': 'Local skills',
|
'agentDetail.localSkills': 'Local skills',
|
||||||
'agentDetail.remoteSkills': 'Remote skills',
|
'agentDetail.remoteSkills': 'Remote skills',
|
||||||
'agentDetail.showInSidebar': 'Show in sidebar',
|
'agentDetail.showInSidebar': 'Show in sidebar',
|
||||||
@@ -684,13 +697,13 @@ export default {
|
|||||||
'common.more': 'More options',
|
'common.more': 'More options',
|
||||||
'common.dismiss': 'Dismiss',
|
'common.dismiss': 'Dismiss',
|
||||||
'system.agentCallFailed': '{agent} failed: {reason}',
|
'system.agentCallFailed': '{agent} failed: {reason}',
|
||||||
'system.agentBudgetExhausted': '{agent} stopped because a hard run budget was exceeded.',
|
'system.agentBudgetExhausted': '{agent} stopped after reaching this run’s usage limit.',
|
||||||
'system.agentStopped': '{agent} was stopped by the user.',
|
'system.agentStopped': '{agent} was stopped by the user.',
|
||||||
'system.agentInterrupted': '{agent} was interrupted when Meldwork restarted.',
|
'system.agentInterrupted': '{agent} was interrupted when Meldwork restarted.',
|
||||||
'system.autoStopped': 'Automatic discussion stopped: {reason}',
|
'system.autoStopped': 'Automatic discussion stopped: {reason}',
|
||||||
'system.autoDiscussionStalled': 'Automatic discussion stopped because repeated handoffs produced no new contribution.',
|
'system.autoDiscussionStalled': 'Automatic discussion stopped because repeated handoffs produced no new contribution.',
|
||||||
'system.autoRoundLimit': 'Automatic discussion reached its {rounds}-round limit. The latest replies above are the current result; raise the round limit or send a follow-up to continue.',
|
'system.autoRoundLimit': 'Automatic discussion reached its {rounds}-round limit. The latest replies above are the current result; raise the round limit or send a follow-up to continue.',
|
||||||
'system.autoCrossReview': 'The delivery owner signaled completion before peer discussion; one cross-review round runs first.',
|
'system.autoCrossReview': 'The Agent leading delivery thinks the task is done, so the other Agents cross-check it once first.',
|
||||||
'system.autoOwnerChanged': '{agent} took over as delivery owner because {previous} is no longer available.',
|
'system.autoOwnerChanged': '{agent} took over as delivery owner because {previous} is no longer available.',
|
||||||
'system.autoRouteIgnored': 'Requests for {agents} were skipped because they are not active in this discussion.',
|
'system.autoRouteIgnored': 'Requests for {agents} were skipped because they are not active in this discussion.',
|
||||||
'system.autoPeerBlockerForwarded': '{agent} reported a blocker; the delivery owner will decide how to proceed. Reason: {reason}',
|
'system.autoPeerBlockerForwarded': '{agent} reported a blocker; the delivery owner will decide how to proceed. Reason: {reason}',
|
||||||
@@ -703,7 +716,7 @@ export default {
|
|||||||
'system.agentPausedAfterFailure': '{agent} is paused for the rest of this discussion: {reason} It stays in the group and will be tried again on the next message.',
|
'system.agentPausedAfterFailure': '{agent} is paused for the rest of this discussion: {reason} It stays in the group and will be tried again on the next message.',
|
||||||
'system.agentRemovedAfterFailure': '{agent} was removed from the group after a terminal failure. The discussion continued with the remaining Agents.',
|
'system.agentRemovedAfterFailure': '{agent} was removed from the group after a terminal failure. The discussion continued with the remaining Agents.',
|
||||||
'system.autoTimeout': 'Automatic discussion stopped after a runtime timeout before consensus.',
|
'system.autoTimeout': 'Automatic discussion stopped after a runtime timeout before consensus.',
|
||||||
'system.runCircuitBreaker': 'The mandatory circuit breaker stopped this run after {maxAttempts} Agent attempts.',
|
'system.runCircuitBreaker': 'This run stopped automatically after {maxAttempts} failed Agent attempts.',
|
||||||
'error.generic': 'The operation could not be completed.',
|
'error.generic': 'The operation could not be completed.',
|
||||||
'error.cloudAgentBridgeAddress': 'Enter an HTTPS IP address for the server.',
|
'error.cloudAgentBridgeAddress': 'Enter an HTTPS IP address for the server.',
|
||||||
'error.cloudAgentBridgeManifest': 'The server did not return a recognized Meldwork Agent Bridge manifest.',
|
'error.cloudAgentBridgeManifest': 'The server did not return a recognized Meldwork Agent Bridge manifest.',
|
||||||
|
|||||||
+32
-19
@@ -1,12 +1,8 @@
|
|||||||
export default {
|
export default {
|
||||||
'app.localWorkspace': '多智能体协作工作台',
|
'app.localWorkspace': '多 Agent 协作工作台',
|
||||||
'app.desktopRequired': '请在 Meldwork 桌面端打开。',
|
'app.desktopRequired': '请在 Meldwork 桌面端打开。',
|
||||||
'app.desktopRequiredDetail': '当前窗口无法访问本地 Agent 桥接。',
|
'app.desktopRequiredDetail': '当前窗口无法访问本地 Agent 桥接。',
|
||||||
'app.loading': '正在读取本地 Agent',
|
'app.loading': '正在读取本地 Agent',
|
||||||
'agentDiscovery.kicker': '本地工作台启动中',
|
|
||||||
'agentDiscovery.title': '正在寻找本地 Agent',
|
|
||||||
'agentDiscovery.body': 'Meldwork 正在检查这台电脑上可用的 Agent CLI。',
|
|
||||||
'agentDiscovery.status': '正在准备工作台',
|
|
||||||
'onboarding.discoverTitle': '把本地 Agent 汇聚到一起',
|
'onboarding.discoverTitle': '把本地 Agent 汇聚到一起',
|
||||||
'onboarding.discoverBody': 'Meldwork 会检测这台电脑上已经可用的受支持 Agent CLI。',
|
'onboarding.discoverBody': 'Meldwork 会检测这台电脑上已经可用的受支持 Agent CLI。',
|
||||||
'onboarding.collaborationTitle': '让多个 Agent 共享同一任务',
|
'onboarding.collaborationTitle': '让多个 Agent 共享同一任务',
|
||||||
@@ -23,6 +19,7 @@ export default {
|
|||||||
'onboarding.loading': '正在准备工作台',
|
'onboarding.loading': '正在准备工作台',
|
||||||
'onboarding.continue': '下一步',
|
'onboarding.continue': '下一步',
|
||||||
'onboarding.start': '开始使用',
|
'onboarding.start': '开始使用',
|
||||||
|
'onboarding.skip': '跳过',
|
||||||
'nav.newGroup': '新建群聊',
|
'nav.newGroup': '新建群聊',
|
||||||
'nav.collapseSidebar': '收起侧边栏',
|
'nav.collapseSidebar': '收起侧边栏',
|
||||||
'nav.expandSidebar': '展开侧边栏',
|
'nav.expandSidebar': '展开侧边栏',
|
||||||
@@ -74,6 +71,9 @@ export default {
|
|||||||
'home.recentTitle': '继续工作',
|
'home.recentTitle': '继续工作',
|
||||||
'home.recentSubtitle': '打开最近会话,并沿用已有的本地上下文。',
|
'home.recentSubtitle': '打开最近会话,并沿用已有的本地上下文。',
|
||||||
'home.recentEmpty': '创建群聊或单聊后,最近会话会显示在这里。',
|
'home.recentEmpty': '创建群聊或单聊后,最近会话会显示在这里。',
|
||||||
|
'home.attentionTitle': '需要你处理',
|
||||||
|
'home.attentionItem': '{agent} 在等你确认:{type}',
|
||||||
|
'home.attentionOpen': '去处理',
|
||||||
'home.firstTaskTitle': '开始第一个任务',
|
'home.firstTaskTitle': '开始第一个任务',
|
||||||
'home.firstTaskSubtitle': '当前还没有本地会话记录。',
|
'home.firstTaskSubtitle': '当前还没有本地会话记录。',
|
||||||
'home.agentStatusTitle': '开始新任务',
|
'home.agentStatusTitle': '开始新任务',
|
||||||
@@ -99,6 +99,14 @@ export default {
|
|||||||
'agent.incompatibleVersion': '版本不受支持',
|
'agent.incompatibleVersion': '版本不受支持',
|
||||||
'agent.incompatibleCapability': '缺少必需能力',
|
'agent.incompatibleCapability': '缺少必需能力',
|
||||||
'agent.incompatibleProtocol': '必需协议不可用',
|
'agent.incompatibleProtocol': '必需协议不可用',
|
||||||
|
'agent.versionUnknown': '未知版本',
|
||||||
|
'agent.note.versionUnsupported': '已安装的版本 {version} 不在 Meldwork 已验证的范围内,暂时无法使用。',
|
||||||
|
'agent.note.cannotStart': '已安装的版本 {version} 没能通过启动检查:它的命令行无法运行,或缺少 Meldwork 需要的能力。',
|
||||||
|
'agent.note.protocolUnavailable': '已安装的版本 {version} 不支持 Meldwork 需要的通信协议。',
|
||||||
|
'agent.note.verifiedRange': '已验证的版本:{range}。',
|
||||||
|
'agent.note.probeTimeout': '检测超时。请确认它能在终端里正常启动,然后重新检测。',
|
||||||
|
'agent.note.needsLogin': '请先在终端运行它并完成登录,然后重新检测。',
|
||||||
|
'agent.note.needsLoginCloud': '这是云端 Agent:请到对应的服务器上完成登录,然后重新检测。',
|
||||||
'agent.detectedVersion': '版本 {version}',
|
'agent.detectedVersion': '版本 {version}',
|
||||||
'agent.localSkills': '{count} 个本地技能',
|
'agent.localSkills': '{count} 个本地技能',
|
||||||
'agent.cloudSkills': '{count} 个远端技能',
|
'agent.cloudSkills': '{count} 个远端技能',
|
||||||
@@ -218,9 +226,9 @@ export default {
|
|||||||
'run.status.interrupted': '已中断',
|
'run.status.interrupted': '已中断',
|
||||||
'run.status.notStarted': '未开始',
|
'run.status.notStarted': '未开始',
|
||||||
'run.status.roundLimit': '达到轮数上限',
|
'run.status.roundLimit': '达到轮数上限',
|
||||||
'run.status.budgetExhausted': '达到硬预算上限',
|
'run.status.budgetExhausted': '已达到用量上限',
|
||||||
'run.status.circuitBreaker': '已触发熔断器',
|
'run.status.circuitBreaker': '失败过多,已自动停止',
|
||||||
'run.status.unknown': '已更新',
|
'run.status.unknown': '状态未知',
|
||||||
'run.step.reasoning': '分析任务',
|
'run.step.reasoning': '分析任务',
|
||||||
'run.step.process': '运行进程',
|
'run.step.process': '运行进程',
|
||||||
'run.step.writeFile': '写入文件',
|
'run.step.writeFile': '写入文件',
|
||||||
@@ -255,7 +263,7 @@ export default {
|
|||||||
'trace.detailUnavailable': '这条早期运行仅保留了工具名称和状态,未记录具体输入与结果。',
|
'trace.detailUnavailable': '这条早期运行仅保留了工具名称和状态,未记录具体输入与结果。',
|
||||||
'trace.eventDetailUnavailable': '该事件未记录具体输入与结果。',
|
'trace.eventDetailUnavailable': '该事件未记录具体输入与结果。',
|
||||||
'trace.truncated': '为保持会话稳定,部分执行详情已被压缩。',
|
'trace.truncated': '为保持会话稳定,部分执行详情已被压缩。',
|
||||||
'trace.attemptInjection': '本次尝试注入',
|
'trace.attemptInjection': '上下文与诊断',
|
||||||
'trace.contextIncluded': '本次尝试注入 {count} 条消息',
|
'trace.contextIncluded': '本次尝试注入 {count} 条消息',
|
||||||
'trace.contextOmitted': '压缩 {count} 条消息',
|
'trace.contextOmitted': '压缩 {count} 条消息',
|
||||||
'trace.contextChars': '上下文 {count} 字符',
|
'trace.contextChars': '上下文 {count} 字符',
|
||||||
@@ -292,10 +300,6 @@ export default {
|
|||||||
'trace.sessionUnknownLegacyWarning': '旧版原生 Session 的历史来源未知。本次尝试注入的消息 ID 并不代表完整上下文。',
|
'trace.sessionUnknownLegacyWarning': '旧版原生 Session 的历史来源未知。本次尝试注入的消息 ID 并不代表完整上下文。',
|
||||||
'humanGate.pendingTitle': '需要你的决定',
|
'humanGate.pendingTitle': '需要你的决定',
|
||||||
'humanGate.waiting': '等待你的决定',
|
'humanGate.waiting': '等待你的决定',
|
||||||
'humanGate.waitingWordOne': '等待中',
|
|
||||||
'humanGate.waitingWordTwo': '思考中',
|
|
||||||
'humanGate.waitingWordThree': '准备中',
|
|
||||||
'humanGate.waitingWordFour': '整理中',
|
|
||||||
'humanGate.type.permission': '权限',
|
'humanGate.type.permission': '权限',
|
||||||
'humanGate.type.budget': '预算',
|
'humanGate.type.budget': '预算',
|
||||||
'humanGate.type.decision': '评审决定',
|
'humanGate.type.decision': '评审决定',
|
||||||
@@ -411,9 +415,9 @@ export default {
|
|||||||
'composer.autoRounds': '{count} 轮',
|
'composer.autoRounds': '{count} 轮',
|
||||||
'composer.maxRounds': '最大轮数',
|
'composer.maxRounds': '最大轮数',
|
||||||
'composer.unlimitedRounds': '不限轮数',
|
'composer.unlimitedRounds': '不限轮数',
|
||||||
'composer.unlimitedHint': '讨论会持续到协作自然结束或你手动停止。Agent 可以讨论、分工和协作,在形成具体结果后再检查重要缺口;Agent 超时与不可关闭的熔断器仍然生效。',
|
'composer.unlimitedHint': '讨论会持续到协作自然结束或你手动停止。Agent 可以讨论、分工和协作,在形成具体结果后再检查重要缺口。单个 Agent 超时,或连续失败过多时,仍会自动停止。',
|
||||||
'composer.unlimitedConfirmTitle': '继续使用不限制轮数吗?',
|
'composer.unlimitedConfirmTitle': '继续使用不限制轮数吗?',
|
||||||
'composer.unlimitedConfirm': '讨论会持续到协作自然结束或你手动停止,并可能消耗更多资源。Agent 可以讨论、分工和协作,在形成具体结果后再检查重要缺口;Agent 超时与不可关闭的熔断器仍然生效。是否继续?',
|
'composer.unlimitedConfirm': '讨论会持续到协作自然结束或你手动停止,并可能消耗更多资源。Agent 可以讨论、分工和协作,在形成具体结果后再检查重要缺口。单个 Agent 超时,或连续失败过多时,仍会自动停止。',
|
||||||
'composer.useBoundedRounds': '改回固定轮数',
|
'composer.useBoundedRounds': '改回固定轮数',
|
||||||
'composer.addTarget': '加入 {agent}',
|
'composer.addTarget': '加入 {agent}',
|
||||||
'composer.removeTarget': '移除 {agent}',
|
'composer.removeTarget': '移除 {agent}',
|
||||||
@@ -489,6 +493,14 @@ export default {
|
|||||||
'systemSettings.agents': 'Agent',
|
'systemSettings.agents': 'Agent',
|
||||||
'systemSettings.cloudServers': '云服务器',
|
'systemSettings.cloudServers': '云服务器',
|
||||||
'systemSettings.knowledgeBases': '知识库',
|
'systemSettings.knowledgeBases': '知识库',
|
||||||
|
'systemSettings.general': '通用',
|
||||||
|
'generalSettings.languageTitle': '语言',
|
||||||
|
'generalSettings.languageHint': '界面文字使用的语言。',
|
||||||
|
'generalSettings.themeTitle': '外观',
|
||||||
|
'generalSettings.themeHint': '选择“跟随系统”时,会随系统的浅色与深色外观自动切换。',
|
||||||
|
'generalSettings.themeSystem': '跟随系统',
|
||||||
|
'generalSettings.themeLight': '浅色',
|
||||||
|
'generalSettings.themeDark': '深色',
|
||||||
'systemSettings.openAgentDetail': '查看 {agent} 详情',
|
'systemSettings.openAgentDetail': '查看 {agent} 详情',
|
||||||
'systemSettings.openAgentDetailDefault': 'Agent 详情',
|
'systemSettings.openAgentDetailDefault': 'Agent 详情',
|
||||||
'systemSettings.officialAgents': '官方 Agent',
|
'systemSettings.officialAgents': '官方 Agent',
|
||||||
@@ -531,6 +543,7 @@ export default {
|
|||||||
'cloudAgents.connect': '连接并探测',
|
'cloudAgents.connect': '连接并探测',
|
||||||
'cloudAgents.refresh': '刷新连接',
|
'cloudAgents.refresh': '刷新连接',
|
||||||
'cloudAgents.delete': '删除连接',
|
'cloudAgents.delete': '删除连接',
|
||||||
|
'cloudAgents.deletePrompt': '删除这个连接?',
|
||||||
'cloudAgents.online': '在线',
|
'cloudAgents.online': '在线',
|
||||||
'cloudAgents.offline': '无法连接',
|
'cloudAgents.offline': '无法连接',
|
||||||
'cloudAgents.agentCount': '{count} 个 Agent CLI',
|
'cloudAgents.agentCount': '{count} 个 Agent CLI',
|
||||||
@@ -642,7 +655,7 @@ export default {
|
|||||||
'agentDetail.location': '位置',
|
'agentDetail.location': '位置',
|
||||||
'agentDetail.cloudLocation': '{server} · 版本 {version}',
|
'agentDetail.cloudLocation': '{server} · 版本 {version}',
|
||||||
'agentDetail.images': '图片',
|
'agentDetail.images': '图片',
|
||||||
'agentDetail.soul': '角色',
|
'agentDetail.soul': '简介',
|
||||||
'agentDetail.localSkills': '本地技能',
|
'agentDetail.localSkills': '本地技能',
|
||||||
'agentDetail.remoteSkills': '远端技能',
|
'agentDetail.remoteSkills': '远端技能',
|
||||||
'agentDetail.showInSidebar': '在侧边栏显示',
|
'agentDetail.showInSidebar': '在侧边栏显示',
|
||||||
@@ -684,13 +697,13 @@ export default {
|
|||||||
'common.more': '更多操作',
|
'common.more': '更多操作',
|
||||||
'common.dismiss': '关闭',
|
'common.dismiss': '关闭',
|
||||||
'system.agentCallFailed': '{agent} 调用失败:{reason}',
|
'system.agentCallFailed': '{agent} 调用失败:{reason}',
|
||||||
'system.agentBudgetExhausted': '{agent} 因达到运行硬预算上限而停止。',
|
'system.agentBudgetExhausted': '{agent} 已达到本次运行的用量上限,已停止。',
|
||||||
'system.agentStopped': '{agent} 已由用户停止。',
|
'system.agentStopped': '{agent} 已由用户停止。',
|
||||||
'system.agentInterrupted': 'Meldwork 重启时,{agent} 的运行已中断。',
|
'system.agentInterrupted': 'Meldwork 重启时,{agent} 的运行已中断。',
|
||||||
'system.autoStopped': '自动讨论已停止:{reason}',
|
'system.autoStopped': '自动讨论已停止:{reason}',
|
||||||
'system.autoDiscussionStalled': '自动讨论因重复转交且没有新增内容而停止。',
|
'system.autoDiscussionStalled': '自动讨论因重复转交且没有新增内容而停止。',
|
||||||
'system.autoRoundLimit': '自动讨论已达到 {rounds} 轮上限,以上最新回复即为当前结果;如需继续,可调高轮数上限或追加消息。',
|
'system.autoRoundLimit': '自动讨论已达到 {rounds} 轮上限,以上最新回复即为当前结果;如需继续,可调高轮数上限或追加消息。',
|
||||||
'system.autoCrossReview': '交付负责人在同伴讨论前判断任务完成;将先进行一轮交叉审查。',
|
'system.autoCrossReview': '负责交付的 Agent 认为任务已完成,先请其他 Agent 交叉检查一轮。',
|
||||||
'system.autoOwnerChanged': '{previous} 已不可用,由 {agent} 接任交付负责人。',
|
'system.autoOwnerChanged': '{previous} 已不可用,由 {agent} 接任交付负责人。',
|
||||||
'system.autoRouteIgnored': '已跳过对 {agents} 的请求,因为它们不在本次讨论的活跃成员中。',
|
'system.autoRouteIgnored': '已跳过对 {agents} 的请求,因为它们不在本次讨论的活跃成员中。',
|
||||||
'system.autoPeerBlockerForwarded': '{agent} 报告了阻塞,将由交付负责人决定如何继续。原因:{reason}',
|
'system.autoPeerBlockerForwarded': '{agent} 报告了阻塞,将由交付负责人决定如何继续。原因:{reason}',
|
||||||
@@ -703,7 +716,7 @@ export default {
|
|||||||
'system.agentPausedAfterFailure': '{agent} 在本次讨论中暂停:{reason} 它仍在群聊中,下一条消息会再次尝试。',
|
'system.agentPausedAfterFailure': '{agent} 在本次讨论中暂停:{reason} 它仍在群聊中,下一条消息会再次尝试。',
|
||||||
'system.agentRemovedAfterFailure': '{agent} 发生终止性故障,已自动移出群聊;其余 Agent 将继续讨论。',
|
'system.agentRemovedAfterFailure': '{agent} 发生终止性故障,已自动移出群聊;其余 Agent 将继续讨论。',
|
||||||
'system.autoTimeout': '自动讨论在达成共识前因运行超时而停止。',
|
'system.autoTimeout': '自动讨论在达成共识前因运行超时而停止。',
|
||||||
'system.runCircuitBreaker': '不可关闭的熔断器已在 {maxAttempts} 次 Agent 尝试后停止本次运行。',
|
'system.runCircuitBreaker': '连续 {maxAttempts} 次 Agent 尝试都失败了,本次运行已自动停止。',
|
||||||
'error.generic': '操作未能完成。',
|
'error.generic': '操作未能完成。',
|
||||||
'error.cloudAgentBridgeAddress': '请输入服务器的 HTTPS IP 地址。',
|
'error.cloudAgentBridgeAddress': '请输入服务器的 HTTPS IP 地址。',
|
||||||
'error.cloudAgentBridgeManifest': '服务器未返回可识别的 Meldwork Agent Bridge 信息。',
|
'error.cloudAgentBridgeManifest': '服务器未返回可识别的 Meldwork Agent Bridge 信息。',
|
||||||
|
|||||||
@@ -1,151 +1,89 @@
|
|||||||
.agent-discovery-overlay {
|
/* Agent discovery runs in the background: the sidebar shows skeleton rows
|
||||||
position: fixed;
|
until the first detection returns. Only transform and opacity animate. */
|
||||||
inset: 0;
|
.sidebar-discovery {
|
||||||
z-index: 90;
|
|
||||||
display: grid;
|
display: grid;
|
||||||
place-items: center;
|
gap: 4px;
|
||||||
overflow: hidden;
|
padding: 4px 3px 6px;
|
||||||
isolation: isolate;
|
|
||||||
padding: 32px;
|
|
||||||
background: var(--bg);
|
|
||||||
color: var(--text);
|
|
||||||
cursor: progress;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-content {
|
.sidebar-discovery-row {
|
||||||
position: relative;
|
height: 38px;
|
||||||
z-index: 1;
|
|
||||||
width: min(430px, 100%);
|
|
||||||
display: grid;
|
display: grid;
|
||||||
justify-items: center;
|
grid-template-columns: 28px minmax(0, 1fr);
|
||||||
gap: 0;
|
align-items: center;
|
||||||
padding: 12px 20px;
|
gap: 8px;
|
||||||
text-align: center;
|
padding: 0 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-mark {
|
.sidebar-discovery-row i,
|
||||||
position: relative;
|
.sidebar-discovery-row b {
|
||||||
display: block;
|
display: block;
|
||||||
width: 52px;
|
border-radius: var(--radius-sm);
|
||||||
height: 52px;
|
background: color-mix(in srgb, var(--surface-active) 80%, transparent);
|
||||||
margin-bottom: 28px;
|
animation: discovery-shimmer 1.4s var(--ease-standard) infinite;
|
||||||
object-fit: contain;
|
|
||||||
filter: drop-shadow(0 9px 20px color-mix(in srgb, var(--accent) 18%, transparent));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-copy {
|
.sidebar-discovery-row i {
|
||||||
position: relative;
|
width: 28px;
|
||||||
z-index: 1;
|
height: 28px;
|
||||||
display: grid;
|
border-radius: 50%;
|
||||||
justify-items: center;
|
|
||||||
text-shadow: 0 1px 14px color-mix(in srgb, var(--bg) 72%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-kicker {
|
.sidebar-discovery-row b {
|
||||||
margin: 0 0 9px;
|
width: 62%;
|
||||||
color: var(--accent);
|
height: 10px;
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 700;
|
|
||||||
letter-spacing: 0.08em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-copy h1 {
|
.sidebar-discovery-row:nth-child(2) b { width: 48%; animation-delay: 0.12s; }
|
||||||
margin: 0;
|
.sidebar-discovery-row:nth-child(3) b { width: 56%; animation-delay: 0.24s; }
|
||||||
font-size: 30px;
|
|
||||||
line-height: 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-discovery-copy > p:not(.agent-discovery-kicker) {
|
.sidebar-discovery p {
|
||||||
max-width: 360px;
|
margin: 4px 8px 0;
|
||||||
margin: 10px 0 0;
|
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
line-height: 1.55;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-progress {
|
@keyframes discovery-shimmer {
|
||||||
width: min(260px, 100%);
|
0%, 100% { opacity: 0.55; }
|
||||||
|
50% { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-workspace-state.discovering,
|
||||||
|
.home-workspace-state.discovering svg {
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.boot-progress {
|
||||||
|
width: 140px;
|
||||||
height: 3px;
|
height: 3px;
|
||||||
margin-top: 26px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: color-mix(in srgb, var(--border) 68%, transparent);
|
background: color-mix(in srgb, var(--border) 70%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-progress span {
|
.boot-progress span {
|
||||||
display: block;
|
display: block;
|
||||||
width: 46%;
|
width: 40%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: inherit;
|
border-radius: inherit;
|
||||||
background: var(--onboarding-frame-gradient);
|
|
||||||
box-shadow: 0 0 14px color-mix(in srgb, var(--accent) 34%, transparent);
|
|
||||||
animation: agent-discovery-progress 1.35s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-discovery-status {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 7px;
|
|
||||||
margin-top: 13px;
|
|
||||||
color: var(--muted);
|
|
||||||
font-size: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-discovery-status-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--accent) 12%, transparent);
|
animation: boot-progress 1.2s var(--ease-standard) infinite;
|
||||||
animation: agent-discovery-dot 1.25s ease-in-out infinite;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-enter-active,
|
@keyframes boot-progress {
|
||||||
.agent-discovery-leave-active {
|
from { transform: translateX(-110%); }
|
||||||
transition: opacity 0.28s ease;
|
to { transform: translateX(260%); }
|
||||||
}
|
|
||||||
|
|
||||||
.agent-discovery-enter-active .agent-discovery-content,
|
|
||||||
.agent-discovery-leave-active .agent-discovery-content {
|
|
||||||
transition: opacity 0.28s ease, transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-discovery-enter-from,
|
|
||||||
.agent-discovery-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-discovery-enter-from .agent-discovery-content,
|
|
||||||
.agent-discovery-leave-to .agent-discovery-content {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px) scale(0.985);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes agent-discovery-progress {
|
|
||||||
0% { transform: translateX(-125%); opacity: 0.4; }
|
|
||||||
55% { opacity: 1; }
|
|
||||||
100% { transform: translateX(300%); opacity: 0.4; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes agent-discovery-dot {
|
|
||||||
0%, 100% { opacity: 0.42; transform: scale(0.86); }
|
|
||||||
50% { opacity: 1; transform: scale(1); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.agent-discovery-progress span,
|
.sidebar-discovery-row i,
|
||||||
.agent-discovery-status-dot {
|
.sidebar-discovery-row b,
|
||||||
|
.boot-progress span {
|
||||||
animation: none;
|
animation: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-discovery-progress span {
|
.boot-progress span {
|
||||||
width: 58%;
|
width: 100%;
|
||||||
transform: translateX(36%);
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
.agent-discovery-overlay { padding: 24px; }
|
|
||||||
.agent-discovery-copy h1 { font-size: 26px; }
|
|
||||||
.agent-discovery-mark { margin-bottom: 20px; }
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,21 @@
|
|||||||
font-synthesis: none;
|
font-synthesis: none;
|
||||||
text-rendering: optimizeLegibility;
|
text-rendering: optimizeLegibility;
|
||||||
color-scheme: light;
|
color-scheme: light;
|
||||||
|
/* Type scale. Nothing that has to be read goes below --text-xs; --text-2xs
|
||||||
|
is only for badges, tags and keyboard hints. */
|
||||||
|
--font-mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
--text-2xs: 11px;
|
||||||
|
--text-xs: 12px;
|
||||||
|
--text-sm: 13px;
|
||||||
|
--text-md: 14px;
|
||||||
|
--text-lg: 16px;
|
||||||
|
--text-xl: 18px;
|
||||||
|
--text-2xl: 22px;
|
||||||
|
--text-3xl: 28px;
|
||||||
|
/* PingFang SC stops at Semibold, so anything above 600 renders the same in
|
||||||
|
Chinese. Hierarchy comes from size and color, not from heavier weights. */
|
||||||
|
--weight-medium: 500;
|
||||||
|
--weight-semibold: 600;
|
||||||
--bg: #f3f6f8;
|
--bg: #f3f6f8;
|
||||||
--sidebar: #e9eef1;
|
--sidebar: #e9eef1;
|
||||||
--surface: #f9fbfc;
|
--surface: #f9fbfc;
|
||||||
@@ -13,20 +28,55 @@
|
|||||||
--border-strong: #b9c8cf;
|
--border-strong: #b9c8cf;
|
||||||
--text: #26343c;
|
--text: #26343c;
|
||||||
--text-soft: #50616a;
|
--text-soft: #50616a;
|
||||||
--muted: #788b94;
|
/* ≥4.5:1 on every light surface; small secondary text must stay readable. */
|
||||||
--faint: #9aaab1;
|
--muted: #5a6b75;
|
||||||
|
/* ≈3.3:1. Placeholders, disabled text and decorative icons only. */
|
||||||
|
--faint: #7d8d95;
|
||||||
--accent: #d45f52;
|
--accent: #d45f52;
|
||||||
--accent-hover: #b94e45;
|
--accent-hover: #b94e45;
|
||||||
--accent-soft: #f7e3df;
|
--accent-soft: #f7e3df;
|
||||||
--accent-contrast: #fffaf8;
|
--accent-contrast: #fffaf8;
|
||||||
--danger: #b74743;
|
/* Accent as text (≥4.5:1, also on --accent-soft) and as a filled button behind light text (≈4.6:1). */
|
||||||
--danger-soft: #fbe8e6;
|
--accent-text: #ad4840;
|
||||||
|
--accent-strong: #bd5047;
|
||||||
|
--accent-strong-hover: #a9463e;
|
||||||
|
--focus-ring: color-mix(in srgb, var(--accent) 72%, var(--surface-raised));
|
||||||
|
--scrollbar-thumb: rgb(38 52 60 / 0.22);
|
||||||
|
/* Status colors. The accent is for actions, selection and brand moments;
|
||||||
|
it never means "ready" or "done". */
|
||||||
|
--success: #2a7353;
|
||||||
|
--success-soft: #e6f4ec;
|
||||||
|
--warning: #9a5d08;
|
||||||
|
--warning-soft: #fdf1dc;
|
||||||
|
--danger: #b42318;
|
||||||
|
--danger-soft: #fdeceb;
|
||||||
|
--info: #2f6db3;
|
||||||
|
--info-soft: #e8f0fb;
|
||||||
--shadow: 0 12px 32px rgb(41 62 73 / 0.12);
|
--shadow: 0 12px 32px rgb(41 62 73 / 0.12);
|
||||||
|
--elev-1: 0 1px 2px rgb(32 48 58 / 0.06), 0 1px 1px rgb(32 48 58 / 0.04);
|
||||||
|
--elev-2: 0 4px 14px rgb(32 48 58 / 0.1), 0 1px 3px rgb(32 48 58 / 0.06);
|
||||||
|
--elev-3: 0 12px 30px rgb(32 48 58 / 0.14), 0 2px 6px rgb(32 48 58 / 0.06);
|
||||||
|
--radius-xs: 4px;
|
||||||
|
--radius-lg: 14px;
|
||||||
|
/* Layers, lowest to highest. */
|
||||||
|
--z-dropdown: 50;
|
||||||
|
--z-drawer: 60;
|
||||||
|
--z-overlay: 90;
|
||||||
|
--z-modal: 100;
|
||||||
|
--z-onboarding: 110;
|
||||||
|
--z-toast: 120;
|
||||||
|
--z-popover: 140;
|
||||||
|
--z-lightbox: 1000;
|
||||||
|
/* Motion. Enter with --ease-out, leave faster with --ease-in. */
|
||||||
|
--dur-press: 80ms;
|
||||||
|
--dur-fast: 120ms;
|
||||||
|
--dur-base: 180ms;
|
||||||
|
--dur-slow: 240ms;
|
||||||
|
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
--ease-standard: cubic-bezier(0.2, 0, 0, 1);
|
||||||
|
--ease-in: cubic-bezier(0.4, 0, 1, 1);
|
||||||
--shell-gradient: linear-gradient(145deg, #f1f8f7 0%, #e2efef 48%, #c9dde0 100%);
|
--shell-gradient: linear-gradient(145deg, #f1f8f7 0%, #e2efef 48%, #c9dde0 100%);
|
||||||
--onboarding-frame-gradient: linear-gradient(145deg, #dcefeb 0%, #9fc2ca 48%, #ddb5aa 100%);
|
--onboarding-frame-gradient: linear-gradient(145deg, #dcefeb 0%, #9fc2ca 48%, #ddb5aa 100%);
|
||||||
--pixel-blast-primary: #d92f24;
|
|
||||||
--pixel-blast-secondary: #007d91;
|
|
||||||
--pixel-blast-tertiary: #ed512f;
|
|
||||||
--workspace-surface: #fbfcfd;
|
--workspace-surface: #fbfcfd;
|
||||||
--workspace-shadow: 0 1px 2px rgb(41 62 73 / 0.08), 0 10px 28px rgb(41 62 73 / 0.08);
|
--workspace-shadow: 0 1px 2px rgb(41 62 73 / 0.08), 0 10px 28px rgb(41 62 73 / 0.08);
|
||||||
--radius-sm: 5px;
|
--radius-sm: 5px;
|
||||||
@@ -41,32 +91,44 @@
|
|||||||
|
|
||||||
:root[data-theme="dark"] {
|
:root[data-theme="dark"] {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
--bg: #121516;
|
--bg: #111618;
|
||||||
--sidebar: #191c1e;
|
--sidebar: #161c1e;
|
||||||
--surface: #1b1f21;
|
--surface: #1f2427;
|
||||||
--surface-raised: #24292c;
|
--surface-raised: #262c2f;
|
||||||
--surface-hover: #292e31;
|
--surface-hover: #2b3134;
|
||||||
--surface-active: #30363a;
|
--surface-active: #32393c;
|
||||||
--border: #3a4246;
|
--border: #3a4246;
|
||||||
--border-strong: #515b60;
|
--border-strong: #515b60;
|
||||||
--text: #e6ebed;
|
--text: #e6ebed;
|
||||||
--text-soft: #c3cdd1;
|
--text-soft: #c3cdd1;
|
||||||
--muted: #93a0a5;
|
--muted: #9aa6ab;
|
||||||
--faint: #707b80;
|
--faint: #808b90;
|
||||||
--accent: #d98568;
|
--accent: #d98568;
|
||||||
--accent-hover: #efa080;
|
--accent-hover: #efa080;
|
||||||
--accent-soft: #3b2a24;
|
--accent-soft: #3b2a24;
|
||||||
--accent-contrast: #1b110d;
|
--accent-contrast: #1b110d;
|
||||||
--danger: #ee817c;
|
--accent-text: #e2906f;
|
||||||
|
--accent-strong: #d98568;
|
||||||
|
--accent-strong-hover: #efa080;
|
||||||
|
--scrollbar-thumb: rgb(230 235 237 / 0.2);
|
||||||
|
--success: #6cc3a0;
|
||||||
|
--success-soft: #17302a;
|
||||||
|
--warning: #e0a54a;
|
||||||
|
--warning-soft: #35290f;
|
||||||
|
--danger: #f97066;
|
||||||
--danger-soft: #402326;
|
--danger-soft: #402326;
|
||||||
|
--info: #7fb0ea;
|
||||||
|
--info-soft: #172a40;
|
||||||
--shadow: 0 16px 42px rgb(0 0 0 / 0.42);
|
--shadow: 0 16px 42px rgb(0 0 0 / 0.42);
|
||||||
--shell-gradient: linear-gradient(145deg, #2d3a3b 0%, #222e32 48%, #172226 100%);
|
--elev-1: 0 1px 2px rgb(0 0 0 / 0.3);
|
||||||
|
--elev-2: 0 6px 18px rgb(0 0 0 / 0.32), 0 1px 3px rgb(0 0 0 / 0.24);
|
||||||
|
--elev-3: 0 14px 34px rgb(0 0 0 / 0.4), 0 2px 6px rgb(0 0 0 / 0.24);
|
||||||
|
/* The backdrop stays darker than the workspace card so the card reads as
|
||||||
|
raised, the same way it does in the light theme. */
|
||||||
|
--shell-gradient: linear-gradient(145deg, #1f292b 0%, #182124 48%, #111a1d 100%);
|
||||||
--onboarding-frame-gradient: linear-gradient(145deg, #4a696b 0%, #2e4751 52%, #775047 100%);
|
--onboarding-frame-gradient: linear-gradient(145deg, #4a696b 0%, #2e4751 52%, #775047 100%);
|
||||||
--pixel-blast-primary: #ff836d;
|
--workspace-surface: #1c2124;
|
||||||
--pixel-blast-secondary: #54f5ff;
|
--workspace-shadow: 0 0 0 1px rgb(255 255 255 / 0.05), 0 14px 34px rgb(0 0 0 / 0.32);
|
||||||
--pixel-blast-tertiary: #ffa36f;
|
|
||||||
--workspace-surface: #181c1e;
|
|
||||||
--workspace-shadow: 0 1px 2px rgb(0 0 0 / 0.42), 0 14px 34px rgb(0 0 0 / 0.24);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -97,7 +159,7 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: var(--bg);
|
background: var(--bg);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 14px;
|
font-size: var(--text-md);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +186,7 @@ button:focus-visible,
|
|||||||
input:focus-visible,
|
input:focus-visible,
|
||||||
textarea:focus-visible,
|
textarea:focus-visible,
|
||||||
select:focus-visible {
|
select:focus-visible {
|
||||||
outline: 2px solid var(--accent);
|
outline: 2px solid var(--focus-ring);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,6 +203,18 @@ select:disabled {
|
|||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.conversation-nav,
|
||||||
|
.trace-panel-scroll,
|
||||||
|
.settings-panel,
|
||||||
|
.agent-home,
|
||||||
|
.collapsed-group-menu-list,
|
||||||
|
.trace-select-menu,
|
||||||
|
.onboarding-slide > div,
|
||||||
|
.trace-event-body pre {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: var(--scrollbar-thumb) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
button svg {
|
button svg {
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
@@ -295,5 +369,5 @@ button svg {
|
|||||||
|
|
||||||
.bridge-state h1 {
|
.bridge-state h1 {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
font-size: 24px;
|
font-size: var(--text-2xl);
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
.onboarding-slide h1 {
|
.onboarding-slide h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 25px;
|
font-size: var(--text-2xl);
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +149,31 @@
|
|||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.onboarding-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skipping used to need Escape, which nobody discovers. */
|
||||||
|
.onboarding-skip {
|
||||||
|
min-height: 36px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color var(--dur-fast) var(--ease-standard), background-color var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.onboarding-skip:hover {
|
||||||
|
background: var(--surface-hover);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.onboarding-primary {
|
.onboarding-primary {
|
||||||
min-width: 170px;
|
min-width: 170px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
width: min(300px, calc(100vw - 96px));
|
width: min(300px, calc(100vw - 96px));
|
||||||
max-height: min(420px, calc(100vh - 32px));
|
max-height: min(420px, calc(100vh - 32px));
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 90;
|
z-index: var(--z-overlay);
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
@@ -29,13 +29,13 @@
|
|||||||
|
|
||||||
.collapsed-group-menu-header strong {
|
.collapsed-group-menu-header strong {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed-group-menu-header small {
|
.collapsed-group-menu-header small {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed-group-menu-icon {
|
.collapsed-group-menu-icon {
|
||||||
@@ -104,13 +104,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.collapsed-group-option-main strong {
|
.collapsed-group-option-main strong {
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed-group-option-main small {
|
.collapsed-group-option-main small {
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 9px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed-group-option-state {
|
.collapsed-group-option-state {
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
|
|
||||||
.collapsed-group-option-state time {
|
.collapsed-group-option-state time {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 9px;
|
font-size: var(--text-xs);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 14px 8px;
|
padding: 14px 8px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-link > span:nth-child(2) {
|
.conversation-link > span:nth-child(2) {
|
||||||
@@ -160,13 +160,38 @@
|
|||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-pulse {
|
/* "Needs your decision": an amber dot with a soft halo. */
|
||||||
|
.attention-mark {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attention-dot {
|
||||||
|
position: relative;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
display: block;
|
flex: 0 0 auto;
|
||||||
|
display: inline-block;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--accent);
|
background: var(--warning);
|
||||||
animation: run-pulse 1.2s ease-in-out infinite;
|
}
|
||||||
|
|
||||||
|
.attention-dot::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: -3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--warning);
|
||||||
|
opacity: 0.22;
|
||||||
|
animation: attention-halo 1.8s var(--ease-standard) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes attention-halo {
|
||||||
|
0% { opacity: 0.4; transform: scale(0.7); }
|
||||||
|
70%, 100% { opacity: 0; transform: scale(1.7); }
|
||||||
}
|
}
|
||||||
|
|
||||||
.run-finished-mark {
|
.run-finished-mark {
|
||||||
@@ -175,7 +200,7 @@
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: var(--accent);
|
color: var(--success);
|
||||||
animation: run-finished-in 0.22s ease-out both;
|
animation: run-finished-in 0.22s ease-out both;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,10 +209,6 @@
|
|||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes run-pulse {
|
|
||||||
50% { opacity: 0.42; transform: scale(0.78); }
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes run-finished-in {
|
@keyframes run-finished-in {
|
||||||
from { opacity: 0; transform: scale(0.7); }
|
from { opacity: 0; transform: scale(0.7); }
|
||||||
}
|
}
|
||||||
@@ -195,7 +216,7 @@
|
|||||||
.nav-empty {
|
.nav-empty {
|
||||||
margin: 2px 8px 0;
|
margin: 2px 8px 0;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
font-size: 12px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-footer {
|
.sidebar-footer {
|
||||||
@@ -303,5 +324,5 @@
|
|||||||
|
|
||||||
.footer-count {
|
.footer-count {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
@@ -81,18 +81,18 @@
|
|||||||
|
|
||||||
.brand-button strong {
|
.brand-button strong {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 15px;
|
font-size: var(--text-md);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-button.active strong {
|
.brand-button.active strong {
|
||||||
color: var(--accent);
|
color: var(--accent-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-button small {
|
.brand-button small {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -222,22 +222,10 @@
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.collapsed-group-count {
|
.collapsed-group-attention {
|
||||||
min-width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: -2px;
|
top: 5px;
|
||||||
right: -3px;
|
right: 5px;
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
padding: 0 3px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--accent);
|
|
||||||
color: var(--accent-contrast);
|
|
||||||
font-size: 8px;
|
|
||||||
font-weight: 800;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-button:hover {
|
.icon-button:hover {
|
||||||
@@ -260,28 +248,38 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 2px 12px 12px;
|
margin: 2px 12px 12px;
|
||||||
border: 1px solid var(--accent);
|
border: 1px solid var(--accent-strong);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--accent);
|
background: var(--accent-strong);
|
||||||
color: var(--accent-contrast);
|
color: var(--accent-contrast);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color var(--dur-fast) var(--ease-standard),
|
||||||
|
border-color var(--dur-fast) var(--ease-standard),
|
||||||
|
transform var(--dur-press) var(--ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-group-button:hover,
|
.new-group-button:hover,
|
||||||
.primary-button:hover,
|
.primary-button:hover:not(:disabled),
|
||||||
.send-button:hover {
|
.send-button:hover:not(:disabled) {
|
||||||
border-color: var(--accent-hover);
|
border-color: var(--accent-strong-hover);
|
||||||
background: var(--accent-hover);
|
background: var(--accent-strong-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Pressing scales slightly instead of shifting, so nothing next to the
|
||||||
|
button moves with it. The composer's send and stop buttons keep their
|
||||||
|
original one-pixel press. */
|
||||||
.new-group-button:active,
|
.new-group-button:active,
|
||||||
.primary-button:active,
|
.primary-button:active:not(:disabled),
|
||||||
.secondary-button:active,
|
.secondary-button:active:not(:disabled),
|
||||||
.send-button:active,
|
.danger-button:active:not(:disabled),
|
||||||
.stop-button:active,
|
|
||||||
.danger-button:active,
|
|
||||||
.sidebar-more-button:active {
|
.sidebar-more-button:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-button:active,
|
||||||
|
.stop-button:active {
|
||||||
transform: translateY(1px);
|
transform: translateY(1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,14 +299,33 @@
|
|||||||
height: 28px;
|
height: 28px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
padding: 0 8px;
|
padding: 0 8px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: color var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A collapsed section shows how much it hides. */
|
||||||
|
.nav-heading-count {
|
||||||
|
min-width: 18px;
|
||||||
|
height: 16px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--surface-active);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--text-2xs);
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-heading:hover,
|
.nav-heading:hover,
|
||||||
@@ -326,9 +343,9 @@
|
|||||||
|
|
||||||
.sidebar-agent-location-heading {
|
.sidebar-agent-location-heading {
|
||||||
margin: 6px 8px 3px;
|
margin: 6px 8px 3px;
|
||||||
color: var(--faint);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: var(--text-xs);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -390,6 +407,22 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.agent-attention-dot {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
right: -3px;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-agent.unavailable .sidebar-agent-main img {
|
||||||
|
filter: grayscale(1);
|
||||||
|
opacity: 0.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-agent-main small[data-tone="warning"] {
|
||||||
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
.agent-logo-cloud > svg {
|
.agent-logo-cloud > svg {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -4px;
|
right: -4px;
|
||||||
@@ -418,12 +451,12 @@
|
|||||||
|
|
||||||
.sidebar-agent-main strong {
|
.sidebar-agent-main strong {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-agent-main small {
|
.sidebar-agent-main small {
|
||||||
color: var(--faint);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-agent-new,
|
.sidebar-agent-new,
|
||||||
@@ -547,6 +580,7 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
|
font-size: var(--text-sm);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
@@ -612,7 +646,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.direct-session-row > .run-mark,
|
.direct-session-row > .run-mark,
|
||||||
.direct-session-row > .run-finished-mark {
|
.direct-session-row > .run-finished-mark,
|
||||||
|
.direct-session-row > .attention-mark {
|
||||||
grid-column: 4;
|
grid-column: 4;
|
||||||
grid-row: 1;
|
grid-row: 1;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
@@ -628,7 +663,7 @@
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 26px minmax(0, 1fr) 58px;
|
grid-template-columns: 26px minmax(0, 1fr) auto;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 9px;
|
gap: 9px;
|
||||||
padding: 0 9px;
|
padding: 0 9px;
|
||||||
@@ -636,16 +671,24 @@
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
|
font-size: var(--text-sm);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-conversation-row {
|
.group-conversation-row {
|
||||||
|
--row-background: var(--bg);
|
||||||
position: relative;
|
position: relative;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.group-conversation-row:hover,
|
||||||
|
.group-conversation-row:focus-within {
|
||||||
|
--row-background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
.group-conversation-row.active {
|
.group-conversation-row.active {
|
||||||
|
--row-background: var(--surface-active);
|
||||||
background: var(--surface-active);
|
background: var(--surface-active);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -654,17 +697,24 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Row actions float over the end of the name with a short fade, so the name
|
||||||
|
can use the full row width when they are hidden. */
|
||||||
.group-conversation-actions {
|
.group-conversation-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
right: 6px;
|
right: 4px;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
|
padding-left: 18px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: linear-gradient(90deg, transparent, var(--row-background) 16px);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transform: translateY(-50%) translateX(4px);
|
transform: translateY(-50%) translateX(4px);
|
||||||
transition: opacity 0.14s ease, transform 0.16s ease;
|
transition:
|
||||||
|
opacity var(--dur-fast) var(--ease-standard),
|
||||||
|
transform var(--dur-base) var(--ease-out);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-conversation-row.active .group-conversation-actions,
|
.group-conversation-row.active .group-conversation-actions,
|
||||||
@@ -675,15 +725,13 @@
|
|||||||
transform: translateY(-50%) translateX(0);
|
transform: translateY(-50%) translateX(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-conversation-row.active .conversation-link > .run-mark,
|
.conversation-link:hover {
|
||||||
.group-conversation-row:hover .conversation-link > .run-mark,
|
color: var(--text);
|
||||||
.group-conversation-row:focus-within .conversation-link > .run-mark {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation-link:hover {
|
.group-conversation-row:hover:not(.active),
|
||||||
|
.group-conversation-row:focus-within:not(.active) {
|
||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
color: var(--text);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-more-button {
|
.sidebar-more-button {
|
||||||
@@ -699,8 +747,8 @@
|
|||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
transition: background 0.14s ease, color 0.14s ease, transform 0.14s ease;
|
transition: background 0.14s ease, color 0.14s ease, transform 0.14s ease;
|
||||||
@@ -755,3 +803,29 @@
|
|||||||
.titlebar-preferences { display: flex; gap: 2px; }
|
.titlebar-preferences { display: flex; gap: 2px; }
|
||||||
.app-shell:not([data-platform="darwin"]) { --desktop-titlebar-height: 32px; }
|
.app-shell:not([data-platform="darwin"]) { --desktop-titlebar-height: 32px; }
|
||||||
.titlebar-actions .shortcut-menu { top: calc(100% + 8px); right: 0; }
|
.titlebar-actions .shortcut-menu { top: calc(100% + 8px); right: 0; }
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
.sidebar:not(.collapsed) .brand-button span,
|
||||||
|
.sidebar:not(.collapsed) .new-group-button span,
|
||||||
|
.sidebar:not(.collapsed) .nav-heading,
|
||||||
|
.sidebar:not(.collapsed) .sidebar-agent-location-heading,
|
||||||
|
.sidebar:not(.collapsed) .sidebar-agent-main > span,
|
||||||
|
.sidebar:not(.collapsed) .sidebar-agent-new,
|
||||||
|
.sidebar:not(.collapsed) .direct-session-list,
|
||||||
|
.sidebar:not(.collapsed) .group-nav-section {
|
||||||
|
transition: opacity var(--dur-base) var(--ease-standard) 80ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
.sidebar:not(.collapsed) .brand-button span,
|
||||||
|
.sidebar:not(.collapsed) .new-group-button span,
|
||||||
|
.sidebar:not(.collapsed) .nav-heading,
|
||||||
|
.sidebar:not(.collapsed) .sidebar-agent-location-heading,
|
||||||
|
.sidebar:not(.collapsed) .sidebar-agent-main > span,
|
||||||
|
.sidebar:not(.collapsed) .sidebar-agent-new,
|
||||||
|
.sidebar:not(.collapsed) .direct-session-list,
|
||||||
|
.sidebar:not(.collapsed) .group-nav-section {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.manager-toolbar-actions {
|
.manager-toolbar-actions {
|
||||||
@@ -78,13 +78,13 @@
|
|||||||
display: block;
|
display: block;
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.manager-note {
|
.manager-note {
|
||||||
max-width: 190px;
|
max-width: 190px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -94,8 +94,8 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
margin-top: 7px;
|
margin-top: 7px;
|
||||||
color: var(--accent);
|
color: var(--accent-text);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.install-progress {
|
.install-progress {
|
||||||
|
|||||||
@@ -2,25 +2,30 @@
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: 22px;
|
right: 22px;
|
||||||
bottom: 22px;
|
bottom: 22px;
|
||||||
z-index: 120;
|
z-index: var(--z-toast);
|
||||||
max-width: min(420px, calc(100vw - 44px));
|
max-width: min(420px, calc(100vw - 44px));
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 9px;
|
gap: 10px;
|
||||||
padding: 11px 13px;
|
padding: 12px 12px 12px 14px;
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--surface-raised);
|
background: var(--surface-raised);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--elev-3);
|
||||||
color: var(--text-soft);
|
color: var(--text);
|
||||||
line-height: 1.45;
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message[data-tone="error"] {
|
||||||
|
border-color: color-mix(in srgb, var(--danger) 30%, var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-toast-message {
|
.copy-toast-message {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: calc(var(--desktop-titlebar-height) + 14px);
|
top: calc(var(--desktop-titlebar-height) + 14px);
|
||||||
left: 50%;
|
left: 50%;
|
||||||
z-index: 130;
|
z-index: var(--z-toast);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@@ -29,14 +34,14 @@
|
|||||||
background: color-mix(in srgb, var(--surface-raised) 92%, var(--accent-soft));
|
background: color-mix(in srgb, var(--surface-raised) 92%, var(--accent-soft));
|
||||||
box-shadow: 0 6px 18px rgb(23 35 40 / 0.16);
|
box-shadow: 0 6px 18px rgb(23 35 40 / 0.16);
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-toast-message svg {
|
.copy-toast-message svg {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
color: var(--accent);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-message > span {
|
.toast-message > span {
|
||||||
@@ -47,8 +52,12 @@
|
|||||||
.toast-message svg {
|
.toast-message svg {
|
||||||
width: 17px;
|
width: 17px;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
margin-top: 1px;
|
margin-top: 2px;
|
||||||
color: var(--accent);
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-message[data-tone="error"] > svg {
|
||||||
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-message .toast-dismiss-button svg {
|
.toast-message .toast-dismiss-button svg {
|
||||||
@@ -58,15 +67,18 @@
|
|||||||
color: currentColor;
|
color: currentColor;
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-enter-active,
|
.toast-enter-active {
|
||||||
|
transition: opacity var(--dur-base) var(--ease-out), transform var(--dur-slow) var(--ease-out);
|
||||||
|
}
|
||||||
|
|
||||||
.toast-leave-active {
|
.toast-leave-active {
|
||||||
transition: opacity 0.16s ease, transform 0.16s ease;
|
transition: opacity var(--dur-fast) var(--ease-in), transform var(--dur-fast) var(--ease-in);
|
||||||
}
|
}
|
||||||
|
|
||||||
.toast-enter-from,
|
.toast-enter-from,
|
||||||
.toast-leave-to {
|
.toast-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(8px);
|
transform: translateY(12px) scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
.copy-toast-enter-active,
|
.copy-toast-enter-active,
|
||||||
@@ -132,19 +144,13 @@
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--surface-raised);
|
background: var(--surface-raised);
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
animation: shortcut-menu-in 0.12s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shortcut-menu-in {
|
|
||||||
from { opacity: 0; transform: translateY(-3px); }
|
|
||||||
to { opacity: 1; transform: translateY(0); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-menu header {
|
.shortcut-menu header {
|
||||||
padding: 2px 4px 8px;
|
padding: 2px 4px 8px;
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-menu ul {
|
.shortcut-menu ul {
|
||||||
@@ -163,7 +169,7 @@
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 0 4px;
|
padding: 0 4px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.shortcut-menu kbd {
|
.shortcut-menu kbd {
|
||||||
@@ -171,7 +177,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font: 10px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
|
font: var(--text-2xs)/1.2 var(--font-mono);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,3 +188,20 @@
|
|||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Shared entrance for menus outside the chat page. Menus that open upward
|
||||||
|
grow from their bottom edge. */
|
||||||
|
.collapsed-group-menu {
|
||||||
|
transform-origin: bottom left;
|
||||||
|
animation: popover-in var(--dur-base) var(--ease-out) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trace-select-menu,
|
||||||
|
.shortcut-menu {
|
||||||
|
transform-origin: top center;
|
||||||
|
animation: popover-in var(--dur-base) var(--ease-out) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes popover-in {
|
||||||
|
from { opacity: 0; transform: scale(0.96); }
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-header h1 {
|
.home-header h1 {
|
||||||
font-size: 29px;
|
font-size: var(--text-3xl);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,17 +45,22 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
padding: 0 13px;
|
padding: 0 14px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color var(--dur-fast) var(--ease-standard),
|
||||||
|
border-color var(--dur-fast) var(--ease-standard),
|
||||||
|
color var(--dur-fast) var(--ease-standard),
|
||||||
|
transform var(--dur-press) var(--ease-standard);
|
||||||
}
|
}
|
||||||
|
|
||||||
.primary-button,
|
.primary-button,
|
||||||
.send-button {
|
.send-button {
|
||||||
border: 1px solid var(--accent);
|
border: 1px solid var(--accent-strong);
|
||||||
background: var(--accent);
|
background: var(--accent-strong);
|
||||||
color: var(--accent-contrast);
|
color: var(--accent-contrast);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,14 +79,14 @@
|
|||||||
.compact {
|
.compact {
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-summary {
|
.home-summary {
|
||||||
max-width: 1180px;
|
max-width: 1180px;
|
||||||
margin: 32px auto 12px;
|
margin: 32px auto 12px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-summary strong {
|
.home-summary strong {
|
||||||
@@ -108,7 +113,7 @@
|
|||||||
|
|
||||||
.setup-guide h2 {
|
.setup-guide h2 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 17px;
|
font-size: var(--text-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-guide p {
|
.setup-guide p {
|
||||||
@@ -120,7 +125,7 @@
|
|||||||
|
|
||||||
.setup-guide ol {
|
.setup-guide ol {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -146,7 +151,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.setup-guide li.complete > svg {
|
.setup-guide li.complete > svg {
|
||||||
color: var(--accent);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-guide li strong,
|
.setup-guide li strong,
|
||||||
@@ -156,13 +161,13 @@
|
|||||||
|
|
||||||
.setup-guide li strong {
|
.setup-guide li strong {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.setup-guide li small {
|
.setup-guide li small {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,7 +240,7 @@
|
|||||||
|
|
||||||
.agent-name-row strong {
|
.agent-name-row strong {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: 15px;
|
font-size: var(--text-md);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -243,7 +248,7 @@
|
|||||||
.agent-card-description {
|
.agent-card-description {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
@@ -255,7 +260,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -266,17 +271,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.agent-state.ready {
|
.agent-state.ready {
|
||||||
color: var(--accent);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-state.warning {
|
.agent-state.warning {
|
||||||
color: var(--danger);
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-version {
|
.agent-version {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: var(--text-xs);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -296,7 +301,7 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: color-mix(in srgb, var(--surface-hover) 54%, transparent);
|
background: color-mix(in srgb, var(--surface-hover) 54%, transparent);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: var(--text-2xs);
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@@ -307,7 +312,7 @@
|
|||||||
max-width: min(20ch, 40%);
|
max-width: min(20ch, 40%);
|
||||||
flex: 0 1 20ch;
|
flex: 0 1 20ch;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-card-meta-row {
|
.agent-card-meta-row {
|
||||||
@@ -337,9 +342,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--accent);
|
color: var(--accent-text);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,13 +436,13 @@
|
|||||||
|
|
||||||
.agent-catalog-category-header h2 {
|
.agent-catalog-category-header h2 {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 14px;
|
font-size: var(--text-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-catalog-category-header p,
|
.agent-catalog-category-header p,
|
||||||
.agent-catalog-category-header small {
|
.agent-catalog-category-header small {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.agent-catalog-category-header p {
|
.agent-catalog-category-header p {
|
||||||
@@ -454,7 +459,7 @@
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: color-mix(in srgb, var(--surface) 58%, var(--bg));
|
background: color-mix(in srgb, var(--surface) 58%, var(--bg));
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-agent-card .manager-note,
|
.settings-agent-card .manager-note,
|
||||||
@@ -467,3 +472,42 @@
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Why an installed Agent is unavailable, and what to do about it. */
|
||||||
|
.agent-card-note {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0 16px 10px 77px;
|
||||||
|
color: var(--text-soft);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card-note > span {
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card-note > button {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-xs);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent-text);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card-note > button:hover:not(:disabled) {
|
||||||
|
background: var(--accent-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-card-note[data-tone="warning"] > span {
|
||||||
|
border-left: 2px solid var(--warning);
|
||||||
|
padding-left: 8px;
|
||||||
|
}
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-dashboard-header h1 {
|
.home-dashboard-header h1 {
|
||||||
font-size: 28px;
|
font-size: var(--text-3xl);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,20 +60,131 @@
|
|||||||
gap: 7px;
|
gap: 7px;
|
||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-workspace-state svg {
|
.home-workspace-state svg {
|
||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
color: var(--accent);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-workspace-state.attention,
|
.home-workspace-state.attention,
|
||||||
.home-workspace-state.attention svg {
|
.home-workspace-state.attention svg {
|
||||||
color: var(--danger);
|
color: var(--warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention {
|
||||||
|
width: min(1080px, 100%);
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 4px 12px 10px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: color-mix(in srgb, var(--warning-soft) 70%, var(--workspace-surface));
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-header {
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-header small {
|
||||||
|
min-width: 20px;
|
||||||
|
height: 18px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--warning);
|
||||||
|
color: var(--workspace-surface);
|
||||||
|
font-size: var(--text-2xs);
|
||||||
|
font-weight: 600;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-item {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 52px;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 28px minmax(0, 1fr) auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-soft);
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-item:hover,
|
||||||
|
.home-attention-item:focus-visible {
|
||||||
|
background: color-mix(in srgb, var(--surface-raised) 72%, transparent);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-item > img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-copy strong,
|
||||||
|
.home-attention-copy small {
|
||||||
|
display: block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-copy strong {
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-copy small {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
color: var(--warning);
|
||||||
|
font-size: var(--text-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-attention-action svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-recovery-notice {
|
.home-recovery-notice {
|
||||||
@@ -85,13 +196,13 @@
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 12px 14px;
|
padding: 12px 14px;
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: color-mix(in srgb, var(--danger-soft) 58%, var(--bg));
|
background: color-mix(in srgb, var(--warning-soft) 70%, var(--workspace-surface));
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-recovery-notice > svg {
|
.home-recovery-notice > svg {
|
||||||
width: 19px;
|
width: 19px;
|
||||||
height: 19px;
|
height: 19px;
|
||||||
color: var(--danger);
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-recovery-notice h2,
|
.home-recovery-notice h2,
|
||||||
@@ -101,13 +212,13 @@
|
|||||||
|
|
||||||
.home-recovery-notice h2 {
|
.home-recovery-notice h2 {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 13px;
|
font-size: var(--text-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-recovery-notice p {
|
.home-recovery-notice p {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,13 +256,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.home-panel-header h2 {
|
.home-panel-header h2 {
|
||||||
font-size: 15px;
|
font-size: var(--text-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-panel-header p {
|
.home-panel-header p {
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,14 +336,14 @@
|
|||||||
|
|
||||||
.home-recent-copy strong,
|
.home-recent-copy strong,
|
||||||
.home-agent-item strong {
|
.home-agent-item strong {
|
||||||
font-size: 12px;
|
font-size: var(--text-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-recent-copy small,
|
.home-recent-copy small,
|
||||||
.home-agent-item small {
|
.home-agent-item small {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-recent-item > svg {
|
.home-recent-item > svg {
|
||||||
@@ -283,7 +394,7 @@
|
|||||||
.home-panel-empty p {
|
.home-panel-empty p {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
|
|
||||||
.knowledge-base-ready-summary {
|
.knowledge-base-ready-summary {
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,14 +33,14 @@
|
|||||||
.knowledge-base-group-header h3 {
|
.knowledge-base-group-header h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text-soft);
|
color: var(--text-soft);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
font-weight: 700;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-group-header p {
|
.knowledge-base-group-header p {
|
||||||
margin: 4px 0 0;
|
margin: 4px 0 0;
|
||||||
color: var(--faint);
|
color: var(--faint);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-list,
|
.knowledge-base-list,
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
.knowledge-base-item-copy strong {
|
.knowledge-base-item-copy strong {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 15px;
|
font-size: var(--text-md);
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -117,8 +117,8 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -129,11 +129,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-status.connected {
|
.knowledge-base-status.connected {
|
||||||
color: var(--accent);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-status.warning {
|
.knowledge-base-status.warning {
|
||||||
color: var(--danger);
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-status.checking {
|
.knowledge-base-status.checking {
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
.knowledge-base-card-description {
|
.knowledge-base-card-description {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
@@ -153,7 +153,7 @@
|
|||||||
.knowledge-base-path {
|
.knowledge-base-path {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,21 +173,21 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background: color-mix(in srgb, var(--surface-hover) 54%, transparent);
|
background: color-mix(in srgb, var(--surface-hover) 54%, transparent);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 10px;
|
font-size: var(--text-2xs);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-tag.connected {
|
.knowledge-base-tag.connected {
|
||||||
background: color-mix(in srgb, var(--accent-soft) 62%, transparent);
|
background: var(--success-soft);
|
||||||
color: var(--accent);
|
color: var(--success);
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-tag.warning {
|
.knowledge-base-tag.warning {
|
||||||
background: color-mix(in srgb, var(--danger-soft, var(--surface-hover)) 60%, transparent);
|
background: var(--warning-soft);
|
||||||
color: var(--danger);
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
.knowledge-base-tag.checking {
|
.knowledge-base-tag.checking {
|
||||||
@@ -227,9 +227,9 @@
|
|||||||
border: 0;
|
border: 0;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--accent);
|
color: var(--accent-text);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -252,7 +252,7 @@
|
|||||||
.knowledge-base-action:disabled,
|
.knowledge-base-action:disabled,
|
||||||
.knowledge-base-action:disabled:hover {
|
.knowledge-base-action:disabled:hover {
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
color: var(--accent);
|
color: var(--accent-text);
|
||||||
cursor: wait;
|
cursor: wait;
|
||||||
opacity: 0.72;
|
opacity: 0.72;
|
||||||
}
|
}
|
||||||
@@ -264,5 +264,5 @@
|
|||||||
|
|
||||||
.knowledge-base-action.ghost:hover {
|
.knowledge-base-action.ghost:hover {
|
||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
color: var(--accent);
|
color: var(--accent-text);
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
|
|
||||||
.system-settings-header h1 {
|
.system-settings-header h1 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 28px;
|
font-size: var(--text-2xl);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@
|
|||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-weight: 650;
|
font-weight: 600;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +88,13 @@
|
|||||||
|
|
||||||
.cloud-agent-bridge-panel h2 {
|
.cloud-agent-bridge-panel h2 {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 18px;
|
font-size: var(--text-lg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cloud-agent-bridge-panel .manager-toolbar p {
|
.cloud-agent-bridge-panel .manager-toolbar p {
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cloud-agent-connect-form input {
|
.cloud-agent-connect-form input {
|
||||||
@@ -130,8 +130,8 @@
|
|||||||
|
|
||||||
.cloud-agent-bridge-error {
|
.cloud-agent-bridge-error {
|
||||||
margin: 8px 0;
|
margin: 8px 0;
|
||||||
color: var(--danger, #b34d4d);
|
color: var(--danger);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cloud-agent-bridge-list {
|
.cloud-agent-bridge-list {
|
||||||
@@ -160,7 +160,7 @@
|
|||||||
|
|
||||||
.cloud-agent-bridge-item-main > strong {
|
.cloud-agent-bridge-item-main > strong {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-size: 13px;
|
font-size: var(--text-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cloud-agent-bridge-agent-list {
|
.cloud-agent-bridge-agent-list {
|
||||||
@@ -168,7 +168,7 @@
|
|||||||
margin: -1px 0 0;
|
margin: -1px 0 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--text-soft) !important;
|
color: var(--text-soft) !important;
|
||||||
font-size: 11px !important;
|
font-size: var(--text-xs) !important;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -176,13 +176,109 @@
|
|||||||
.cloud-agent-bridge-item-main > span,
|
.cloud-agent-bridge-item-main > span,
|
||||||
.cloud-agent-bridge-item-main > small {
|
.cloud-agent-bridge-item-main > small {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 11px;
|
font-size: var(--text-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bridge-online { color: var(--success, #4f7564) !important; }
|
.bridge-online { color: var(--success) !important; }
|
||||||
.bridge-offline { color: var(--danger, #b34d4d) !important; }
|
.bridge-offline { color: var(--danger) !important; }
|
||||||
|
|
||||||
.cloud-agent-bridge-empty {
|
.cloud-agent-bridge-empty {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 12px;
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings-panel {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
gap: 0;
|
||||||
|
max-width: 760px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 18px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings-row:first-child {
|
||||||
|
padding-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings-copy {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings-copy h2 {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: var(--text-md);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-settings-copy p {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Shared segmented choice: one selected option, same look in every mode. */
|
||||||
|
.choice-segmented {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 3px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: var(--surface-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-segmented button {
|
||||||
|
min-height: 30px;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: calc(var(--radius) - 3px);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color var(--dur-fast) var(--ease-standard),
|
||||||
|
color var(--dur-fast) var(--ease-standard),
|
||||||
|
box-shadow var(--dur-fast) var(--ease-standard);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-segmented button svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-segmented button:hover:not(.active) {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.choice-segmented button.active {
|
||||||
|
background: var(--surface-raised);
|
||||||
|
box-shadow: var(--elev-1);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-agent-bridge-confirm {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: var(--text-soft);
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cloud-agent-bridge-confirm span {
|
||||||
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
@@ -21,6 +21,7 @@ export function createWorkspaceControllers({
|
|||||||
createDirectSession: conversationActions.createDirectSession,
|
createDirectSession: conversationActions.createDirectSession,
|
||||||
deleteSidebarConversation: conversationActions.deleteSidebarConversation,
|
deleteSidebarConversation: conversationActions.deleteSidebarConversation,
|
||||||
directGroupsFor: conversationNavigation.directGroupsFor,
|
directGroupsFor: conversationNavigation.directGroupsFor,
|
||||||
|
discoveringAgents: app.discoveringAgents,
|
||||||
dismissSidebarDeleteConfirmation: conversationActions.dismissSidebarDeleteConfirmation,
|
dismissSidebarDeleteConfirmation: conversationActions.dismissSidebarDeleteConfirmation,
|
||||||
formatNavTime: conversationNavigation.formatNavTime,
|
formatNavTime: conversationNavigation.formatNavTime,
|
||||||
goHome: app.goHome,
|
goHome: app.goHome,
|
||||||
@@ -34,6 +35,7 @@ export function createWorkspaceControllers({
|
|||||||
hasMoreGroupGroups: conversationNavigation.hasMoreGroupGroups,
|
hasMoreGroupGroups: conversationNavigation.hasMoreGroupGroups,
|
||||||
isDirectCreationPending: conversationNavigation.isDirectCreationPending,
|
isDirectCreationPending: conversationNavigation.isDirectCreationPending,
|
||||||
isDirectSessionListExpanded: conversationNavigation.isDirectSessionListExpanded,
|
isDirectSessionListExpanded: conversationNavigation.isDirectSessionListExpanded,
|
||||||
|
isGroupAwaitingDecision: app.isGroupAwaitingDecision,
|
||||||
isGroupRunning: app.isGroupRunning,
|
isGroupRunning: app.isGroupRunning,
|
||||||
isSidebarAgentExpanded: conversationNavigation.isSidebarAgentExpanded,
|
isSidebarAgentExpanded: conversationNavigation.isSidebarAgentExpanded,
|
||||||
openConversationRename: conversationActions.openConversationRename,
|
openConversationRename: conversationActions.openConversationRename,
|
||||||
@@ -67,6 +69,7 @@ export function createWorkspaceControllers({
|
|||||||
agentImageLabel: agentCatalog.agentImageLabel,
|
agentImageLabel: agentCatalog.agentImageLabel,
|
||||||
agentSkillLabel: agentSkills.agentSkillLabel,
|
agentSkillLabel: agentSkills.agentSkillLabel,
|
||||||
agentState: agentCatalog.agentState,
|
agentState: agentCatalog.agentState,
|
||||||
|
agentStateNote: agentCatalog.agentStateNote,
|
||||||
cancelInstall: agentManagement.cancelInstall,
|
cancelInstall: agentManagement.cancelInstall,
|
||||||
cloudAgentBridges: app.cloudAgentBridges,
|
cloudAgentBridges: app.cloudAgentBridges,
|
||||||
focusedAgentKind: app.focusedAgentKind,
|
focusedAgentKind: app.focusedAgentKind,
|
||||||
@@ -96,8 +99,10 @@ export function createWorkspaceControllers({
|
|||||||
requestInstall: agentManagement.requestInstall,
|
requestInstall: agentManagement.requestInstall,
|
||||||
runKnowledgeBasePrimaryAction: knowledgeBase.runKnowledgeBasePrimaryAction,
|
runKnowledgeBasePrimaryAction: knowledgeBase.runKnowledgeBasePrimaryAction,
|
||||||
selectSystemSettingsSection: app.selectSystemSettingsSection,
|
selectSystemSettingsSection: app.selectSystemSettingsSection,
|
||||||
|
setThemePreference: app.setThemePreference,
|
||||||
systemSettingsSection: app.systemSettingsSection,
|
systemSettingsSection: app.systemSettingsSection,
|
||||||
t: app.t,
|
t: app.t,
|
||||||
|
themePreference: app.themePreference,
|
||||||
}
|
}
|
||||||
|
|
||||||
const workspaceModalController = {
|
const workspaceModalController = {
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ export default defineConfig(() => ({
|
|||||||
output: {
|
output: {
|
||||||
codeSplitting: {
|
codeSplitting: {
|
||||||
groups: [
|
groups: [
|
||||||
{ name: 'pixel-blast', test: /node_modules[\\/]three[\\/]/ },
|
|
||||||
{ name: 'vendor', test: /node_modules[\\/]/ },
|
{ name: 'vendor', test: /node_modules[\\/]/ },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in new issue
Block a user