feat(ui): session nav follows visible list
Cmd+Shift+[ and Cmd+Shift+] now cycle through visible sessions only (parents + expanded children) and no longer include Instance Info. Sidebar session list auto-scrolls to keep the active session row in view.
This commit is contained in:
@@ -869,12 +869,6 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
threads={sessionThreads()}
|
threads={sessionThreads()}
|
||||||
activeSessionId={activeSessionIdForInstance()}
|
activeSessionId={activeSessionIdForInstance()}
|
||||||
onSelect={handleSessionSelect}
|
onSelect={handleSessionSelect}
|
||||||
onClose={(id) => {
|
|
||||||
const result = props.onCloseSession(id)
|
|
||||||
if (result instanceof Promise) {
|
|
||||||
void result.catch((error) => log.error("Failed to close session:", error))
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onNew={() => {
|
onNew={() => {
|
||||||
const result = props.onNewSession()
|
const result = props.onNewSession()
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
|
|||||||
@@ -1,15 +1,22 @@
|
|||||||
import { Component, For, Show, createSignal, createMemo, createEffect, JSX } from "solid-js"
|
import { Component, For, Show, createSignal, createMemo, createEffect, JSX, onCleanup } from "solid-js"
|
||||||
import type { Session, SessionStatus } from "../types/session"
|
import type { Session, SessionStatus } from "../types/session"
|
||||||
import type { SessionThread } from "../stores/session-state"
|
import type { SessionThread } from "../stores/session-state"
|
||||||
import { getSessionStatus } from "../stores/session-status"
|
import { getSessionStatus } from "../stores/session-status"
|
||||||
import { Bot, User, Info, X, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid"
|
import { Bot, User, Info, Copy, Trash2, Pencil, ShieldAlert, ChevronDown } from "lucide-solid"
|
||||||
import KeyboardHint from "./keyboard-hint"
|
import KeyboardHint from "./keyboard-hint"
|
||||||
import Kbd from "./kbd"
|
import Kbd from "./kbd"
|
||||||
import SessionRenameDialog from "./session-rename-dialog"
|
import SessionRenameDialog from "./session-rename-dialog"
|
||||||
import { keyboardRegistry } from "../lib/keyboard-registry"
|
import { keyboardRegistry } from "../lib/keyboard-registry"
|
||||||
import { formatShortcut } from "../lib/keyboard-utils"
|
import { formatShortcut } from "../lib/keyboard-utils"
|
||||||
import { showToastNotification } from "../lib/notifications"
|
import { showToastNotification } from "../lib/notifications"
|
||||||
import { deleteSession, loading, renameSession } from "../stores/sessions"
|
import {
|
||||||
|
deleteSession,
|
||||||
|
ensureSessionParentExpanded,
|
||||||
|
isSessionParentExpanded,
|
||||||
|
loading,
|
||||||
|
renameSession,
|
||||||
|
toggleSessionParentExpanded,
|
||||||
|
} from "../stores/sessions"
|
||||||
import { getLogger } from "../lib/logger"
|
import { getLogger } from "../lib/logger"
|
||||||
import { copyToClipboard } from "../lib/clipboard"
|
import { copyToClipboard } from "../lib/clipboard"
|
||||||
const log = getLogger("session")
|
const log = getLogger("session")
|
||||||
@@ -22,7 +29,6 @@ interface SessionListProps {
|
|||||||
threads: SessionThread[]
|
threads: SessionThread[]
|
||||||
activeSessionId: string | null
|
activeSessionId: string | null
|
||||||
onSelect: (sessionId: string) => void
|
onSelect: (sessionId: string) => void
|
||||||
onClose: (sessionId: string) => void
|
|
||||||
onNew: () => void
|
onNew: () => void
|
||||||
showHeader?: boolean
|
showHeader?: boolean
|
||||||
showFooter?: boolean
|
showFooter?: boolean
|
||||||
@@ -41,24 +47,6 @@ function formatSessionStatus(status: SessionStatus): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function arraysEqual(prev: readonly string[] | undefined, next: readonly string[]): boolean {
|
|
||||||
if (!prev) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev.length !== next.length) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < prev.length; i++) {
|
|
||||||
if (prev[i] !== next[i]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const SessionList: Component<SessionListProps> = (props) => {
|
const SessionList: Component<SessionListProps> = (props) => {
|
||||||
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
|
const [renameTarget, setRenameTarget] = createSignal<{ id: string; title: string; label: string } | null>(null)
|
||||||
const [isRenaming, setIsRenaming] = createSignal(false)
|
const [isRenaming, setIsRenaming] = createSignal(false)
|
||||||
@@ -69,35 +57,13 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
return deleting ? deleting.has(sessionId) : false
|
return deleting ? deleting.has(sessionId) : false
|
||||||
}
|
}
|
||||||
|
|
||||||
const [expandedParents, setExpandedParents] = createSignal<Set<string>>(new Set())
|
|
||||||
|
|
||||||
const toggleParentExpanded = (parentId: string) => {
|
|
||||||
setExpandedParents((prev) => {
|
|
||||||
const next = new Set(prev)
|
|
||||||
if (next.has(parentId)) {
|
|
||||||
next.delete(parentId)
|
|
||||||
} else {
|
|
||||||
next.add(parentId)
|
|
||||||
}
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureParentExpanded = (parentId: string) => {
|
|
||||||
setExpandedParents((prev) => {
|
|
||||||
if (prev.has(parentId)) return prev
|
|
||||||
const next = new Set(prev)
|
|
||||||
next.add(parentId)
|
|
||||||
return next
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectSession = (sessionId: string) => {
|
const selectSession = (sessionId: string) => {
|
||||||
if (sessionId !== "info") {
|
if (sessionId !== "info") {
|
||||||
const session = props.sessions.get(sessionId)
|
const session = props.sessions.get(sessionId)
|
||||||
const parentId = session?.parentId ?? session?.id
|
const parentId = session?.parentId ?? session?.id
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
ensureParentExpanded(parentId)
|
ensureSessionParentExpanded(props.instanceId, parentId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,7 +128,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
|
|
||||||
const SessionRow: Component<{
|
const SessionRow: Component<{
|
||||||
sessionId: string
|
sessionId: string
|
||||||
canClose?: boolean
|
|
||||||
isChild?: boolean
|
isChild?: boolean
|
||||||
isLastChild?: boolean
|
isLastChild?: boolean
|
||||||
hasChildren?: boolean
|
hasChildren?: boolean
|
||||||
@@ -186,6 +151,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
class={`session-item-base ${rowProps.isChild ? `session-item-child${rowProps.isLastChild ? " session-item-child-last" : ""} session-item-border-assistant session-item-kind-assistant` : "session-item-border-user session-item-kind-user"} ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
class={`session-item-base ${rowProps.isChild ? `session-item-child${rowProps.isLastChild ? " session-item-child-last" : ""} session-item-border-assistant session-item-kind-assistant` : "session-item-border-user session-item-kind-user"} ${isActive() ? "session-item-active" : "session-item-inactive"}`}
|
||||||
|
data-session-id={rowProps.sessionId}
|
||||||
onClick={() => selectSession(rowProps.sessionId)}
|
onClick={() => selectSession(rowProps.sessionId)}
|
||||||
title={title()}
|
title={title()}
|
||||||
role="button"
|
role="button"
|
||||||
@@ -201,20 +167,6 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
)}
|
)}
|
||||||
<span class="session-item-title session-item-title--clamp">{title()}</span>
|
<span class="session-item-title session-item-title--clamp">{title()}</span>
|
||||||
</div>
|
</div>
|
||||||
<Show when={rowProps.canClose}>
|
|
||||||
<span
|
|
||||||
class="session-item-close opacity-80 hover:opacity-100 hover:bg-status-error hover:text-white rounded p-0.5 transition-all"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation()
|
|
||||||
props.onClose(rowProps.sessionId)
|
|
||||||
}}
|
|
||||||
role="button"
|
|
||||||
tabIndex={0}
|
|
||||||
aria-label="Close session"
|
|
||||||
>
|
|
||||||
<X class="w-3 h-3" />
|
|
||||||
</span>
|
|
||||||
</Show>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="session-item-row session-item-meta">
|
<div class="session-item-row session-item-meta">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
@@ -315,9 +267,56 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const parentId = activeParentId()
|
const parentId = activeParentId()
|
||||||
if (!parentId) return
|
if (!parentId) return
|
||||||
ensureParentExpanded(parentId)
|
ensureSessionParentExpanded(props.instanceId, parentId)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const listEl = createSignal<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
const escapeCss = (value: string) => {
|
||||||
|
if (typeof CSS !== "undefined" && typeof (CSS as any).escape === "function") {
|
||||||
|
return (CSS as any).escape(value)
|
||||||
|
}
|
||||||
|
return value.replace(/\\/g, "\\\\").replace(/\"/g, "\\\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrollActiveIntoView = (sessionId: string) => {
|
||||||
|
const root = listEl[0]()
|
||||||
|
if (!root) return
|
||||||
|
|
||||||
|
const selector = `[data-session-id="${escapeCss(sessionId)}"]`
|
||||||
|
|
||||||
|
const scrollNow = () => {
|
||||||
|
const target = root.querySelector(selector) as HTMLElement | null
|
||||||
|
if (!target) return
|
||||||
|
target.scrollIntoView({ block: "nearest", inline: "nearest" })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof requestAnimationFrame === "undefined") {
|
||||||
|
scrollNow()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait a couple frames so expand/collapse DOM settles.
|
||||||
|
let raf1 = 0
|
||||||
|
let raf2 = 0
|
||||||
|
raf1 = requestAnimationFrame(() => {
|
||||||
|
raf2 = requestAnimationFrame(() => {
|
||||||
|
scrollNow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
onCleanup(() => {
|
||||||
|
if (raf1) cancelAnimationFrame(raf1)
|
||||||
|
if (raf2) cancelAnimationFrame(raf2)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
const activeId = props.activeSessionId
|
||||||
|
if (!activeId) return
|
||||||
|
scrollActiveIntoView(activeId)
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full"
|
class="session-list-container bg-surface-secondary border-r border-base flex flex-col w-full"
|
||||||
@@ -335,7 +334,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="session-list flex-1 overflow-y-auto">
|
<div class="session-list flex-1 overflow-y-auto" ref={(el) => listEl[1](el)}>
|
||||||
<div class="session-section">
|
<div class="session-section">
|
||||||
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
<div class="session-section-header px-3 py-2 text-xs font-semibold text-primary/70 uppercase tracking-wide">
|
||||||
Instance
|
Instance
|
||||||
@@ -343,6 +342,7 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="session-list-item group">
|
<div class="session-list-item group">
|
||||||
<button
|
<button
|
||||||
class={`session-item-base ${props.activeSessionId === "info" ? "session-item-active" : "session-item-inactive"}`}
|
class={`session-item-base ${props.activeSessionId === "info" ? "session-item-active" : "session-item-inactive"}`}
|
||||||
|
data-session-id="info"
|
||||||
onClick={() => selectSession("info")}
|
onClick={() => selectSession("info")}
|
||||||
title="Instance Info"
|
title="Instance Info"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -367,16 +367,16 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
<For each={props.threads}>
|
<For each={props.threads}>
|
||||||
{(thread) => {
|
{(thread) => {
|
||||||
const expanded = () => expandedParents().has(thread.parent.id)
|
const expanded = () => isSessionParentExpanded(props.instanceId, thread.parent.id)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SessionRow
|
<SessionRow
|
||||||
sessionId={thread.parent.id}
|
sessionId={thread.parent.id}
|
||||||
canClose
|
hasChildren={thread.children.length > 0}
|
||||||
hasChildren={thread.children.length > 0}
|
expanded={expanded()}
|
||||||
expanded={expanded()}
|
onToggleExpand={() => toggleSessionParentExpanded(props.instanceId, thread.parent.id)}
|
||||||
onToggleExpand={() => toggleParentExpanded(thread.parent.id)}
|
/>
|
||||||
/>
|
|
||||||
<Show when={expanded() && thread.children.length > 0}>
|
<Show when={expanded() && thread.children.length > 0}>
|
||||||
<For each={thread.children}>
|
<For each={thread.children}>
|
||||||
{(child, index) => (
|
{(child, index) => (
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ import type { Preferences, ExpansionPreference } from "../../stores/preferences"
|
|||||||
import { createCommandRegistry, type Command } from "../commands"
|
import { createCommandRegistry, type Command } from "../commands"
|
||||||
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
|
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
|
||||||
import type { ClientPart, MessageInfo } from "../../types/message"
|
import type { ClientPart, MessageInfo } from "../../types/message"
|
||||||
import {
|
import { getSessions, getVisibleSessionIds, setActiveSession, setActiveSessionFromList } from "../../stores/sessions"
|
||||||
activeParentSessionId,
|
|
||||||
activeSessionId as activeSessionMap,
|
|
||||||
getSessionFamily,
|
|
||||||
getSessions,
|
|
||||||
setActiveSession,
|
|
||||||
} from "../../stores/sessions"
|
|
||||||
import { showAlertDialog } from "../../stores/alerts"
|
import { showAlertDialog } from "../../stores/alerts"
|
||||||
import type { Instance } from "../../types/instance"
|
import type { Instance } from "../../types/instance"
|
||||||
import type { MessageRecord } from "../../stores/message-v2/types"
|
import type { MessageRecord } from "../../stores/message-v2/types"
|
||||||
@@ -186,15 +180,16 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
action: () => {
|
action: () => {
|
||||||
const instanceId = activeInstanceId()
|
const instanceId = activeInstanceId()
|
||||||
if (!instanceId) return
|
if (!instanceId) return
|
||||||
const parentId = activeParentSessionId().get(instanceId)
|
const ids = getVisibleSessionIds(instanceId)
|
||||||
if (!parentId) return
|
|
||||||
const familySessions = getSessionFamily(instanceId, parentId)
|
|
||||||
const ids = familySessions.map((s) => s.id).concat(["info"])
|
|
||||||
if (ids.length <= 1) return
|
if (ids.length <= 1) return
|
||||||
const current = ids.indexOf(activeSessionMap().get(instanceId) || "")
|
|
||||||
const next = (current + 1) % ids.length
|
const currentActiveId = activeSessionIdForInstance() ?? ""
|
||||||
if (ids[next]) {
|
const currentIndex = ids.indexOf(currentActiveId)
|
||||||
setActiveSession(instanceId, ids[next])
|
const targetIndex = (currentIndex + 1 + ids.length) % ids.length
|
||||||
|
|
||||||
|
const targetSessionId = ids[targetIndex]
|
||||||
|
if (targetSessionId) {
|
||||||
|
setActiveSessionFromList(instanceId, targetSessionId)
|
||||||
emitSessionSidebarRequest({ instanceId, action: "show-session-list" })
|
emitSessionSidebarRequest({ instanceId, action: "show-session-list" })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -210,15 +205,17 @@ export function useCommands(options: UseCommandsOptions) {
|
|||||||
action: () => {
|
action: () => {
|
||||||
const instanceId = activeInstanceId()
|
const instanceId = activeInstanceId()
|
||||||
if (!instanceId) return
|
if (!instanceId) return
|
||||||
const parentId = activeParentSessionId().get(instanceId)
|
const ids = getVisibleSessionIds(instanceId)
|
||||||
if (!parentId) return
|
|
||||||
const familySessions = getSessionFamily(instanceId, parentId)
|
|
||||||
const ids = familySessions.map((s) => s.id).concat(["info"])
|
|
||||||
if (ids.length <= 1) return
|
if (ids.length <= 1) return
|
||||||
const current = ids.indexOf(activeSessionMap().get(instanceId) || "")
|
|
||||||
const prev = current <= 0 ? ids.length - 1 : current - 1
|
const currentActiveId = activeSessionIdForInstance() ?? ""
|
||||||
if (ids[prev]) {
|
const currentIndex = ids.indexOf(currentActiveId)
|
||||||
setActiveSession(instanceId, ids[prev])
|
const targetIndex =
|
||||||
|
currentIndex === -1 ? ids.length - 1 : currentIndex <= 0 ? ids.length - 1 : currentIndex - 1
|
||||||
|
|
||||||
|
const targetSessionId = ids[targetIndex]
|
||||||
|
if (targetSessionId) {
|
||||||
|
setActiveSessionFromList(instanceId, targetSessionId)
|
||||||
emitSessionSidebarRequest({ instanceId, action: "show-session-list" })
|
emitSessionSidebarRequest({ instanceId, action: "show-session-list" })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,24 +1,11 @@
|
|||||||
import { keyboardRegistry } from "../keyboard-registry"
|
import { keyboardRegistry } from "../keyboard-registry"
|
||||||
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
|
import { instances, activeInstanceId, setActiveInstanceId } from "../../stores/instances"
|
||||||
import { getSessionFamily, activeSessionId, setActiveSession, activeParentSessionId } from "../../stores/sessions"
|
import { activeSessionId, getVisibleSessionIds, setActiveSession, setActiveSessionFromList } from "../../stores/sessions"
|
||||||
|
|
||||||
export function registerNavigationShortcuts() {
|
export function registerNavigationShortcuts() {
|
||||||
const isMac = () => navigator.platform.toLowerCase().includes("mac")
|
const isMac = () => navigator.platform.toLowerCase().includes("mac")
|
||||||
|
|
||||||
const buildNavigationOrder = (instanceId: string): string[] => {
|
|
||||||
const parentId = activeParentSessionId().get(instanceId)
|
|
||||||
if (!parentId) return []
|
|
||||||
|
|
||||||
const familySessions = getSessionFamily(instanceId, parentId)
|
|
||||||
if (familySessions.length === 0) return []
|
|
||||||
|
|
||||||
const [parentSession, ...childSessions] = familySessions
|
|
||||||
if (!parentSession) return []
|
|
||||||
|
|
||||||
const sortedChildren = childSessions.slice().sort((a, b) => b.time.updated - a.time.updated)
|
|
||||||
|
|
||||||
return [parentSession.id, "info", ...sortedChildren.map((session) => session.id)]
|
|
||||||
}
|
|
||||||
|
|
||||||
keyboardRegistry.register({
|
keyboardRegistry.register({
|
||||||
id: "instance-prev",
|
id: "instance-prev",
|
||||||
@@ -58,20 +45,23 @@ export function registerNavigationShortcuts() {
|
|||||||
const instanceId = activeInstanceId()
|
const instanceId = activeInstanceId()
|
||||||
if (!instanceId) return
|
if (!instanceId) return
|
||||||
|
|
||||||
const navigationIds = buildNavigationOrder(instanceId)
|
const navigationIds = getVisibleSessionIds(instanceId)
|
||||||
if (navigationIds.length === 0) return
|
if (navigationIds.length === 0) return
|
||||||
|
|
||||||
const currentActiveId = activeSessionId().get(instanceId)
|
const currentActiveId = activeSessionId().get(instanceId) ?? ""
|
||||||
let currentIndex = navigationIds.indexOf(currentActiveId || "")
|
const currentIndex = navigationIds.indexOf(currentActiveId)
|
||||||
|
|
||||||
if (currentIndex === -1) {
|
const targetIndex =
|
||||||
currentIndex = navigationIds.length - 1
|
currentIndex === -1
|
||||||
}
|
? navigationIds.length - 1
|
||||||
|
: currentIndex <= 0
|
||||||
|
? navigationIds.length - 1
|
||||||
|
: currentIndex - 1
|
||||||
|
|
||||||
const targetIndex = currentIndex <= 0 ? navigationIds.length - 1 : currentIndex - 1
|
|
||||||
const targetSessionId = navigationIds[targetIndex]
|
const targetSessionId = navigationIds[targetIndex]
|
||||||
|
if (targetSessionId) {
|
||||||
setActiveSession(instanceId, targetSessionId)
|
setActiveSessionFromList(instanceId, targetSessionId)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: "previous session",
|
description: "previous session",
|
||||||
context: "global",
|
context: "global",
|
||||||
@@ -85,20 +75,17 @@ export function registerNavigationShortcuts() {
|
|||||||
const instanceId = activeInstanceId()
|
const instanceId = activeInstanceId()
|
||||||
if (!instanceId) return
|
if (!instanceId) return
|
||||||
|
|
||||||
const navigationIds = buildNavigationOrder(instanceId)
|
const navigationIds = getVisibleSessionIds(instanceId)
|
||||||
if (navigationIds.length === 0) return
|
if (navigationIds.length === 0) return
|
||||||
|
|
||||||
const currentActiveId = activeSessionId().get(instanceId)
|
const currentActiveId = activeSessionId().get(instanceId) ?? ""
|
||||||
let currentIndex = navigationIds.indexOf(currentActiveId || "")
|
const currentIndex = navigationIds.indexOf(currentActiveId)
|
||||||
|
const targetIndex = (currentIndex + 1 + navigationIds.length) % navigationIds.length
|
||||||
|
|
||||||
if (currentIndex === -1) {
|
|
||||||
currentIndex = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetIndex = (currentIndex + 1) % navigationIds.length
|
|
||||||
const targetSessionId = navigationIds[targetIndex]
|
const targetSessionId = navigationIds[targetIndex]
|
||||||
|
if (targetSessionId) {
|
||||||
setActiveSession(instanceId, targetSessionId)
|
setActiveSessionFromList(instanceId, targetSessionId)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
description: "next session",
|
description: "next session",
|
||||||
context: "global",
|
context: "global",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createSignal } from "solid-js"
|
import { batch, createSignal } from "solid-js"
|
||||||
|
|
||||||
import type { Session, SessionStatus, Agent, Provider } from "../types/session"
|
import type { Session, SessionStatus, Agent, Provider } from "../types/session"
|
||||||
import { deleteSession, loadMessages } from "./session-api"
|
import { deleteSession, loadMessages } from "./session-api"
|
||||||
@@ -46,6 +46,8 @@ const [loading, setLoading] = createSignal({
|
|||||||
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
|
const [messagesLoaded, setMessagesLoaded] = createSignal<Map<string, Set<string>>>(new Map())
|
||||||
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
|
const [sessionInfoByInstance, setSessionInfoByInstance] = createSignal<Map<string, Map<string, SessionInfo>>>(new Map())
|
||||||
|
|
||||||
|
const [expandedSessionParents, setExpandedSessionParents] = createSignal<Map<string, Set<string>>>(new Map())
|
||||||
|
|
||||||
export type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
export type InstanceSessionIndicatorStatus = "permission" | SessionStatus
|
||||||
|
|
||||||
type InstanceIndicatorCounts = {
|
type InstanceIndicatorCounts = {
|
||||||
@@ -430,6 +432,91 @@ function getSessionThreads(instanceId: string): SessionThread[] {
|
|||||||
return threads
|
return threads
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSessionParentExpanded(instanceId: string, parentSessionId: string): boolean {
|
||||||
|
return Boolean(expandedSessionParents().get(instanceId)?.has(parentSessionId))
|
||||||
|
}
|
||||||
|
|
||||||
|
function setSessionParentExpanded(instanceId: string, parentSessionId: string, expanded: boolean): void {
|
||||||
|
setExpandedSessionParents((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
const currentSet = next.get(instanceId) ?? new Set<string>()
|
||||||
|
const updated = new Set(currentSet)
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
updated.add(parentSessionId)
|
||||||
|
} else {
|
||||||
|
updated.delete(parentSessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updated.size === 0) {
|
||||||
|
next.delete(instanceId)
|
||||||
|
} else {
|
||||||
|
next.set(instanceId, updated)
|
||||||
|
}
|
||||||
|
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSessionParentExpanded(instanceId: string, parentSessionId: string): void {
|
||||||
|
setExpandedSessionParents((prev) => {
|
||||||
|
const next = new Map(prev)
|
||||||
|
const currentSet = next.get(instanceId) ?? new Set<string>()
|
||||||
|
const updated = new Set(currentSet)
|
||||||
|
|
||||||
|
if (updated.has(parentSessionId)) {
|
||||||
|
updated.delete(parentSessionId)
|
||||||
|
} else {
|
||||||
|
updated.add(parentSessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
next.set(instanceId, updated)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureSessionParentExpanded(instanceId: string, parentSessionId: string): void {
|
||||||
|
if (isSessionParentExpanded(instanceId, parentSessionId)) return
|
||||||
|
setSessionParentExpanded(instanceId, parentSessionId, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getVisibleSessionIds(instanceId: string): string[] {
|
||||||
|
const threads = getSessionThreads(instanceId)
|
||||||
|
if (threads.length === 0) return []
|
||||||
|
|
||||||
|
const expanded = expandedSessionParents().get(instanceId)
|
||||||
|
const ids: string[] = []
|
||||||
|
|
||||||
|
for (const thread of threads) {
|
||||||
|
ids.push(thread.parent.id)
|
||||||
|
if (expanded?.has(thread.parent.id)) {
|
||||||
|
for (const child of thread.children) {
|
||||||
|
ids.push(child.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveSessionFromList(instanceId: string, sessionId: string): void {
|
||||||
|
const session = sessions().get(instanceId)?.get(sessionId)
|
||||||
|
if (!session) return
|
||||||
|
|
||||||
|
if (session.parentId === null) {
|
||||||
|
setActiveParentSession(instanceId, sessionId)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const parentId = session.parentId
|
||||||
|
if (!parentId) return
|
||||||
|
|
||||||
|
batch(() => {
|
||||||
|
setActiveParentSession(instanceId, parentId)
|
||||||
|
setActiveSession(instanceId, sessionId)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function isSessionBusy(instanceId: string, sessionId: string): boolean {
|
function isSessionBusy(instanceId: string, sessionId: string): boolean {
|
||||||
const instanceSessions = sessions().get(instanceId)
|
const instanceSessions = sessions().get(instanceId)
|
||||||
if (!instanceSessions) return false
|
if (!instanceSessions) return false
|
||||||
@@ -586,6 +673,12 @@ export {
|
|||||||
getChildSessions,
|
getChildSessions,
|
||||||
getSessionFamily,
|
getSessionFamily,
|
||||||
getSessionThreads,
|
getSessionThreads,
|
||||||
|
getVisibleSessionIds,
|
||||||
|
isSessionParentExpanded,
|
||||||
|
setSessionParentExpanded,
|
||||||
|
toggleSessionParentExpanded,
|
||||||
|
ensureSessionParentExpanded,
|
||||||
|
setActiveSessionFromList,
|
||||||
isSessionBusy,
|
isSessionBusy,
|
||||||
isSessionMessagesLoading,
|
isSessionMessagesLoading,
|
||||||
getSessionInfo,
|
getSessionInfo,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
clearActiveParentSession,
|
clearActiveParentSession,
|
||||||
clearInstanceDraftPrompts,
|
clearInstanceDraftPrompts,
|
||||||
clearSessionDraftPrompt,
|
clearSessionDraftPrompt,
|
||||||
|
ensureSessionParentExpanded,
|
||||||
getActiveParentSession,
|
getActiveParentSession,
|
||||||
getActiveSession,
|
getActiveSession,
|
||||||
getChildSessions,
|
getChildSessions,
|
||||||
@@ -18,17 +19,22 @@ import {
|
|||||||
getSessionInfo,
|
getSessionInfo,
|
||||||
getSessionThreads,
|
getSessionThreads,
|
||||||
getSessions,
|
getSessions,
|
||||||
|
getVisibleSessionIds,
|
||||||
isSessionBusy,
|
isSessionBusy,
|
||||||
isSessionMessagesLoading,
|
isSessionMessagesLoading,
|
||||||
|
isSessionParentExpanded,
|
||||||
loading,
|
loading,
|
||||||
providers,
|
providers,
|
||||||
sessionInfoByInstance,
|
sessionInfoByInstance,
|
||||||
sessions,
|
sessions,
|
||||||
setActiveParentSession,
|
setActiveParentSession,
|
||||||
setActiveSession,
|
setActiveSession,
|
||||||
|
setActiveSessionFromList,
|
||||||
setSessionDraftPrompt,
|
setSessionDraftPrompt,
|
||||||
|
setSessionParentExpanded,
|
||||||
setSessionStatus,
|
setSessionStatus,
|
||||||
} from "./session-state"
|
toggleSessionParentExpanded,
|
||||||
|
} from "./session-state"
|
||||||
|
|
||||||
import { getDefaultModel } from "./session-models"
|
import { getDefaultModel } from "./session-models"
|
||||||
import {
|
import {
|
||||||
@@ -86,6 +92,7 @@ export {
|
|||||||
clearSessionDraftPrompt,
|
clearSessionDraftPrompt,
|
||||||
createSession,
|
createSession,
|
||||||
deleteSession,
|
deleteSession,
|
||||||
|
ensureSessionParentExpanded,
|
||||||
executeCustomCommand,
|
executeCustomCommand,
|
||||||
renameSession,
|
renameSession,
|
||||||
runShellCommand,
|
runShellCommand,
|
||||||
@@ -103,8 +110,10 @@ export {
|
|||||||
getSessionInfo,
|
getSessionInfo,
|
||||||
getSessionThreads,
|
getSessionThreads,
|
||||||
getSessions,
|
getSessions,
|
||||||
|
getVisibleSessionIds,
|
||||||
isSessionBusy,
|
isSessionBusy,
|
||||||
isSessionMessagesLoading,
|
isSessionMessagesLoading,
|
||||||
|
isSessionParentExpanded,
|
||||||
loadMessages,
|
loadMessages,
|
||||||
loading,
|
loading,
|
||||||
providers,
|
providers,
|
||||||
@@ -113,8 +122,11 @@ export {
|
|||||||
sessions,
|
sessions,
|
||||||
setActiveParentSession,
|
setActiveParentSession,
|
||||||
setActiveSession,
|
setActiveSession,
|
||||||
|
setActiveSessionFromList,
|
||||||
setSessionDraftPrompt,
|
setSessionDraftPrompt,
|
||||||
|
setSessionParentExpanded,
|
||||||
setSessionStatus,
|
setSessionStatus,
|
||||||
|
toggleSessionParentExpanded,
|
||||||
updateSessionAgent,
|
updateSessionAgent,
|
||||||
updateSessionModel,
|
updateSessionModel,
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user