fix(tui): address inbox review findings
This commit is contained in:
parent
4f936c2c95
commit
dbd934689f
9 changed files with 125 additions and 40 deletions
|
|
@ -69,6 +69,7 @@ import { DialogSessionList } from "./component/dialog-session-list"
|
||||||
import { DialogOpen } from "./component/dialog-open"
|
import { DialogOpen } from "./component/dialog-open"
|
||||||
import { SessionTabs } from "./component/session-tabs"
|
import { SessionTabs } from "./component/session-tabs"
|
||||||
import { SessionInbox } from "./component/session-inbox"
|
import { SessionInbox } from "./component/session-inbox"
|
||||||
|
import { SESSION_INBOX_MIN_TERMINAL_WIDTH } from "./context/session-tabs-model"
|
||||||
import { ThemeErrorToast } from "./component/theme-error-toast"
|
import { ThemeErrorToast } from "./component/theme-error-toast"
|
||||||
import { ThemeProvider, useTheme, useThemes } from "./context/theme"
|
import { ThemeProvider, useTheme, useThemes } from "./context/theme"
|
||||||
import { Home } from "./routes/home"
|
import { Home } from "./routes/home"
|
||||||
|
|
@ -520,7 +521,8 @@ function App(props: { pair?: DialogPairCredentials }) {
|
||||||
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
|
const terminalTitleEnabled = () => config.data.terminal?.title ?? true
|
||||||
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
|
const copyOnSelectEnabled = () => config.data.terminal?.copy_on_select ?? process.platform !== "win32"
|
||||||
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
|
const pasteSummaryEnabled = () => config.data.prompt?.paste !== "full"
|
||||||
const inboxTabsEnabled = () => config.data.tabs?.layout === "inbox" && dimensions().width >= 72
|
const inboxTabsEnabled = () =>
|
||||||
|
config.data.tabs?.layout === "inbox" && dimensions().width >= SESSION_INBOX_MIN_TERMINAL_WIDTH
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
renderer.useMouse = config.data.mouse
|
renderer.useMouse = config.data.mouse
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
import { TextAttributes, type ScrollBoxRenderable } from "@opentui/core"
|
||||||
import { useTerminalDimensions } from "@opentui/solid"
|
import { useTerminalDimensions } from "@opentui/solid"
|
||||||
import type { SessionMessageAssistant } from "@opencode-ai/client"
|
import type { SessionMessageAssistant } from "@opencode-ai/client"
|
||||||
import { For, Show, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
import { For, Match, Show, Switch, createEffect, createMemo, createSignal, onCleanup } from "solid-js"
|
||||||
import { useConfig } from "../config"
|
import { useConfig } from "../config"
|
||||||
import { useData } from "../context/data"
|
import { useData } from "../context/data"
|
||||||
import { Keymap } from "../context/keymap"
|
import { Keymap } from "../context/keymap"
|
||||||
import { usePromptRef } from "../context/prompt"
|
import { usePromptRef } from "../context/prompt"
|
||||||
import { useRoute } from "../context/route"
|
import { useRoute } from "../context/route"
|
||||||
import { useSessionTabs } from "../context/session-tabs"
|
import { useSessionTabs } from "../context/session-tabs"
|
||||||
import { sessionInboxGroup, type SessionInboxGroup } from "../context/session-tabs-model"
|
import { sessionInboxGroup, sessionInboxWidth, type SessionInboxGroup } from "../context/session-tabs-model"
|
||||||
import { useTheme, useThemes } from "../context/theme"
|
import { useTheme, useThemes } from "../context/theme"
|
||||||
import { tint } from "../theme/color"
|
import { tint } from "../theme/color"
|
||||||
import { getScrollAcceleration } from "../util/scroll"
|
import { getScrollAcceleration } from "../util/scroll"
|
||||||
|
|
@ -101,40 +101,42 @@ export function SessionInboxRow(props: {
|
||||||
>
|
>
|
||||||
{props.row.title}
|
{props.row.title}
|
||||||
</text>
|
</text>
|
||||||
<Show when={!props.row.status.busy && (props.row.status.unread || props.row.status.attention)}>
|
<Show when={props.row.status.attention || (!props.row.status.busy && props.row.status.unread)}>
|
||||||
<text width={2} fg={feedback()} selectable={false}>
|
<text width={2} fg={feedback()} selectable={false}>
|
||||||
●
|
●
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Show>
|
||||||
</box>
|
</box>
|
||||||
<box height={1}>
|
<box height={1}>
|
||||||
<Show
|
<Switch
|
||||||
when={!props.pendingDone && !props.row.status.busy}
|
|
||||||
fallback={
|
fallback={
|
||||||
<Show
|
<text
|
||||||
when={props.pendingDone}
|
height={1}
|
||||||
fallback={
|
fg={tint(theme.text.subdued, theme.text.default, props.selected ? 0.25 : 0)}
|
||||||
<Spinner color={accent()}>
|
wrapMode="none"
|
||||||
<span style={{ fg: accent() }}>{props.verb ?? activityVerb(props.row.sessionID)}</span>
|
truncate
|
||||||
</Spinner>
|
selectable={false}
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<text fg={theme.text.feedback.warning.default} wrapMode="none" truncate>
|
{props.row.preview}
|
||||||
Space again to mark done
|
</text>
|
||||||
</text>
|
|
||||||
</Show>
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<text
|
<Match when={props.pendingDone}>
|
||||||
height={1}
|
<text fg={theme.text.feedback.warning.default} wrapMode="none" truncate>
|
||||||
fg={tint(theme.text.subdued, theme.text.default, props.selected ? 0.25 : 0)}
|
Space again to mark done
|
||||||
wrapMode="none"
|
</text>
|
||||||
truncate
|
</Match>
|
||||||
selectable={false}
|
<Match when={props.row.status.attention}>
|
||||||
>
|
<text fg={theme.text.feedback.warning.default} wrapMode="none" truncate>
|
||||||
{props.row.preview}
|
Action required
|
||||||
</text>
|
</text>
|
||||||
</Show>
|
</Match>
|
||||||
|
<Match when={props.row.status.busy}>
|
||||||
|
<Spinner color={accent()}>
|
||||||
|
<span style={{ fg: accent() }}>{props.verb ?? activityVerb(props.row.sessionID)}</span>
|
||||||
|
</Spinner>
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</box>
|
</box>
|
||||||
|
|
@ -158,7 +160,7 @@ export function SessionInbox() {
|
||||||
clearInterval(verbTimer)
|
clearInterval(verbTimer)
|
||||||
tabs.navigation.blur()
|
tabs.navigation.blur()
|
||||||
})
|
})
|
||||||
const width = createMemo(() => Math.max(28, Math.min(40, Math.floor(dimensions().width * 0.28))))
|
const width = createMemo(() => sessionInboxWidth(dimensions().width))
|
||||||
const hueStep = () => (themes.mode() === "light" ? 800 : 200)
|
const hueStep = () => (themes.mode() === "light" ? 800 : 200)
|
||||||
const accent = () => theme.hue.accent[hueStep()]
|
const accent = () => theme.hue.accent[hueStep()]
|
||||||
const rows = createMemo(() => {
|
const rows = createMemo(() => {
|
||||||
|
|
@ -178,13 +180,14 @@ export function SessionInbox() {
|
||||||
.filter((part) => part.type === "text")
|
.filter((part) => part.type === "text")
|
||||||
.map((part) => part.text)
|
.map((part) => part.text)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
|
const updated = tabs.updated(tab.sessionID)
|
||||||
return {
|
return {
|
||||||
sessionID: tab.sessionID,
|
sessionID: tab.sessionID,
|
||||||
title: session ? withTimestampedFallback(session) : (tab.title ?? "Loading session…"),
|
title: session ? withTimestampedFallback(session) : (tab.title ?? "Loading session…"),
|
||||||
updated: session?.time.updated ?? 0,
|
updated,
|
||||||
preview: markdownPreview(preview ?? "") || "No assistant response yet",
|
preview: markdownPreview(preview ?? "") || "No assistant response yet",
|
||||||
status,
|
status,
|
||||||
group: sessionInboxGroup(session?.time.updated ?? 0, status.busy),
|
group: sessionInboxGroup(updated, status.busy),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.toSorted((a, b) => b.updated - a.updated)
|
.toSorted((a, b) => b.updated - a.updated)
|
||||||
|
|
@ -204,8 +207,11 @@ export function SessionInbox() {
|
||||||
if (!row) return
|
if (!row) return
|
||||||
const top = scroll.scrollTop + row.y - scroll.viewport.y
|
const top = scroll.scrollTop + row.y - scroll.viewport.y
|
||||||
const bottom = top + row.height
|
const bottom = top + row.height
|
||||||
if (top < scroll.scrollTop) scroll.scrollTo(top)
|
if (top < scroll.scrollTop) {
|
||||||
else if (bottom > scroll.scrollTop + scroll.viewport.height) scroll.scrollTo(bottom - scroll.viewport.height)
|
scroll.scrollTo(top)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (bottom > scroll.scrollTop + scroll.viewport.height) scroll.scrollTo(bottom - scroll.viewport.height)
|
||||||
})
|
})
|
||||||
|
|
||||||
const leave = () => {
|
const leave = () => {
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ export const Definitions = {
|
||||||
session_export: keybind("<leader>x", "Export session to editor"),
|
session_export: keybind("<leader>x", "Export session to editor"),
|
||||||
session_copy: keybind("none", "Copy session transcript"),
|
session_copy: keybind("none", "Copy session transcript"),
|
||||||
session_move: keybind("none", "Move session"),
|
session_move: keybind("none", "Move session"),
|
||||||
session_new: keybind("alt+t,ctrl+t,<leader>n", "Create a new session"),
|
session_new: keybind("alt+t,<leader>n", "Create a new session"),
|
||||||
session_list: keybind("<leader>l", "List all sessions"),
|
session_list: keybind("<leader>l", "List all sessions"),
|
||||||
open_menu: keybind("ctrl+o", "Open recent sessions and projects"),
|
open_menu: keybind("ctrl+o", "Open recent sessions and projects"),
|
||||||
session_tab_next: keybind("ctrl+tab,<leader>right,alt+shift+]", "Switch to next open tab"),
|
session_tab_next: keybind("ctrl+tab,<leader>right,alt+shift+]", "Switch to next open tab"),
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ export const sessionTabOverflowWidth = (count: number) => String(count).length +
|
||||||
|
|
||||||
export type SessionInboxGroup = "running" | "today" | "yesterday" | "earlier"
|
export type SessionInboxGroup = "running" | "today" | "yesterday" | "earlier"
|
||||||
|
|
||||||
|
export const SESSION_INBOX_MIN_TERMINAL_WIDTH = 72
|
||||||
|
|
||||||
|
export function sessionInboxWidth(terminalWidth: number) {
|
||||||
|
return Math.max(28, Math.min(40, Math.floor(terminalWidth * 0.28)))
|
||||||
|
}
|
||||||
|
|
||||||
export function sessionInboxGroup(updated: number, running: boolean, now = Date.now()): SessionInboxGroup {
|
export function sessionInboxGroup(updated: number, running: boolean, now = Date.now()): SessionInboxGroup {
|
||||||
if (running) return "running"
|
if (running) return "running"
|
||||||
const today = new Date(now)
|
const today = new Date(now)
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||||
})
|
})
|
||||||
const fallback = empty()
|
const fallback = empty()
|
||||||
const [promptPulses, setPromptPulses] = createSignal<Record<string, number>>({})
|
const [promptPulses, setPromptPulses] = createSignal<Record<string, number>>({})
|
||||||
|
const [lastActivity, setLastActivity] = createSignal<Record<string, number>>({})
|
||||||
const [navigationActive, setNavigationActive] = createSignal(false)
|
const [navigationActive, setNavigationActive] = createSignal(false)
|
||||||
const [navigationSelection, setNavigationSelection] = createSignal<string>()
|
const [navigationSelection, setNavigationSelection] = createSignal<string>()
|
||||||
const [navigationPendingDone, setNavigationPendingDone] = createSignal<string>()
|
const [navigationPendingDone, setNavigationPendingDone] = createSignal<string>()
|
||||||
|
|
@ -83,6 +84,12 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||||
}
|
}
|
||||||
|
|
||||||
const root = (sessionID: string) => data.session.root(sessionID)
|
const root = (sessionID: string) => data.session.root(sessionID)
|
||||||
|
const updated = (sessionID: string) =>
|
||||||
|
Math.max(data.session.get(sessionID)?.time.updated ?? 0, lastActivity()[root(sessionID)] ?? 0)
|
||||||
|
const touch = (sessionID: string, created: number) => {
|
||||||
|
const session = root(sessionID)
|
||||||
|
setLastActivity((value) => ({ ...value, [session]: Math.max(value[session] ?? 0, created) }))
|
||||||
|
}
|
||||||
const title = (sessionID: string, persisted?: string, fallback?: string) => {
|
const title = (sessionID: string, persisted?: string, fallback?: string) => {
|
||||||
const session = data.session.get(sessionID)
|
const session = data.session.get(sessionID)
|
||||||
return session?.title ?? persisted ?? fallback ?? (session ? withTimestampedFallback(session) : undefined)
|
return session?.title ?? persisted ?? fallback ?? (session ? withTimestampedFallback(session) : undefined)
|
||||||
|
|
@ -121,7 +128,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||||
const recent = () =>
|
const recent = () =>
|
||||||
orderSessionTabs(state().tabs, (sessionID) => ({
|
orderSessionTabs(state().tabs, (sessionID) => ({
|
||||||
busy: status(sessionID).busy,
|
busy: status(sessionID).busy,
|
||||||
updated: data.session.get(sessionID)?.time.updated ?? 0,
|
updated: updated(sessionID),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
function markUnread(sessionID: string, unread: SessionTabUnread) {
|
function markUnread(sessionID: string, unread: SessionTabUnread) {
|
||||||
|
|
@ -219,13 +226,29 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
onCleanup(event.on("session.execution.succeeded", (evt) => markUnread(evt.data.sessionID, "activity")))
|
onCleanup(
|
||||||
onCleanup(event.on("session.execution.interrupted", (evt) => markUnread(evt.data.sessionID, "activity")))
|
event.on("session.execution.succeeded", (evt) => {
|
||||||
onCleanup(event.on("session.execution.failed", (evt) => markUnread(evt.data.sessionID, "error")))
|
touch(evt.data.sessionID, evt.created)
|
||||||
|
markUnread(evt.data.sessionID, "activity")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
onCleanup(
|
||||||
|
event.on("session.execution.interrupted", (evt) => {
|
||||||
|
touch(evt.data.sessionID, evt.created)
|
||||||
|
markUnread(evt.data.sessionID, "activity")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
onCleanup(
|
||||||
|
event.on("session.execution.failed", (evt) => {
|
||||||
|
touch(evt.data.sessionID, evt.created)
|
||||||
|
markUnread(evt.data.sessionID, "error")
|
||||||
|
}),
|
||||||
|
)
|
||||||
onCleanup(
|
onCleanup(
|
||||||
event.on("session.input.admitted", (evt) => {
|
event.on("session.input.admitted", (evt) => {
|
||||||
if (!enabled() || evt.data.input.type !== "user") return
|
if (!enabled() || evt.data.input.type !== "user") return
|
||||||
const sessionID = root(evt.data.sessionID)
|
const sessionID = root(evt.data.sessionID)
|
||||||
|
touch(sessionID, evt.created)
|
||||||
if (current() === sessionID || !state().tabs.some((tab) => tab.sessionID === sessionID)) return
|
if (current() === sessionID || !state().tabs.some((tab) => tab.sessionID === sessionID)) return
|
||||||
setPromptPulses((pulses) => ({ ...pulses, [sessionID]: (pulses[sessionID] ?? 0) + 1 }))
|
setPromptPulses((pulses) => ({ ...pulses, [sessionID]: (pulses[sessionID] ?? 0) + 1 }))
|
||||||
}),
|
}),
|
||||||
|
|
@ -272,6 +295,7 @@ export const { use: useSessionTabs, provider: SessionTabsProvider } = createSimp
|
||||||
return state().tabs
|
return state().tabs
|
||||||
},
|
},
|
||||||
recent,
|
recent,
|
||||||
|
updated,
|
||||||
newTab() {
|
newTab() {
|
||||||
return newTab()
|
return newTab()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,8 @@ import { DialogExportOptions } from "../../ui/dialog-export-options"
|
||||||
import { DialogExportResult } from "../../ui/dialog-export-result"
|
import { DialogExportResult } from "../../ui/dialog-export-result"
|
||||||
import { sessionEpilogue } from "../../util/presentation"
|
import { sessionEpilogue } from "../../util/presentation"
|
||||||
import { useConfig } from "../../config"
|
import { useConfig } from "../../config"
|
||||||
|
import { useSessionTabs } from "../../context/session-tabs"
|
||||||
|
import { SESSION_INBOX_MIN_TERMINAL_WIDTH, sessionInboxWidth } from "../../context/session-tabs-model"
|
||||||
import { useClipboard } from "../../context/clipboard"
|
import { useClipboard } from "../../context/clipboard"
|
||||||
import { nextThinkingMode, reasoningSummary, type ThinkingMode } from "../../context/thinking"
|
import { nextThinkingMode, reasoningSummary, type ThinkingMode } from "../../context/thinking"
|
||||||
import { getScrollAcceleration } from "../../util/scroll"
|
import { getScrollAcceleration } from "../../util/scroll"
|
||||||
|
|
@ -190,6 +192,7 @@ export function Session() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const dimensions = useTerminalDimensions()
|
const dimensions = useTerminalDimensions()
|
||||||
|
const sessionTabs = useSessionTabs()
|
||||||
const sidebar = createMemo(() => config.session?.sidebar ?? "auto")
|
const sidebar = createMemo(() => config.session?.sidebar ?? "auto")
|
||||||
const [sidebarOpen, setSidebarOpen] = createSignal(false)
|
const [sidebarOpen, setSidebarOpen] = createSignal(false)
|
||||||
const thinkingMode = createMemo<ThinkingMode>(() => config.session?.thinking ?? "hide")
|
const thinkingMode = createMemo<ThinkingMode>(() => config.session?.thinking ?? "hide")
|
||||||
|
|
@ -199,14 +202,20 @@ export function Session() {
|
||||||
const diffWrapMode = createMemo(() => config.diffs?.wrap ?? "word")
|
const diffWrapMode = createMemo(() => config.diffs?.wrap ?? "word")
|
||||||
const groupExploration = createMemo(() => config.session?.grouping !== "none")
|
const groupExploration = createMemo(() => config.session?.grouping !== "none")
|
||||||
|
|
||||||
const wide = createMemo(() => dimensions().width > 120)
|
const viewportWidth = createMemo(() => {
|
||||||
|
const width = dimensions().width
|
||||||
|
if (!sessionTabs.enabled() || config.tabs?.layout !== "inbox" || width < SESSION_INBOX_MIN_TERMINAL_WIDTH)
|
||||||
|
return width
|
||||||
|
return width - sessionInboxWidth(width)
|
||||||
|
})
|
||||||
|
const wide = createMemo(() => viewportWidth() > 120)
|
||||||
const sidebarVisible = createMemo(() => {
|
const sidebarVisible = createMemo(() => {
|
||||||
if (session()?.parentID) return false
|
if (session()?.parentID) return false
|
||||||
if (sidebarOpen()) return true
|
if (sidebarOpen()) return true
|
||||||
if (sidebar() === "auto" && wide()) return true
|
if (sidebar() === "auto" && wide()) return true
|
||||||
return false
|
return false
|
||||||
})
|
})
|
||||||
const contentWidth = createMemo(() => dimensions().width - (sidebarVisible() ? 42 : 0) - 4)
|
const contentWidth = createMemo(() => viewportWidth() - (sidebarVisible() ? 42 : 0) - 4)
|
||||||
const models = createMemo(() => data.location.model.list(location()) ?? [])
|
const models = createMemo(() => data.location.model.list(location()) ?? [])
|
||||||
|
|
||||||
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
const scrollAcceleration = createMemo(() => getScrollAcceleration(config))
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ test("navigates session tabs with leader arrows", () => {
|
||||||
test("preserves pinned session bindings alongside tab bindings", () => {
|
test("preserves pinned session bindings alongside tab bindings", () => {
|
||||||
const config = resolve({}, { terminalSuspend: true })
|
const config = resolve({}, { terminalSuspend: true })
|
||||||
|
|
||||||
expect(config.keybinds.get("session.new")).toMatchObject([{ key: "alt+t,ctrl+t,<leader>n" }])
|
expect(config.keybinds.get("session.new")).toMatchObject([{ key: "alt+t,<leader>n" }])
|
||||||
expect(config.keybinds.has("session.toggle.thinking")).toBe(false)
|
expect(config.keybinds.has("session.toggle.thinking")).toBe(false)
|
||||||
expect(config.keybinds.get("session.pin.toggle")).toMatchObject([{ key: "ctrl+f" }])
|
expect(config.keybinds.get("session.pin.toggle")).toMatchObject([{ key: "ctrl+f" }])
|
||||||
expect(config.keybinds.get("session.quick_switch.1")).toMatchObject([{ key: "<leader>1" }])
|
expect(config.keybinds.get("session.quick_switch.1")).toMatchObject([{ key: "<leader>1" }])
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
reopenSessionTab,
|
reopenSessionTab,
|
||||||
seedSessionTabMotion,
|
seedSessionTabMotion,
|
||||||
sessionInboxGroup,
|
sessionInboxGroup,
|
||||||
|
sessionInboxWidth,
|
||||||
sessionTabComplete,
|
sessionTabComplete,
|
||||||
sessionTabOverflowWidth,
|
sessionTabOverflowWidth,
|
||||||
} from "../../src/context/session-tabs-model"
|
} from "../../src/context/session-tabs-model"
|
||||||
|
|
@ -42,6 +43,12 @@ describe("session tabs", () => {
|
||||||
expect(sessionInboxGroup(new Date(2026, 6, 29, 23).getTime(), false, now)).toBe("earlier")
|
expect(sessionInboxGroup(new Date(2026, 6, 29, 23).getTime(), false, now)).toBe("earlier")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("sizes the inbox within its minimum and maximum rails", () => {
|
||||||
|
expect(sessionInboxWidth(72)).toBe(28)
|
||||||
|
expect(sessionInboxWidth(120)).toBe(33)
|
||||||
|
expect(sessionInboxWidth(200)).toBe(40)
|
||||||
|
})
|
||||||
|
|
||||||
test("moves a tab to a clamped index and returns the same tabs for no-ops", () => {
|
test("moves a tab to a clamped index and returns the same tabs for no-ops", () => {
|
||||||
const tabs = ["a", "b", "c"].map((sessionID) => ({ sessionID }))
|
const tabs = ["a", "b", "c"].map((sessionID) => ({ sessionID }))
|
||||||
expect(moveSessionTab(tabs, "a", 2).map((tab) => tab.sessionID)).toEqual(["b", "c", "a"])
|
expect(moveSessionTab(tabs, "a", 2).map((tab) => tab.sessionID)).toEqual(["b", "c", "a"])
|
||||||
|
|
|
||||||
|
|
@ -218,6 +218,37 @@ test("user prompt admissions pulse an already-busy background tab", async () =>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("tracks live inbox recency beyond cached session metadata", async () => {
|
||||||
|
const setup = await renderSessionTabs("background")
|
||||||
|
|
||||||
|
try {
|
||||||
|
expect(setup.tabs.updated("background")).toBe(0)
|
||||||
|
setup.emit({
|
||||||
|
id: "evt_admitted",
|
||||||
|
created: 100,
|
||||||
|
type: "session.input.admitted",
|
||||||
|
durable: { aggregateID: "background", seq: 1, version: 1 },
|
||||||
|
data: {
|
||||||
|
sessionID: "background",
|
||||||
|
inputID: "msg_1",
|
||||||
|
input: { type: "user", data: { text: "work" }, delivery: "steer" },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await wait(() => setup.tabs.updated("background") === 100)
|
||||||
|
|
||||||
|
setup.emit({
|
||||||
|
id: "evt_succeeded",
|
||||||
|
created: 200,
|
||||||
|
type: "session.execution.succeeded",
|
||||||
|
durable: { aggregateID: "background", seq: 2, version: 1 },
|
||||||
|
data: { sessionID: "background" },
|
||||||
|
})
|
||||||
|
await wait(() => setup.tabs.updated("background") === 200)
|
||||||
|
} finally {
|
||||||
|
setup.destroy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
test("tracks a temporary new session tab across close and creation", async () => {
|
test("tracks a temporary new session tab across close and creation", async () => {
|
||||||
const setup = await renderSessionTabs("first")
|
const setup = await renderSessionTabs("first")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue