feat(tui): add inbox tab layout
This commit is contained in:
parent
85ea15e56d
commit
a01d24e908
15 changed files with 693 additions and 27 deletions
|
|
@ -17,8 +17,11 @@ test("validates mini replay settings", () => {
|
|||
test("validates the session tabs setting", () => {
|
||||
const decode = Schema.decodeUnknownSync(Info)
|
||||
|
||||
expect(decode({ tabs: { enabled: true } })).toEqual({ tabs: { enabled: true } })
|
||||
expect(decode({ tabs: { enabled: true, layout: "inbox" } })).toEqual({
|
||||
tabs: { enabled: true, layout: "inbox" },
|
||||
})
|
||||
expect(() => decode({ tabs: { enabled: "on" } })).toThrow()
|
||||
expect(() => decode({ tabs: { layout: "sidebar" } })).toThrow()
|
||||
})
|
||||
|
||||
test("resolves nested config and keybind defaults", () => {
|
||||
|
|
|
|||
|
|
@ -117,9 +117,11 @@ test("navigates session tabs with leader arrows", () => {
|
|||
test("preserves pinned session bindings alongside tab bindings", () => {
|
||||
const config = resolve({}, { terminalSuspend: true })
|
||||
|
||||
expect(config.keybinds.get("session.new")).toMatchObject([{ key: "alt+t,ctrl+t,<leader>n" }])
|
||||
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.quick_switch.1")).toMatchObject([{ key: "<leader>1" }])
|
||||
expect(config.keybinds.get("session.tab.select.1")).toMatchObject([{ key: "<leader>1,ctrl+1" }])
|
||||
expect(config.keybinds.get("session.tab.select.1")).toMatchObject([{ key: "<leader>1,ctrl+1,alt+1" }])
|
||||
})
|
||||
|
||||
test("disables suspend and assigns ctrl+z to undo when unsupported", () => {
|
||||
|
|
|
|||
|
|
@ -6,15 +6,42 @@ import {
|
|||
moveSessionTab,
|
||||
moveSessionTabHistory,
|
||||
openSessionTab,
|
||||
orderSessionTabs,
|
||||
recordClosedSessionTab,
|
||||
recordSessionTabHistory,
|
||||
reopenSessionTab,
|
||||
seedSessionTabMotion,
|
||||
sessionInboxGroup,
|
||||
sessionTabComplete,
|
||||
sessionTabOverflowWidth,
|
||||
} from "../../src/context/session-tabs-model"
|
||||
|
||||
describe("session tabs", () => {
|
||||
test("orders running sessions first and completed sessions by recent update", () => {
|
||||
const tabs = ["old", "running-old", "new", "running-new"].map((sessionID) => ({ sessionID }))
|
||||
const state = {
|
||||
old: { busy: false, updated: 10 },
|
||||
"running-old": { busy: true, updated: 20 },
|
||||
new: { busy: false, updated: 40 },
|
||||
"running-new": { busy: true, updated: 30 },
|
||||
}
|
||||
|
||||
expect(orderSessionTabs(tabs, (sessionID) => state[sessionID as keyof typeof state]).map((tab) => tab.sessionID)).toEqual([
|
||||
"running-new",
|
||||
"running-old",
|
||||
"new",
|
||||
"old",
|
||||
])
|
||||
})
|
||||
|
||||
test("groups inbox tabs by running state and local calendar day", () => {
|
||||
const now = new Date(2026, 6, 31, 12).getTime()
|
||||
expect(sessionInboxGroup(new Date(2026, 6, 20).getTime(), true, now)).toBe("running")
|
||||
expect(sessionInboxGroup(new Date(2026, 6, 31, 1).getTime(), false, now)).toBe("today")
|
||||
expect(sessionInboxGroup(new Date(2026, 6, 30, 1).getTime(), false, now)).toBe("yesterday")
|
||||
expect(sessionInboxGroup(new Date(2026, 6, 29, 23).getTime(), false, now)).toBe("earlier")
|
||||
})
|
||||
|
||||
test("moves a tab to a clamped index and returns the same tabs for no-ops", () => {
|
||||
const tabs = ["a", "b", "c"].map((sessionID) => ({ sessionID }))
|
||||
expect(moveSessionTab(tabs, "a", 2).map((tab) => tab.sessionID)).toEqual(["b", "c", "a"])
|
||||
|
|
|
|||
|
|
@ -248,3 +248,33 @@ test("tracks a temporary new session tab across close and creation", async () =>
|
|||
setup.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("navigates the inbox without changing sessions and confirms done twice", async () => {
|
||||
const setup = await renderSessionTabs("first")
|
||||
|
||||
try {
|
||||
await wait(() => setup.tabs.current() === "first")
|
||||
setup.route.navigate({ type: "session", sessionID: "second" })
|
||||
await wait(() => setup.tabs.current() === "second" && setup.tabs.tabs().length === 2)
|
||||
setup.route.navigate({ type: "session", sessionID: "first" })
|
||||
await wait(() => setup.tabs.current() === "first")
|
||||
|
||||
expect(setup.tabs.navigation.focus()).toBe(true)
|
||||
expect(setup.tabs.navigation.selected()).toBe("first")
|
||||
setup.tabs.navigation.move(1)
|
||||
expect(setup.tabs.navigation.selected()).toBe("second")
|
||||
expect(setup.tabs.current()).toBe("first")
|
||||
|
||||
setup.tabs.navigation.done()
|
||||
expect(setup.tabs.navigation.pendingDone()).toBe("second")
|
||||
expect(setup.tabs.tabs().map((tab) => tab.sessionID)).toEqual(["first", "second"])
|
||||
setup.tabs.navigation.done()
|
||||
await wait(() => setup.tabs.tabs().length === 1)
|
||||
|
||||
expect(setup.tabs.tabs().map((tab) => tab.sessionID)).toEqual(["first"])
|
||||
expect(setup.tabs.current()).toBe("first")
|
||||
expect(setup.tabs.navigation.selected()).toBe("first")
|
||||
} finally {
|
||||
setup.destroy()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -139,3 +139,40 @@ test("global commands stay reachable when the mode changes", async () => {
|
|||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test("dispatches direct and leader tab-number bindings", async () => {
|
||||
const calls: number[] = []
|
||||
|
||||
function Harness() {
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "global",
|
||||
commands: Array.from({ length: 2 }, (_, index) => ({
|
||||
id: `session.tab.select.${index + 1}`,
|
||||
run: () => void calls.push(index + 1),
|
||||
})),
|
||||
}))
|
||||
Keymap.createLayer(() => ({
|
||||
mode: "global",
|
||||
bindings: ["session.tab.select.1", "session.tab.select.2"],
|
||||
}))
|
||||
return <box />
|
||||
}
|
||||
|
||||
const app = await testRender(() => (
|
||||
<ConfigProvider config={createTuiResolvedConfig()}>
|
||||
<Keymap.Provider>
|
||||
<Harness />
|
||||
</Keymap.Provider>
|
||||
</ConfigProvider>
|
||||
))
|
||||
try {
|
||||
app.mockInput.pressKey("1", { ctrl: true })
|
||||
expect(calls).toEqual([])
|
||||
app.mockInput.pressKey("1", { meta: true })
|
||||
app.mockInput.pressKey("x", { ctrl: true })
|
||||
app.mockInput.pressKey("2")
|
||||
expect(calls).toEqual([1, 2])
|
||||
} finally {
|
||||
app.renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
|
|
|||
9
packages/tui/test/util/activity-verb.test.ts
Normal file
9
packages/tui/test/util/activity-verb.test.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { ACTIVITY_VERBS, activityVerb } from "../../src/util/activity-verb"
|
||||
|
||||
test("rotates through 60 stable activity verbs", () => {
|
||||
expect(ACTIVITY_VERBS).toHaveLength(60)
|
||||
expect(new Set(ACTIVITY_VERBS).size).toBe(60)
|
||||
expect(activityVerb("session-a", 0)).toBe(activityVerb("session-a", 60))
|
||||
expect(activityVerb("session-a", 1)).not.toBe(activityVerb("session-a", 0))
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue