Newer
Older
navi-1 / webclient / src / stores / chat / state.js
import { ref, shallowRef } from 'vue'

/**
 * State of the chat store: the refs, maps and sets the area modules share.
 * Created once per store instance.
 *
 * `toolIndex` and the two underscore sets are deliberately non-reactive. The
 * index must hold the array's reactive proxy (a write to a raw copy never
 * wakes a computed that read the card through the proxy), and a late event
 * arriving after the turn closed must still find its card and its task.
 */
export function createChatState() {
  return {
    currentId: ref(null),
    currentProfileId: ref(null),
    messages: ref([]),
    streaming: ref(false),
    pendingImages: ref([]),   // base64 strings
    pendingFiles: ref([]),    // { name, path, size, content_type }
    artifacts: ref([]),       // published session content
    files: ref([]),           // session directory tree
    contextTokens: ref(0),
    maxContextTokens: ref(0),
    // The model (and llm.json target) that answered the current turn, from the
    // model_info event. `servedFallback` is true when it is NOT the route's
    // primary — the one case the UI has to say out loud: a backup host is
    // carrying the conversation, so answers differ in style and window.
    servedModel: ref(null),
    servedTarget: ref(null),
    servedFallback: ref(false),
    // WebSocket liveness, mirrored from useWebSocket so the session-info dialog
    // can show it without reaching into ChatArea's composable instance.
    wsConnected: ref(false),
    wsReconnecting: ref(false),
    loading: ref(false),
    // Why the last session load failed (translated, ready to show), and the
    // session it was for — the retry button reopens exactly that one. A failed
    // load keeps the previous transcript in place instead of blanking the page.
    loadError: ref(null),
    loadErrorSessionId: ref(null),
    // Images currently being read/resized (FileReader + canvas) — no preview yet
    imagesProcessing: ref(0),
    // Message sent over WS but the run hasn't started (stream_start not yet seen)
    awaitingStream: ref(false),

    // The in-progress streaming message (direct ref for perf)
    streamingMsg: shallowRef(null),
    // True while replaying buffered events from a reconnect — suppresses animations
    replayMode: ref(false),
    // Index of the message to scroll to after loading a session from search results
    scrollToMessageIndex: ref(null),
    // Current scheduled recall for the active session (null if none)
    recall: ref(null),
    // Persistent terminal output keyed by terminal name
    terminals: ref({}),
    // Live background tasks keyed by task_id (from out-of-band task_update events)
    backgroundTasks: ref({}),
    // tool_call_id → tool card. Non-reactive index for id-first matching —
    // works even for events that arrive after stream_end closed the turn.
    toolIndex: new Map(),
    // Images Navi itself looked at (image_view and friends): captured live from
    // tool_call metadata and from history. Rendered as a group in the artifacts
    // panel, not as chat bubbles.
    viewedImages: ref([]),
    _viewedImageKeys: new Set(),

    // Task ids whose terminal task_update (completed/failed/cancelled) was seen
    // — a late duplicate 'running' must not resurrect the chip (out-of-order
    // delivery would otherwise leave a "ghost" task running forever).
    _terminalTaskIds: new Set(),

    // Archive (older messages) pagination state
    archiveLoading: ref(false),
    archiveHasMore: ref(false),
    archiveNextBefore: ref(null),

    // Message count from the last full history load — the cheap fingerprint that
    // tells whether another device advanced the session. Compared against
    // GET /sessions/{id}/meta when the tab comes back to the foreground.
    historyCount: ref(0),
    // The user scrolled into older pages: a silent reload would throw them away
    // (and jump the scroll position), so automatic catch-up stays off for the
    // rest of this session view.
    archiveExtended: ref(false),
  }
}