Per-conv streaming state, immediate message_count commit, auto-new-chat on open

stores/chat.ts:
- Replace 7 global stream refs with convStreams: Record<number, ConvStreamState>
- Each conversation tracks its own stream state; 7 names become computed getters
- Add isStreamingConv(id) public helper
- Increment message_count +2 immediately after POST 202 (not at done) so the
  cleanup watcher never deletes a mid-generation conversation
- SSE reconnect retries now run for background conversations regardless of
  which conv is currently viewed
- error toasts only shown when the erroring conv is currently viewed
- deleteConversation cleans up orphaned stream state

ChatView.vue:
- Remove invalid write to store.lastContextMeta (now a read-only computed)
- Add !store.isStreamingConv(newId) guard to watch(convId) fetch so navigating
  back to a generating conversation shows accumulated content without stale refetch
- Add startNewConversation() helper; opening /chat with no ID auto-creates a
  new conversation and redirects to it (on mount and on navigation)
- Deleting a conversation also auto-creates a new one

Also committing previous-session changes (keyboard nav + task routing):
- App.vue: e shortcut only on note-view (not task-view, which is removed)
- TaskCard.vue: route directly to /tasks/:id (viewer), remove View buttons
- router/index.ts: remove task-view route; /tasks/:id now maps to TaskEditorView
- TasksListView.vue: Enter key pushes to /tasks/:id

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 20:32:56 -05:00
parent 404d58d037
commit dc39a56293
7 changed files with 125 additions and 117 deletions
+16 -9
View File
@@ -85,8 +85,12 @@ const inputPlaceholder = computed(() => {
onMounted(async () => {
document.addEventListener("keydown", onGlobalKeydown);
await store.fetchConversations();
if (convId.value && store.currentConversation?.id !== convId.value) {
await store.fetchConversation(convId.value);
if (convId.value) {
if (store.currentConversation?.id !== convId.value) {
await store.fetchConversation(convId.value);
}
} else {
await startNewConversation();
}
nextTick(() => inputEl.value?.focus());
});
@@ -106,16 +110,15 @@ watch(convId, async (newId) => {
suggestedNotes.value = [];
autoInjectedNotes.value = [];
excludedNoteIds.value = [];
store.lastContextMeta = null;
if (newId) {
// Skip re-fetch if this conversation is already loaded (avoids race
// condition where a stale GET overwrites messages during streaming)
if (store.currentConversation?.id !== newId) {
// Skip re-fetch if this conversation is already loaded or still streaming
// (avoids a stale GET overwriting in-progress messages)
if (store.currentConversation?.id !== newId && !store.isStreamingConv(newId)) {
await store.fetchConversation(newId);
}
scrollToBottom();
} else {
store.currentConversation = null;
await startNewConversation();
}
nextTick(() => inputEl.value?.focus());
});
@@ -177,10 +180,14 @@ async function selectConversation(id: number) {
router.push(`/chat/${id}`);
}
async function newConversation() {
async function startNewConversation() {
const conv = await store.createConversation();
await store.fetchConversation(conv.id);
router.push(`/chat/${conv.id}`);
// watch(convId) will fire and fetch the conversation
}
async function newConversation() {
await startNewConversation();
}
async function removeConversation(id: number) {
+1 -1
View File
@@ -39,7 +39,7 @@ function onKeydown(e: KeyboardEvent) {
document.querySelector(".kb-active-item")?.scrollIntoView({ block: "nearest" });
} else if (e.key === "Enter" && activeIndex.value >= 0) {
const task = store.tasks[activeIndex.value];
if (task) router.push(`/tasks/${task.id}/edit`);
if (task) router.push(`/tasks/${task.id}`);
}
}