footer-keymap.test.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. /** @jsxImportSource @opentui/solid */
  2. import { testRender } from "@opentui/solid"
  3. import { Keymap } from "../../src/context/keymap"
  4. import { resolve } from "../../src/config"
  5. import { expect, test } from "bun:test"
  6. import { createSignal } from "solid-js"
  7. import { RunFooterView } from "../../src/mini/footer.view"
  8. import { RUN_THEME_FALLBACK } from "../../src/mini/theme"
  9. import type { FooterState, FooterSubagentState, FooterView } from "../../src/mini/types"
  10. async function renderSubagent(interrupt: "ctrl+i" | "none") {
  11. const [state] = createSignal<FooterState>({
  12. phase: "idle",
  13. status: "",
  14. notice: "",
  15. model: "gpt-5",
  16. usage: "",
  17. first: false,
  18. interrupt: 0,
  19. exit: 0,
  20. })
  21. const [view] = createSignal<FooterView>({ type: "prompt" })
  22. const [subagents] = createSignal<FooterSubagentState>({
  23. tabs: [
  24. {
  25. sessionID: "subagent-1",
  26. label: "Explore",
  27. description: "Inspect the keymap",
  28. status: "running",
  29. },
  30. ],
  31. details: {},
  32. permissions: [],
  33. forms: [],
  34. })
  35. const config = resolve(
  36. {
  37. keybinds: {
  38. "prompt.editor": "none",
  39. "session.queued_prompts": "none",
  40. "composer.subagent.interrupt": interrupt,
  41. },
  42. },
  43. { terminalSuspend: true },
  44. )
  45. const interrupted: string[] = []
  46. function Harness() {
  47. return (
  48. <Keymap.Provider config={config}>
  49. <RunFooterView
  50. directory={() => "/tmp"}
  51. findFiles={async () => []}
  52. agents={() => []}
  53. references={() => []}
  54. commands={() => []}
  55. providers={() => undefined}
  56. currentAgent={() => "Build"}
  57. currentAgentID={() => "build"}
  58. currentAgentExplicit={() => false}
  59. currentModel={() => undefined}
  60. variants={() => []}
  61. currentVariant={() => undefined}
  62. state={state}
  63. view={view}
  64. subagent={subagents}
  65. theme={() => RUN_THEME_FALLBACK}
  66. miniSettings={() => ({
  67. thinking: "hide",
  68. shell_output: "hide",
  69. turn_summary: "show",
  70. footer: "show",
  71. splash: "show",
  72. mono: false,
  73. })}
  74. mono={false}
  75. onSubmit={() => true}
  76. onPermissionReply={() => {}}
  77. onFormReply={() => {}}
  78. onFormCancel={() => {}}
  79. onCycle={() => {}}
  80. onInterrupt={() => false}
  81. onEditorOpen={async () => undefined}
  82. onInputClear={() => {}}
  83. onExit={() => {}}
  84. onAgentSelect={() => {}}
  85. onModelSelect={() => {}}
  86. onVariantSelect={() => {}}
  87. onRows={() => {}}
  88. onLayout={() => {}}
  89. onStatus={() => {}}
  90. onMiniSettingChange={() => {}}
  91. onSubagentInterrupt={(sessionID) => interrupted.push(sessionID)}
  92. />
  93. </Keymap.Provider>
  94. )
  95. }
  96. const app = await testRender(() => <Harness />, { width: 100, height: 8, kittyKeyboard: true })
  97. return { app, interrupted }
  98. }
  99. async function openSubagent(app: Awaited<ReturnType<typeof testRender>>) {
  100. await app.renderOnce()
  101. expect(app.renderer.currentFocusedEditor?.plainText).toBe("")
  102. app.mockInput.pressArrow("down")
  103. await app.renderOnce()
  104. expect(app.captureCharFrame()).toContain("Select subagent")
  105. app.mockInput.pressEnter()
  106. await app.renderOnce()
  107. }
  108. test("configured subagent key updates its hint and action", async () => {
  109. const { app, interrupted } = await renderSubagent("ctrl+i")
  110. try {
  111. await openSubagent(app)
  112. expect(app.captureCharFrame()).toContain("ctrl+i")
  113. app.mockInput.pressKey("i", { ctrl: true })
  114. expect(interrupted).toEqual(["subagent-1"])
  115. } finally {
  116. app.renderer.currentFocusedRenderable?.blur()
  117. app.renderer.currentFocusedEditor?.blur()
  118. app.renderer.destroy()
  119. }
  120. })
  121. test("disabled subagent interrupt has no component fallback", async () => {
  122. const { app, interrupted } = await renderSubagent("none")
  123. try {
  124. await openSubagent(app)
  125. expect(app.captureCharFrame()).not.toContain("ctrl+d")
  126. app.mockInput.pressKey("d", { ctrl: true })
  127. expect(interrupted).toEqual([])
  128. } finally {
  129. app.renderer.currentFocusedRenderable?.blur()
  130. app.renderer.currentFocusedEditor?.blur()
  131. app.renderer.destroy()
  132. }
  133. })