diff --git a/webclient/src/App.vue b/webclient/src/App.vue
index 164f304..6320732 100644
--- a/webclient/src/App.vue
+++ b/webclient/src/App.vue
@@ -109,8 +109,13 @@
const documentTitle = computed(() => {
if (route.value === 'settings') return 'Navi — Settings'
if (!chatStore.currentId) return 'Navi'
- const session = sessionsStore.sessions.find(s => s.session_id === chatStore.currentId)
- return session?.name || chatStore.currentId.slice(0, 8)
+ const id = chatStore.currentId
+ // nameById persists beyond the loaded list page (the list holds only ~30)
+ return (
+ sessionsStore.nameById[id] ||
+ sessionsStore.sessions.find(s => s.session_id === id)?.name ||
+ id.slice(0, 8)
+ )
})
watch(documentTitle, (title) => {
@@ -203,7 +208,7 @@
background: rgba(0, 0, 0, 0.6);
z-index: 99;
- @media (max-width: 1280px) {
+ @media (max-width: 1279.98px) {
display: block;
}
}
diff --git a/webclient/src/components/settings/SettingsView.vue b/webclient/src/components/settings/SettingsView.vue
index d30ad7e..b6eb541 100644
--- a/webclient/src/components/settings/SettingsView.vue
+++ b/webclient/src/components/settings/SettingsView.vue
@@ -42,7 +42,7 @@
.settings-sidebar-toggle {
display: none;
- @media (max-width: 1280px) {
+ @media (max-width: 1279.98px) {
display: flex;
}
}
diff --git a/webclient/src/components/sidebar/AppSidebar.vue b/webclient/src/components/sidebar/AppSidebar.vue
index aaa6152..4a3ea5d 100644
--- a/webclient/src/components/sidebar/AppSidebar.vue
+++ b/webclient/src/components/sidebar/AppSidebar.vue
@@ -282,7 +282,7 @@
.sidebar-close-btn {
display: none;
- @media (max-width: 1280px) {
+ @media (max-width: 1279.98px) {
display: flex;
margin-left: auto;
}
diff --git a/webclient/src/components/sidebar/SessionItem.vue b/webclient/src/components/sidebar/SessionItem.vue
index cd7866f..e1d2f20 100644
--- a/webclient/src/components/sidebar/SessionItem.vue
+++ b/webclient/src/components/sidebar/SessionItem.vue
@@ -4,7 +4,7 @@
:class="{ 'is-active': active, 'is-pinned': session.pinned, 'has-recall': session.has_pending_recall }"
@click="emit('select')"
>
-
+
diff --git a/webclient/src/components/ui/WelcomeScreen.vue b/webclient/src/components/ui/WelcomeScreen.vue
index 4308da6..207735c 100644
--- a/webclient/src/components/ui/WelcomeScreen.vue
+++ b/webclient/src/components/ui/WelcomeScreen.vue
@@ -85,7 +85,7 @@
top: 12px;
left: 12px;
- @media (max-width: 1280px) {
+ @media (max-width: 1279.98px) {
display: flex;
}
}
diff --git a/webclient/src/stores/chat.js b/webclient/src/stores/chat.js
index 5a7877f..a23b88b 100644
--- a/webclient/src/stores/chat.js
+++ b/webclient/src/stores/chat.js
@@ -19,6 +19,7 @@
}
export const useChatStore = defineStore('chat', () => {
+ const sessionsStore = useSessionsStore()
const currentId = ref(null)
const currentProfileId = ref(null)
const messages = ref([])
@@ -86,6 +87,9 @@
// Ignore stale responses — user may have switched to a different session while this loaded
if (_lastLoadId !== id) return
currentProfileId.value = session.profile_id ?? null
+ // The paged session list may not include this session — keep its name
+ // in the persistent map for document.title lookups
+ sessionsStore.rememberName(id, session.name)
messages.value = buildMessageList(page.items ?? [])
// Set archive state from the page itself — hot messages below the tail
// are NOT archived yet, so archive_threshold alone can't tell.
diff --git a/webclient/src/stores/sessions.js b/webclient/src/stores/sessions.js
index b7ec085..2073007 100644
--- a/webclient/src/stores/sessions.js
+++ b/webclient/src/stores/sessions.js
@@ -14,6 +14,20 @@
const searchActive = ref(false)
const searchCache = ref(null)
const hasPendingRecallFilter = ref(false)
+ // Persistent session_id → name map. The paged list only holds ~30 loaded
+ // items, so App's document.title lookup needs a map that survives paging;
+ // it is filled by fetchSessions/fetchMoreSessions/loadSession and never
+ // shrinks on refetch.
+ const nameById = ref({})
+
+ function rememberName(id, name) {
+ if (id == null || !name) return
+ if (nameById.value[id] !== name) nameById.value[id] = name
+ }
+
+ function rememberNames(items) {
+ for (const s of items) rememberName(s.session_id, s.name)
+ }
async function fetchSessions(profileId = null) {
currentProfileId.value = profileId
@@ -38,6 +52,7 @@
}
return it
})
+ rememberNames(sessions.value)
hasMore.value = Array.isArray(page) ? false : page.has_more
nextOffset.value = Array.isArray(page) ? items.length : page.next_offset
if (searchActive.value) {
@@ -69,6 +84,7 @@
...sessions.value,
...items.filter(s => !known.has(s.session_id)),
]
+ rememberNames(sessions.value)
hasMore.value = Array.isArray(page) ? false : page.has_more
nextOffset.value = Array.isArray(page) ? sessions.value.length : page.next_offset
if (searchActive.value) {
@@ -113,8 +129,13 @@
async function createSession(profileId) {
const session = await api.createSession(profileId)
- // Add a complete placeholder so SessionItem renders correctly immediately
- sessions.value.unshift({
+ // Add a complete placeholder so SessionItem renders correctly immediately.
+ // Insert at the position the server list would give it (pinned first, then
+ // last_active desc — a brand-new session is the newest unpinned one), so a
+ // refetch doesn't visibly move it.
+ const firstUnpinned = sessions.value.findIndex(s => !s.pinned)
+ const at = firstUnpinned === -1 ? sessions.value.length : firstUnpinned
+ sessions.value.splice(at, 0, {
session_id: session.session_id,
profile_id: session.profile_id,
created_at: session.created_at,
@@ -154,6 +175,7 @@
}
function updateName(id, name) {
+ rememberName(id, name)
const session = sessions.value.find(s => s.session_id === id)
if (session) session.name = name
}
@@ -164,6 +186,7 @@
})
return {
+ nameById,
sessions,
filteredSessions,
loading,
@@ -175,6 +198,7 @@
hasPendingRecallFilter,
fetchSessions,
fetchMoreSessions,
+ rememberName,
createSession,
deleteSession,
pinSession,
diff --git a/webclient/src/styles/app.scss b/webclient/src/styles/app.scss
index 80ff462..31ffda4 100644
--- a/webclient/src/styles/app.scss
+++ b/webclient/src/styles/app.scss
@@ -2,7 +2,9 @@
// ─── Breakpoints ───────────────────────────────────────────────────────────
$bp-mobile: 768px;
-$bp-sidebar-drawer: 1280px;
+// Drawer mode starts strictly *below* 1280px: use an .02px-exclusive bound
+// so a viewport of exactly 1280px gets the desktop sidebar, not the drawer.
+$bp-sidebar-drawer: 1279.98px;
// ─── App shell ─────────────────────────────────────────────────────────────
$sidebar-width: 380px;
diff --git a/webclient/tests/unit/stores/chat.test.js b/webclient/tests/unit/stores/chat.test.js
index 028c555..98262c3 100644
--- a/webclient/tests/unit/stores/chat.test.js
+++ b/webclient/tests/unit/stores/chat.test.js
@@ -6,6 +6,7 @@
useSessionsStore: () => ({
updatePreview: vi.fn(),
updateName: vi.fn(),
+ rememberName: vi.fn(),
sessions: [],
}),
}))
diff --git a/webclient/tests/unit/stores/sessions.test.js b/webclient/tests/unit/stores/sessions.test.js
index 107bed7..ba734f9 100644
--- a/webclient/tests/unit/stores/sessions.test.js
+++ b/webclient/tests/unit/stores/sessions.test.js
@@ -173,4 +173,44 @@
expect(store.sessions[0]).toBe(ref2)
expect(store.sessions[0].pinned).toBe(true)
})
+
+ it('createSession places the placeholder where the server list would (below pinned)', async () => {
+ api.createSession.mockResolvedValue({
+ session_id: 'new1',
+ profile_id: 'developer',
+ created_at: '2024-01-02T00:00:00Z',
+ })
+ const store = useSessionsStore()
+ store.sessions = [{ session_id: 's1', pinned: true }]
+ await store.createSession('developer')
+ // placeholder lands after the pinned run, matching server's
+ // pinned DESC, last_active DESC ordering
+ expect(store.sessions.map(s => s.session_id)).toEqual(['s1', 'new1'])
+ })
+
+ it('rememberName persists names beyond the loaded page; fetchSessions fills the map', async () => {
+ api.getSessions.mockResolvedValueOnce({
+ items: [{ session_id: 's1', name: 'Work', preview: '', pinned: false }],
+ has_more: false,
+ next_offset: 1,
+ })
+ const store = useSessionsStore()
+ await store.fetchSessions('developer')
+ expect(store.nameById['s1']).toBe('Work')
+
+ store.rememberName('s2', 'Deep session')
+ expect(store.nameById['s2']).toBe('Deep session')
+
+ api.getSessions.mockResolvedValueOnce({
+ items: [{ session_id: 's3', name: null, preview: '', pinned: false }],
+ has_more: false,
+ next_offset: 1,
+ })
+ await store.fetchSessions('developer')
+ // null name must not wipe remembered names
+ expect(store.nameById['s2']).toBe('Deep session')
+
+ store.updateName('s3', 'Named now')
+ expect(store.nameById['s3']).toBe('Named now')
+ })
})