fix: resolve streaming messages splitting into individual bubbles

Simplify addMessage/updateMessage to only write to messages.value,
add syncMessagesToSession() to copy messages back on session switch
and stream completion. Also fix mobile viewport, session list overlay,
hamburger logo, and various responsive improvements.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-15 10:28:53 +08:00
parent f3927e2990
commit 9eaaa4270d
16 changed files with 179 additions and 42 deletions
+1 -1
View File
@@ -31,7 +31,7 @@ onMounted(() => {
@use '@/styles/variables' as *;
.channels-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
+1 -1
View File
@@ -21,7 +21,7 @@ onMounted(() => {
<style scoped lang="scss">
.chat-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
+1 -1
View File
@@ -67,7 +67,7 @@ async function handleSave() {
@use '@/styles/variables' as *;
.jobs-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
+1 -1
View File
@@ -80,7 +80,7 @@ async function handleLogin() {
@use "@/styles/variables" as *;
.login-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
align-items: center;
justify-content: center;
+1 -1
View File
@@ -153,7 +153,7 @@ onMounted(async () => {
@use '@/styles/variables' as *;
.logs-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
+1 -1
View File
@@ -188,7 +188,7 @@ const displayUser = computed(() => (data.value?.user || '').replace(/§/g, '\n\n
@use '@/styles/variables' as *;
.memory-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
+1 -1
View File
@@ -58,7 +58,7 @@ async function handleSaved() {
@use '@/styles/variables' as *;
.models-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
+1 -1
View File
@@ -102,7 +102,7 @@ async function saveApiServer(values: Record<string, any>) {
@use '@/styles/variables' as *;
.settings-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
+23 -6
View File
@@ -44,6 +44,9 @@ async function loadSkills() {
function handleSelect(category: string, skill: string) {
selectedCategory.value = category
selectedSkill.value = skill
if (window.innerWidth <= 768) {
showSidebar.value = false
}
}
</script>
@@ -61,13 +64,14 @@ function handleSelect(category: string, skill: string) {
:placeholder="t('skills.searchPlaceholder')"
size="small"
clearable
class="search-input"
style="width: 160px"
/>
</header>
<div class="skills-content">
<div v-if="loading && categories.length === 0" class="skills-loading">Loading...</div>
<div v-else class="skills-layout">
<div class="mobile-backdrop" :class="{ active: showSidebar }" @click="showSidebar = false" />
<div v-if="showSidebar" class="skills-sidebar">
<SkillList
:categories="categories"
@@ -100,13 +104,13 @@ function handleSelect(category: string, skill: string) {
@use '@/styles/variables' as *;
.skills-view {
height: 100vh;
height: calc(100 * var(--vh));
display: flex;
flex-direction: column;
}
.search-input {
width: 220px;
width: 100px;
@media (max-width: $breakpoint-mobile) {
width: 100%;
@@ -181,9 +185,22 @@ function handleSelect(category: string, skill: string) {
.skills-layout {
position: relative;
}
}
min-width: 0;
min-height: 0;
.mobile-backdrop {
display: block;
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 9;
opacity: 0;
pointer-events: none;
transition: opacity $transition-fast;
&.active {
opacity: 1;
pointer-events: auto;
}
}
}
.empty-detail {