@patze/code-cli 0.43.3 → 0.55.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +238 -0
  2. package/README.md +84 -57
  3. package/VERSION +1 -1
  4. package/dist/backend/auth-token-kind.d.ts +3 -0
  5. package/dist/backend/auth-token-kind.d.ts.map +1 -0
  6. package/dist/backend/auth-token-kind.js +6 -0
  7. package/dist/backend/auth-token-kind.js.map +1 -0
  8. package/dist/backend/client.d.ts.map +1 -1
  9. package/dist/backend/client.js +7 -1
  10. package/dist/backend/client.js.map +1 -1
  11. package/dist/backend/execute-client.d.ts +1 -1
  12. package/dist/backend/execute-client.d.ts.map +1 -1
  13. package/dist/backend/execute-client.js +26 -0
  14. package/dist/backend/execute-client.js.map +1 -1
  15. package/dist/backend/plugin-session-client.d.ts +33 -0
  16. package/dist/backend/plugin-session-client.d.ts.map +1 -0
  17. package/dist/backend/plugin-session-client.js +108 -0
  18. package/dist/backend/plugin-session-client.js.map +1 -0
  19. package/dist/backend/runs-client.d.ts.map +1 -1
  20. package/dist/backend/runs-client.js +2 -1
  21. package/dist/backend/runs-client.js.map +1 -1
  22. package/dist/cli/auth/pairing-login.d.ts +10 -0
  23. package/dist/cli/auth/pairing-login.d.ts.map +1 -0
  24. package/dist/cli/auth/pairing-login.js +78 -0
  25. package/dist/cli/auth/pairing-login.js.map +1 -0
  26. package/dist/cli/commands/doctor.d.ts.map +1 -1
  27. package/dist/cli/commands/doctor.js +10 -4
  28. package/dist/cli/commands/doctor.js.map +1 -1
  29. package/dist/cli/commands/exec.d.ts.map +1 -1
  30. package/dist/cli/commands/exec.js +33 -8
  31. package/dist/cli/commands/exec.js.map +1 -1
  32. package/dist/cli/commands/login.d.ts +1 -0
  33. package/dist/cli/commands/login.d.ts.map +1 -1
  34. package/dist/cli/commands/login.js +36 -5
  35. package/dist/cli/commands/login.js.map +1 -1
  36. package/dist/cli/commands/runs-format.d.ts.map +1 -1
  37. package/dist/cli/commands/runs-format.js +2 -1
  38. package/dist/cli/commands/runs-format.js.map +1 -1
  39. package/dist/cli/commands/runs.d.ts.map +1 -1
  40. package/dist/cli/commands/runs.js +63 -5
  41. package/dist/cli/commands/runs.js.map +1 -1
  42. package/dist/cli/commands/status-format.d.ts.map +1 -1
  43. package/dist/cli/commands/status-format.js +4 -3
  44. package/dist/cli/commands/status-format.js.map +1 -1
  45. package/dist/cli/doctor/e2e-readiness.d.ts +12 -0
  46. package/dist/cli/doctor/e2e-readiness.d.ts.map +1 -0
  47. package/dist/cli/doctor/e2e-readiness.js +95 -0
  48. package/dist/cli/doctor/e2e-readiness.js.map +1 -0
  49. package/dist/cli/help.d.ts.map +1 -1
  50. package/dist/cli/help.js +2 -0
  51. package/dist/cli/help.js.map +1 -1
  52. package/dist/cli/interactive/agent-execute-turn.d.ts.map +1 -1
  53. package/dist/cli/interactive/agent-execute-turn.js +3 -1
  54. package/dist/cli/interactive/agent-execute-turn.js.map +1 -1
  55. package/dist/cli/interactive/agent-turn-context.d.ts +2 -2
  56. package/dist/cli/interactive/agent-turn-context.d.ts.map +1 -1
  57. package/dist/cli/interactive/agent-turn-context.js +23 -8
  58. package/dist/cli/interactive/agent-turn-context.js.map +1 -1
  59. package/dist/cli/interactive/agent-turn.d.ts.map +1 -1
  60. package/dist/cli/interactive/agent-turn.js +8 -7
  61. package/dist/cli/interactive/agent-turn.js.map +1 -1
  62. package/dist/cli/interactive/auth-credential-resolve.d.ts +20 -0
  63. package/dist/cli/interactive/auth-credential-resolve.d.ts.map +1 -0
  64. package/dist/cli/interactive/auth-credential-resolve.js +65 -0
  65. package/dist/cli/interactive/auth-credential-resolve.js.map +1 -0
  66. package/dist/cli/interactive/auth-gate.d.ts +1 -1
  67. package/dist/cli/interactive/auth-gate.d.ts.map +1 -1
  68. package/dist/cli/interactive/auth-gate.js +12 -3
  69. package/dist/cli/interactive/auth-gate.js.map +1 -1
  70. package/dist/cli/interactive/auth-readiness.d.ts +16 -0
  71. package/dist/cli/interactive/auth-readiness.d.ts.map +1 -0
  72. package/dist/cli/interactive/auth-readiness.js +50 -0
  73. package/dist/cli/interactive/auth-readiness.js.map +1 -0
  74. package/dist/cli/interactive/codex-feed-writer.d.ts.map +1 -1
  75. package/dist/cli/interactive/codex-feed-writer.js +4 -0
  76. package/dist/cli/interactive/codex-feed-writer.js.map +1 -1
  77. package/dist/cli/interactive/composer-chrome.d.ts +1 -1
  78. package/dist/cli/interactive/composer-chrome.d.ts.map +1 -1
  79. package/dist/cli/interactive/composer-chrome.js +1 -1
  80. package/dist/cli/interactive/composer-chrome.js.map +1 -1
  81. package/dist/cli/interactive/composer-edit-keys.d.ts +21 -0
  82. package/dist/cli/interactive/composer-edit-keys.d.ts.map +1 -0
  83. package/dist/cli/interactive/composer-edit-keys.js +57 -0
  84. package/dist/cli/interactive/composer-edit-keys.js.map +1 -0
  85. package/dist/cli/interactive/ensure-execute-auth.d.ts.map +1 -1
  86. package/dist/cli/interactive/ensure-execute-auth.js +8 -13
  87. package/dist/cli/interactive/ensure-execute-auth.js.map +1 -1
  88. package/dist/cli/interactive/header-refresh-triggers.d.ts +3 -0
  89. package/dist/cli/interactive/header-refresh-triggers.d.ts.map +1 -0
  90. package/dist/cli/interactive/header-refresh-triggers.js +16 -0
  91. package/dist/cli/interactive/header-refresh-triggers.js.map +1 -0
  92. package/dist/cli/interactive/header-refresh.d.ts +4 -0
  93. package/dist/cli/interactive/header-refresh.d.ts.map +1 -0
  94. package/dist/cli/interactive/header-refresh.js +33 -0
  95. package/dist/cli/interactive/header-refresh.js.map +1 -0
  96. package/dist/cli/interactive/header.d.ts +3 -2
  97. package/dist/cli/interactive/header.d.ts.map +1 -1
  98. package/dist/cli/interactive/header.js +23 -14
  99. package/dist/cli/interactive/header.js.map +1 -1
  100. package/dist/cli/interactive/interactive-footer-hints.d.ts +12 -0
  101. package/dist/cli/interactive/interactive-footer-hints.d.ts.map +1 -0
  102. package/dist/cli/interactive/interactive-footer-hints.js +32 -0
  103. package/dist/cli/interactive/interactive-footer-hints.js.map +1 -0
  104. package/dist/cli/interactive/interactive-model-apply.d.ts +15 -0
  105. package/dist/cli/interactive/interactive-model-apply.d.ts.map +1 -0
  106. package/dist/cli/interactive/interactive-model-apply.js +28 -0
  107. package/dist/cli/interactive/interactive-model-apply.js.map +1 -0
  108. package/dist/cli/interactive/interactive-ui.d.ts +13 -0
  109. package/dist/cli/interactive/interactive-ui.d.ts.map +1 -0
  110. package/dist/cli/interactive/interactive-ui.js +64 -0
  111. package/dist/cli/interactive/interactive-ui.js.map +1 -0
  112. package/dist/cli/interactive/launcher.d.ts.map +1 -1
  113. package/dist/cli/interactive/launcher.js +24 -17
  114. package/dist/cli/interactive/launcher.js.map +1 -1
  115. package/dist/cli/interactive/line-editor.d.ts +5 -4
  116. package/dist/cli/interactive/line-editor.d.ts.map +1 -1
  117. package/dist/cli/interactive/line-editor.js +102 -30
  118. package/dist/cli/interactive/line-editor.js.map +1 -1
  119. package/dist/cli/interactive/model-catalog-resolve.d.ts +11 -0
  120. package/dist/cli/interactive/model-catalog-resolve.d.ts.map +1 -0
  121. package/dist/cli/interactive/model-catalog-resolve.js +49 -0
  122. package/dist/cli/interactive/model-catalog-resolve.js.map +1 -0
  123. package/dist/cli/interactive/model-picker-panel.d.ts +22 -2
  124. package/dist/cli/interactive/model-picker-panel.d.ts.map +1 -1
  125. package/dist/cli/interactive/model-picker-panel.js +99 -16
  126. package/dist/cli/interactive/model-picker-panel.js.map +1 -1
  127. package/dist/cli/interactive/model-selector.d.ts.map +1 -1
  128. package/dist/cli/interactive/model-selector.js +3 -2
  129. package/dist/cli/interactive/model-selector.js.map +1 -1
  130. package/dist/cli/interactive/model-tty-picker.d.ts +3 -1
  131. package/dist/cli/interactive/model-tty-picker.d.ts.map +1 -1
  132. package/dist/cli/interactive/model-tty-picker.js +154 -24
  133. package/dist/cli/interactive/model-tty-picker.js.map +1 -1
  134. package/dist/cli/interactive/plain-codex-feed.d.ts.map +1 -1
  135. package/dist/cli/interactive/plain-codex-feed.js +48 -0
  136. package/dist/cli/interactive/plain-codex-feed.js.map +1 -1
  137. package/dist/cli/interactive/run-cancel.d.ts +23 -0
  138. package/dist/cli/interactive/run-cancel.d.ts.map +1 -0
  139. package/dist/cli/interactive/run-cancel.js +67 -0
  140. package/dist/cli/interactive/run-cancel.js.map +1 -0
  141. package/dist/cli/interactive/select-list-nav.d.ts +10 -0
  142. package/dist/cli/interactive/select-list-nav.d.ts.map +1 -0
  143. package/dist/cli/interactive/select-list-nav.js +45 -0
  144. package/dist/cli/interactive/select-list-nav.js.map +1 -0
  145. package/dist/cli/interactive/session-controller.d.ts +1 -0
  146. package/dist/cli/interactive/session-controller.d.ts.map +1 -1
  147. package/dist/cli/interactive/session-controller.js +5 -1
  148. package/dist/cli/interactive/session-controller.js.map +1 -1
  149. package/dist/cli/interactive/session-hints.d.ts +4 -1
  150. package/dist/cli/interactive/session-hints.d.ts.map +1 -1
  151. package/dist/cli/interactive/session-hints.js +23 -16
  152. package/dist/cli/interactive/session-hints.js.map +1 -1
  153. package/dist/cli/interactive/shell.d.ts.map +1 -1
  154. package/dist/cli/interactive/shell.js +17 -7
  155. package/dist/cli/interactive/shell.js.map +1 -1
  156. package/dist/cli/interactive/slash-dispatch.d.ts +2 -0
  157. package/dist/cli/interactive/slash-dispatch.d.ts.map +1 -1
  158. package/dist/cli/interactive/slash-dispatch.js +55 -28
  159. package/dist/cli/interactive/slash-dispatch.js.map +1 -1
  160. package/dist/cli/interactive/slash-menu.d.ts.map +1 -1
  161. package/dist/cli/interactive/slash-menu.js +29 -2
  162. package/dist/cli/interactive/slash-menu.js.map +1 -1
  163. package/dist/cli/interactive/slash-registry.js +2 -2
  164. package/dist/cli/interactive/slash-registry.js.map +1 -1
  165. package/dist/cli/interactive/smoke-trust-fixture.d.ts +10 -0
  166. package/dist/cli/interactive/smoke-trust-fixture.d.ts.map +1 -0
  167. package/dist/cli/interactive/smoke-trust-fixture.js +66 -0
  168. package/dist/cli/interactive/smoke-trust-fixture.js.map +1 -0
  169. package/dist/cli/interactive/tools-catalog.d.ts.map +1 -1
  170. package/dist/cli/interactive/tools-catalog.js +17 -12
  171. package/dist/cli/interactive/tools-catalog.js.map +1 -1
  172. package/dist/config/config.d.ts +2 -0
  173. package/dist/config/config.d.ts.map +1 -1
  174. package/dist/config/config.js +8 -0
  175. package/dist/config/config.js.map +1 -1
  176. package/dist/config/web-ui.d.ts +5 -0
  177. package/dist/config/web-ui.d.ts.map +1 -0
  178. package/dist/config/web-ui.js +12 -0
  179. package/dist/config/web-ui.js.map +1 -0
  180. package/docs/beta-release-handoff.md +56 -29
  181. package/docs/internal-beta.md +6 -3
  182. package/docs/known-limitations.md +32 -19
  183. package/docs/release-checklist.md +11 -3
  184. package/opentui/src/App.tsx +626 -218
  185. package/package.json +6 -2
@@ -24,14 +24,34 @@ import { classifyPlainOutput, createTranscriptSink, inferErrorKind } from "./tui
24
24
  import { upsertTranscriptEntry } from "../../dist/cli/interactive/transcript-upsert.js"
25
25
  import { applyOpenTuiAgentEvent } from "../../dist/cli/interactive/opentui-agent-event.js"
26
26
  import type { AgentEvent } from "../../dist/cli/interactive/agent-events.js"
27
- import {
28
- dedupeModelIds,
29
- formatModelDisplayName,
30
- getModelCatalogEntry,
31
- isPatzeDefaultModel,
27
+ import { formatModelDisplayName,
32
28
  PATZE_MODEL_CATALOG,
29
+ type PatzeModelCatalogEntry,
33
30
  } from "../../dist/config/models.js"
34
- import { formatModelDisplay } from "../../dist/config/model-preferences.js"
31
+ import {
32
+ buildFilteredModelSelectOptions,
33
+ filterModelCatalogEntries,
34
+ formatCodexListItemLabel,
35
+ formatModelPickerPreferenceHint,
36
+ indexForFilteredCatalogEntry,
37
+ indexForReasoningLevel,
38
+ } from "../../dist/cli/interactive/model-picker-panel.js"
39
+ import { isComposerNewlineKey } from "../../dist/cli/interactive/composer-keys.js"
40
+ import { isHistorySafe } from "../../dist/cli/interactive/redact-input.js"
41
+ import {
42
+ getReasoningLevelsForModelId,
43
+ getDefaultReasoningLevel,
44
+ type ModelReasoningLevel,
45
+ } from "../../dist/config/model-reasoning.js"
46
+ import {
47
+ buildInteractiveFooterHint,
48
+ } from "../../dist/cli/interactive/interactive-footer-hints.js"
49
+ import {
50
+ applySimpleComposerBufferEdit,
51
+ jumpListSelection,
52
+ mapListNavKey,
53
+ moveListSelection,
54
+ } from "../../dist/cli/interactive/select-list-nav.js"
35
55
 
36
56
  extend({ "tui-input": InputRenderable })
37
57
 
@@ -47,12 +67,13 @@ function TuiInput(props: TuiInputProps) {
47
67
  return createElement("tui-input", props)
48
68
  }
49
69
 
50
- type ViewMode = "command" | "input" | "model"
70
+ type ViewMode = "input" | "model" | "model-reasoning"
51
71
 
52
72
  type CommandSelectItem = SelectOption & {
53
73
  name: string
54
74
  description: string
55
75
  value: string
76
+ commandName?: string
56
77
  }
57
78
 
58
79
  type ModelSelectItem = SelectOption & {
@@ -69,6 +90,8 @@ type InteractiveController = {
69
90
  setModelThinking: (value: boolean | null) => void
70
91
  getModelFast: () => boolean | null
71
92
  setModelFast: (value: boolean | null) => void
93
+ getModelReasoningLevel: () => ModelReasoningLevel | null
94
+ setModelReasoningLevel: (value: ModelReasoningLevel | null) => void
72
95
  getExecutionMode: () => "local" | "cloud"
73
96
  snapshot: () => Array<{ input: string; lines: string[] }>
74
97
  }
@@ -106,6 +129,15 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
106
129
  const streamingIdRef = useRef<string | null>(null)
107
130
  const scrollPinnedRef = useRef(true)
108
131
  const activeAbortRef = useRef<AbortController | null>(null)
132
+ const inputHistoryRef = useRef<string[]>([])
133
+ const historyIndexRef = useRef(-1)
134
+ const stashedInputRef = useRef("")
135
+ const commandSelectIndexRef = useRef(0)
136
+ const [commandSelectIndex, setCommandSelectIndex] = useState(0)
137
+ const [modelSelectIndex, setModelSelectIndex] = useState(0)
138
+ const [modelSearch, setModelSearch] = useState("")
139
+ const [reasoningSelectIndex, setReasoningSelectIndex] = useState(0)
140
+ const [menuDismissed, setMenuDismissed] = useState(false)
109
141
 
110
142
  const [ready, setReady] = useState(false)
111
143
  const [bootError, setBootError] = useState<string | null>(null)
@@ -114,18 +146,21 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
114
146
  const [activityFrame, setActivityFrame] = useState(0)
115
147
  const [input, setInput] = useState("")
116
148
  const [mode, setMode] = useState<ViewMode>("input")
117
- const [modelSearch, setModelSearch] = useState("")
118
149
  const [scrollOffset, setScrollOffset] = useState(0)
119
150
  const [transcript, setTranscript] = useState<TranscriptEntry[]>([])
120
151
  const [currentModel, setCurrentModel] = useState("composer-2.5-fast")
121
152
  const [modelThinking, setModelThinking] = useState<boolean | null>(null)
122
153
  const [modelFast, setModelFast] = useState<boolean | null>(null)
154
+ const [modelReasoningLevel, setModelReasoningLevel] = useState<ModelReasoningLevel | null>(null)
155
+ const [pendingModelEntry, setPendingModelEntry] = useState<PatzeModelCatalogEntry | null>(null)
123
156
  const [executionMode, setExecutionMode] = useState<"local" | "cloud">("local")
124
157
  const [executionTarget, setExecutionTarget] = useState<string | null>(null)
125
- const [supportedModels, setSupportedModels] = useState<string[]>([])
158
+ const [pickerCatalog, setPickerCatalog] = useState<PatzeModelCatalogEntry[]>([
159
+ ...PATZE_MODEL_CATALOG,
160
+ ])
126
161
  const [slashMenuReady, setSlashMenuReady] = useState(false)
127
162
  const [startupHeader, setStartupHeader] = useState<string[]>([])
128
- const [composerPlaceholder, setComposerPlaceholder] = useState("Describe a task… · / for commands")
163
+ const [composerPlaceholder, setComposerPlaceholder] = useState("")
129
164
  const [emptySessionHints, setEmptySessionHints] = useState<string[]>([])
130
165
 
131
166
  const followTranscript = useCallback(() => {
@@ -143,6 +178,53 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
143
178
  setScrollOffset(0)
144
179
  }, [])
145
180
 
181
+ const resetInputHistoryNav = useCallback(() => {
182
+ historyIndexRef.current = -1
183
+ stashedInputRef.current = ""
184
+ }, [])
185
+
186
+ const pushInputHistory = useCallback((value: string) => {
187
+ if (!isHistorySafe(value)) {
188
+ return
189
+ }
190
+ const history = inputHistoryRef.current
191
+ if (history[history.length - 1] !== value) {
192
+ inputHistoryRef.current = [...history, value]
193
+ }
194
+ resetInputHistoryNav()
195
+ }, [resetInputHistoryNav])
196
+
197
+ const historyPrev = useCallback((): string | null => {
198
+ const history = inputHistoryRef.current
199
+ if (!history.length) {
200
+ return null
201
+ }
202
+ if (historyIndexRef.current === -1) {
203
+ stashedInputRef.current = input
204
+ historyIndexRef.current = history.length - 1
205
+ } else {
206
+ historyIndexRef.current = Math.max(0, historyIndexRef.current - 1)
207
+ }
208
+ return history[historyIndexRef.current] ?? null
209
+ }, [input])
210
+
211
+ const historyNext = useCallback((): string | null => {
212
+ if (historyIndexRef.current === -1) {
213
+ return null
214
+ }
215
+ const history = inputHistoryRef.current
216
+ if (historyIndexRef.current >= history.length - 1) {
217
+ historyIndexRef.current = -1
218
+ return stashedInputRef.current
219
+ }
220
+ historyIndexRef.current += 1
221
+ return history[historyIndexRef.current] ?? null
222
+ }, [])
223
+
224
+ const transcriptScrollActive = useCallback(() => {
225
+ return !scrollPinnedRef.current || scrollOffset > 0
226
+ }, [scrollOffset])
227
+
146
228
  const nextId = useCallback(() => {
147
229
  const id = nextIdRef.current
148
230
  nextIdRef.current += 1
@@ -239,24 +321,12 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
239
321
  setExecutionMode(controller.session.getExecutionMode())
240
322
  setModelThinking(controller.session.getModelThinking())
241
323
  setModelFast(controller.session.getModelFast())
324
+ setModelReasoningLevel(controller.session.getModelReasoningLevel())
242
325
 
243
- let modelIds = [...models.PATZE_SUPPORTED_MODELS]
244
- const apiKey = process.env.CURSOR_API_KEY?.trim()
245
- if (apiKey) {
246
- try {
247
- const { Cursor } = await import("@cursor/sdk")
248
- const listed = await Cursor.models.list({ apiKey })
249
- const ids = listed
250
- .map((item) => String(item.id ?? "").trim())
251
- .filter(Boolean)
252
- if (ids.length > 0) {
253
- modelIds = ids
254
- }
255
- } catch {
256
- // keep bundled fallback list
257
- }
258
- }
259
- setSupportedModels(modelIds)
326
+ const catalogResolve = await importPatzeDist<
327
+ typeof import("../../dist/cli/interactive/model-catalog-resolve.js")
328
+ >("cli/interactive/model-catalog-resolve.js")
329
+ setPickerCatalog(await catalogResolve.resolvePickerModelCatalog())
260
330
 
261
331
  const cloudGit = await importPatzeDist<
262
332
  typeof import("../../dist/cli/interactive/cloud-git.js")
@@ -278,7 +348,13 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
278
348
  const sessionHints = await importPatzeDist<
279
349
  typeof import("../../dist/cli/interactive/session-hints.js")
280
350
  >("cli/interactive/session-hints.js")
281
- setEmptySessionHints(sessionHints.buildEmptySessionHints(controller.cwd))
351
+ setEmptySessionHints(
352
+ sessionHints.shouldShowEmptySessionHints(controller.cwd)
353
+ ? sessionHints.buildEmptySessionHints(controller.cwd)
354
+ : []
355
+ )
356
+
357
+ setStartupHeader(controller.renderHeaderLines())
282
358
  }, [])
283
359
 
284
360
  useEffect(() => {
@@ -387,7 +463,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
387
463
  const result = await controller.processLine(value, sink, {
388
464
  signal: abort.signal,
389
465
  onActivity: setActivityLabel,
390
- useActivitySpinner: true,
466
+ useActivitySpinner: false,
391
467
  onAgentEvent: (event) => {
392
468
  setActivityLabel(null)
393
469
  setTranscript((items) =>
@@ -403,6 +479,9 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
403
479
  setTranscript([])
404
480
  markScrollPinned()
405
481
  }
482
+ if (result.refreshHeader) {
483
+ setStartupHeader(controller.renderHeaderLines())
484
+ }
406
485
  await refreshStatusMeta()
407
486
  if (result.exitShell) {
408
487
  exitApp()
@@ -430,32 +509,44 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
430
509
  )
431
510
 
432
511
  const openModelPicker = useCallback(() => {
512
+ setPendingModelEntry(null)
433
513
  setModelSearch("")
514
+ const filtered = filterModelCatalogEntries("", pickerCatalog)
515
+ setModelSelectIndex(indexForFilteredCatalogEntry(filtered, currentModel))
434
516
  setMode("model")
435
- }, [])
517
+ }, [currentModel, pickerCatalog])
436
518
 
437
- const toggleModelPreference = useCallback((kind: "thinking" | "fast") => {
438
- const controller = controllerRef.current
439
- if (!controller) {
440
- return
441
- }
442
- if (kind === "thinking") {
443
- const next = controller.session.getModelThinking() === true ? null : true
444
- controller.session.setModelThinking(next)
445
- setModelThinking(next)
446
- } else {
447
- const next = controller.session.getModelFast() === true ? null : true
448
- controller.session.setModelFast(next)
449
- setModelFast(next)
450
- }
451
- controller.persistSession()
452
- }, [])
519
+ const toggleModelPickerPreference = useCallback(
520
+ (preference: "thinking" | "fast") => {
521
+ const controller = controllerRef.current
522
+ if (!controller) {
523
+ return
524
+ }
525
+ void (async () => {
526
+ const prefs = await importPatzeDist<
527
+ typeof import("../../dist/config/model-preferences.js")
528
+ >("config/model-preferences.js")
529
+ const current = preference === "thinking" ? modelThinking : modelFast
530
+ const next = prefs.resolveModelPreferenceToggle(current, "toggle")
531
+ if (preference === "thinking") {
532
+ controller.session.setModelThinking(next)
533
+ setModelThinking(next)
534
+ } else {
535
+ controller.session.setModelFast(next)
536
+ setModelFast(next)
537
+ }
538
+ controller.persistSession()
539
+ })()
540
+ },
541
+ [modelFast, modelThinking]
542
+ )
453
543
 
454
544
  const runCommand = useCallback(
455
545
  async (rawCommand: string) => {
456
546
  const trimmed = rawCommand.trim()
457
547
  setMode("input")
458
548
  setInput("")
549
+ setMenuDismissed(false)
459
550
 
460
551
  if (!trimmed) {
461
552
  return
@@ -492,7 +583,30 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
492
583
  const submitInput = useCallback(
493
584
  (value: string) => {
494
585
  const prompt = trimComposerPrompt(value)
586
+
587
+ if (
588
+ prompt.startsWith("/") &&
589
+ !prompt.includes(" ") &&
590
+ !menuDismissed &&
591
+ slashMenuModule &&
592
+ commandMenu.items.length > 0
593
+ ) {
594
+ const command = commandMenu.items[commandSelectIndexRef.current]
595
+ if (command) {
596
+ setInput("")
597
+ resetInputHistoryNav()
598
+ if (slashMenuModule.commandRequiresArg(command)) {
599
+ setInput(`/${command.name} `)
600
+ setMenuDismissed(false)
601
+ return
602
+ }
603
+ void runCommand(`/${command.name}`)
604
+ return
605
+ }
606
+ }
607
+
495
608
  setInput("")
609
+ resetInputHistoryNav()
496
610
 
497
611
  if (!prompt || busy) {
498
612
  return
@@ -503,9 +617,19 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
503
617
  return
504
618
  }
505
619
 
620
+ pushInputHistory(prompt)
506
621
  void submitToController(prompt)
507
622
  },
508
- [busy, runCommand, submitToController]
623
+ [
624
+ busy,
625
+ commandMenu.items,
626
+ menuDismissed,
627
+ pushInputHistory,
628
+ resetInputHistoryNav,
629
+ runCommand,
630
+ slashMenuModule,
631
+ submitToController,
632
+ ]
509
633
  )
510
634
 
511
635
  const selectModel = useCallback(
@@ -515,46 +639,116 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
515
639
  return
516
640
  }
517
641
 
518
- const modelCommand = await importPatzeDist<
519
- typeof import("../../dist/cli/interactive/model-command.js")
520
- >("cli/interactive/model-command.js")
642
+ const modelApply = await importPatzeDist<
643
+ typeof import("../../dist/cli/interactive/interactive-model-apply.js")
644
+ >("cli/interactive/interactive-model-apply.js")
645
+
646
+ const catalogEntry =
647
+ pickerCatalog.find((entry) => entry.id.toLowerCase() === modelId.toLowerCase()) ?? null
648
+
649
+ const result = modelApply.applyInteractiveModelChoice(
650
+ controller.session as never,
651
+ modelId,
652
+ undefined,
653
+ catalogEntry
654
+ )
655
+ if (result.pendingReasoning) {
656
+ setPendingModelEntry(result.pendingReasoning)
657
+ setReasoningSelectIndex(
658
+ indexForReasoningLevel(
659
+ getReasoningLevelsForModelId(result.pendingReasoning.id),
660
+ currentModel.toLowerCase() === result.pendingReasoning.id.toLowerCase()
661
+ ? modelReasoningLevel
662
+ : null,
663
+ result.pendingReasoning.id
664
+ )
665
+ )
666
+ setMode("model-reasoning")
667
+ return
668
+ }
521
669
 
522
- const result = modelCommand.dispatchModelCommand(controller.session as never, modelId)
523
670
  for (const line of result.lines) {
524
671
  addEntry(inferErrorKind(line), "model", line)
525
672
  }
673
+ controller.persistSession()
674
+ setStartupHeader(controller.renderHeaderLines())
526
675
  await refreshStatusMeta()
527
676
  setMode("input")
528
677
  },
529
- [addEntry, refreshStatusMeta]
678
+ [addEntry, currentModel, modelReasoningLevel, pickerCatalog, refreshStatusMeta]
679
+ )
680
+
681
+ const selectReasoning = useCallback(
682
+ async (level: ModelReasoningLevel) => {
683
+ const controller = controllerRef.current
684
+ if (!controller || !pendingModelEntry) {
685
+ return
686
+ }
687
+
688
+ const modelApply = await importPatzeDist<
689
+ typeof import("../../dist/cli/interactive/interactive-model-apply.js")
690
+ >("cli/interactive/interactive-model-apply.js")
691
+
692
+ const result = modelApply.applyInteractiveModelReasoning(
693
+ controller.session as never,
694
+ pendingModelEntry,
695
+ level
696
+ )
697
+ for (const line of result.lines) {
698
+ addEntry(inferErrorKind(line), "model", line)
699
+ }
700
+ controller.persistSession()
701
+ setPendingModelEntry(null)
702
+ setStartupHeader(controller.renderHeaderLines())
703
+ await refreshStatusMeta()
704
+ setMode("input")
705
+ },
706
+ [addEntry, pendingModelEntry, refreshStatusMeta]
530
707
  )
531
708
 
532
709
  const commandSelectRows = Math.min(6, Math.max(3, rows - 10))
533
710
  const modelSelectRows = Math.min(8, Math.max(3, rows - 10))
534
711
  const commandPanelRows = 2 + commandSelectRows
535
- const modelPanelRows = 3 + modelSelectRows
712
+ const modelPanelRows = 4 + modelSelectRows
536
713
  const headerRows = Math.max(1, startupHeader.length)
537
- const composerChromeRows = 5
714
+ const composerChromeRows = 4
538
715
  const bottomHintRows = 1
539
716
 
717
+ const commandMenu = useMemo(() => {
718
+ if (!slashMenuReady || !slashMenuModule || !input.startsWith("/") || input.includes(" ") || menuDismissed) {
719
+ return { items: [], advancedCount: 0 }
720
+ }
721
+ return slashMenuModule.computeSlashMenuItems(input)
722
+ }, [input, menuDismissed, slashMenuReady])
723
+
724
+ const commandItems = useMemo(() => {
725
+ return commandMenu.items.map((command) => ({
726
+ name: `/${command.name}`,
727
+ description: command.description,
728
+ value: `/${command.name}`,
729
+ commandName: command.name,
730
+ }))
731
+ }, [commandMenu.items])
732
+
733
+ const slashPaletteOpen =
734
+ mode === "input" &&
735
+ input.startsWith("/") &&
736
+ !input.includes(" ") &&
737
+ !menuDismissed &&
738
+ commandMenu.items.length > 0
739
+
540
740
  const transcriptViewportRows = Math.max(
541
741
  4,
542
742
  rows -
543
743
  headerRows -
544
744
  bottomHintRows -
545
- (mode === "model"
745
+ (mode === "model-reasoning"
546
746
  ? modelPanelRows + 2
547
- : mode === "command"
548
- ? commandPanelRows + 2
549
- : composerChromeRows)
747
+ : mode === "model"
748
+ ? modelPanelRows + 2
749
+ : composerChromeRows + (slashPaletteOpen ? commandPanelRows + 2 : 0))
550
750
  )
551
751
 
552
- const composerModelLabel = formatModelDisplay(currentModel, {
553
- thinking: modelThinking,
554
- fast: modelFast,
555
- })
556
- const composerPathLabel = shortenComposerPath(cwd)
557
-
558
752
  const scrollableEntries = useMemo(() => transcript, [transcript])
559
753
 
560
754
  const transcriptLines = useMemo(
@@ -575,40 +769,53 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
575
769
  setScrollOffset((offset) => Math.min(offset, maxScrollOffset))
576
770
  }, [maxScrollOffset])
577
771
 
578
- const commandItems = useMemo(() => {
579
- if (!slashMenuReady || !input.startsWith("/") || input.includes(" ")) {
772
+ useEffect(() => {
773
+ setCommandSelectIndex(0)
774
+ commandSelectIndexRef.current = 0
775
+ }, [commandMenu.items])
776
+
777
+ const modelItems = useMemo(
778
+ () => buildFilteredModelSelectOptions(currentModel, modelSearch, pickerCatalog),
779
+ [currentModel, modelSearch, pickerCatalog]
780
+ )
781
+
782
+ useEffect(() => {
783
+ if (mode !== "model") {
784
+ return
785
+ }
786
+ setModelSelectIndex((index) => {
787
+ if (modelItems.length === 0) {
788
+ return 0
789
+ }
790
+ return Math.min(index, modelItems.length - 1)
791
+ })
792
+ }, [mode, modelItems.length, modelSearch])
793
+
794
+ const reasoningItems = useMemo(() => {
795
+ if (!pendingModelEntry) {
580
796
  return []
581
797
  }
582
- return getCommandItems(input)
583
- }, [input, slashMenuReady])
584
-
585
- const modelItems = useMemo(() => {
586
- const catalogIds = PATZE_MODEL_CATALOG.map((entry) => entry.id)
587
- const merged = dedupeModelIds([...catalogIds, ...supportedModels])
588
- const normalized = modelSearch.trim().toLowerCase()
589
- return merged
590
- .filter((model) => {
591
- if (!normalized) {
592
- return true
593
- }
594
- const display = formatModelDisplayName(model).toLowerCase()
595
- if (model.toLowerCase().includes(normalized) || display.includes(normalized)) {
596
- return true
597
- }
598
- if (normalized === "patze" && isPatzeDefaultModel(model)) {
599
- return true
600
- }
601
- return false
602
- })
603
- .map((model) => {
604
- const entry = getModelCatalogEntry(model)
605
- return {
606
- name: formatModelDisplayName(model),
607
- description: entry?.description ?? "",
608
- value: model,
609
- }
610
- })
611
- }, [currentModel, modelSearch, supportedModels])
798
+ const levels = getReasoningLevelsForModelId(pendingModelEntry.id)
799
+ const currentLevel =
800
+ pendingModelEntry.id.toLowerCase() === currentModel.toLowerCase()
801
+ ? modelReasoningLevel
802
+ : null
803
+ const highlightLevel = currentLevel ?? getDefaultReasoningLevel(pendingModelEntry.id)
804
+ const defaultLevel = getDefaultReasoningLevel(pendingModelEntry.id)
805
+ return levels.map((option) => {
806
+ const item = {
807
+ label: option.label,
808
+ description: option.description,
809
+ isDefault: option.id === defaultLevel,
810
+ isCurrent: option.id === highlightLevel,
811
+ }
812
+ return {
813
+ name: formatCodexListItemLabel(item),
814
+ description: option.description,
815
+ value: option.id,
816
+ }
817
+ })
818
+ }, [currentModel, modelReasoningLevel, pendingModelEntry])
612
819
 
613
820
  const toggleFeedExpand = useCallback((target: "think" | "diff") => {
614
821
  setTranscript((items) => {
@@ -631,21 +838,25 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
631
838
  }, [followTranscript])
632
839
 
633
840
  useKeyboard((key: KeyEvent) => {
634
- const character = getInputCharacter(key)
635
-
636
841
  if (key.ctrl && key.name === "c") {
637
842
  if (busy && activeAbortRef.current) {
638
843
  activeAbortRef.current.abort()
639
844
  addEntry("status", "run", "cancelled · stream stopped locally")
640
845
  return
641
846
  }
847
+ if (!busy && mode === "input" && input.trim()) {
848
+ setInput("")
849
+ return
850
+ }
642
851
  if (!busy) {
643
852
  exitApp()
644
853
  }
645
854
  return
646
855
  }
647
856
 
648
- if (mode === "model" && key.name === "escape") {
857
+ if ((mode === "model" || mode === "model-reasoning") && key.name === "escape") {
858
+ setPendingModelEntry(null)
859
+ setModelSearch("")
649
860
  setMode("input")
650
861
  return
651
862
  }
@@ -653,24 +864,86 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
653
864
  if (mode === "model") {
654
865
  if (key.name === "backspace" || key.name === "delete") {
655
866
  setModelSearch((value) => value.slice(0, -1))
656
- } else if (character === "T") {
657
- toggleModelPreference("thinking")
658
- } else if (character === "F") {
659
- toggleModelPreference("fast")
660
- } else if (isSearchInput(character)) {
661
- setModelSearch((value) => `${value}${character}`)
867
+ return
868
+ }
869
+
870
+ const searchCharacter = getModelPickerSearchCharacter(key)
871
+ if (searchCharacter === "T") {
872
+ toggleModelPickerPreference("thinking")
873
+ return
874
+ }
875
+ if (searchCharacter === "F") {
876
+ toggleModelPickerPreference("fast")
877
+ return
878
+ }
879
+ if (isModelPickerSearchInput(searchCharacter)) {
880
+ setModelSearch((value) => `${value}${searchCharacter}`)
881
+ return
882
+ }
883
+
884
+ if (modelItems.length > 0) {
885
+ if (
886
+ applyOpenTuiListNav(key, {
887
+ count: modelItems.length,
888
+ index: modelSelectIndex,
889
+ setIndex: setModelSelectIndex,
890
+ onEnter: () => {
891
+ const item = modelItems[modelSelectIndex]
892
+ if (item) {
893
+ void selectModel(item.value)
894
+ }
895
+ },
896
+ })
897
+ ) {
898
+ return
899
+ }
662
900
  }
663
901
  return
664
902
  }
665
903
 
666
- if (mode === "command" && key.name === "escape") {
667
- setInput("")
668
- setMode("input")
669
- return
904
+ if (mode === "model-reasoning" && reasoningItems.length > 0) {
905
+ if (
906
+ applyOpenTuiListNav(key, {
907
+ count: reasoningItems.length,
908
+ index: reasoningSelectIndex,
909
+ setIndex: setReasoningSelectIndex,
910
+ onEnter: () => {
911
+ const item = reasoningItems[reasoningSelectIndex]
912
+ if (item) {
913
+ void selectReasoning(item.value as ModelReasoningLevel)
914
+ }
915
+ },
916
+ })
917
+ ) {
918
+ return
919
+ }
670
920
  }
671
921
 
672
922
  if (mode === "input") {
673
923
  const pageSize = Math.max(1, transcriptViewportRows - 1)
924
+ const paletteOpen =
925
+ input.startsWith("/") && !input.includes(" ") && !menuDismissed && commandMenu.items.length > 0
926
+
927
+ if (paletteOpen && commandItems.length > 0) {
928
+ if (
929
+ applyOpenTuiListNav(key, {
930
+ count: commandItems.length,
931
+ index: commandSelectIndex,
932
+ setIndex: (index) => {
933
+ setCommandSelectIndex(index)
934
+ commandSelectIndexRef.current = index
935
+ },
936
+ onEnter: () => {
937
+ const item = commandItems[commandSelectIndexRef.current]
938
+ if (item) {
939
+ selectCommandOption(commandSelectIndexRef.current, item)
940
+ }
941
+ },
942
+ })
943
+ ) {
944
+ return
945
+ }
946
+ }
674
947
 
675
948
  if (key.ctrl && key.name === "t") {
676
949
  toggleFeedExpand("think")
@@ -682,22 +955,64 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
682
955
  return
683
956
  }
684
957
 
685
- if (key.shift && (key.name === "return" || key.name === "enter")) {
958
+ if (key.ctrl && (key.name === "u" || key.name === "k")) {
959
+ const edit = applySimpleComposerBufferEdit(input, key)
960
+ if (edit.handled) {
961
+ setInput(edit.value)
962
+ resetInputHistoryNav()
963
+ return
964
+ }
965
+ }
966
+
967
+ if (isComposerNewlineKey(key.sequence, key)) {
686
968
  setInput((value) => `${value}\n`)
969
+ resetInputHistoryNav()
687
970
  return
688
971
  }
689
972
 
690
- if (key.name === "up") {
691
- markScrollUnpinned()
692
- setScrollOffset((offset) => Math.min(maxScrollOffset, offset + 1))
693
- } else if (key.name === "down") {
694
- setScrollOffset((offset) => {
695
- const next = Math.max(0, offset - 1)
696
- if (next === 0) {
697
- scrollPinnedRef.current = true
973
+ if (key.name === "escape") {
974
+ if (paletteOpen) {
975
+ setMenuDismissed(true)
976
+ }
977
+ return
978
+ }
979
+
980
+ if (key.name === "tab" && paletteOpen) {
981
+ const command = commandMenu.items[commandSelectIndexRef.current]
982
+ if (command) {
983
+ setInput(`/${command.name} `)
984
+ setMenuDismissed(false)
985
+ }
986
+ return
987
+ }
988
+
989
+ const scrollTranscript = transcriptScrollActive() && !paletteOpen
990
+
991
+ if (key.name === "up" || (key.name === "k" && !key.ctrl && !key.meta && !paletteOpen)) {
992
+ if (scrollTranscript) {
993
+ markScrollUnpinned()
994
+ setScrollOffset((offset) => Math.min(maxScrollOffset, offset + 1))
995
+ } else {
996
+ const previous = historyPrev()
997
+ if (previous !== null) {
998
+ setInput(previous)
698
999
  }
699
- return next
700
- })
1000
+ }
1001
+ } else if (key.name === "down" || (key.name === "j" && !key.ctrl && !key.meta && !paletteOpen)) {
1002
+ if (scrollTranscript) {
1003
+ setScrollOffset((offset) => {
1004
+ const next = Math.max(0, offset - 1)
1005
+ if (next === 0) {
1006
+ scrollPinnedRef.current = true
1007
+ }
1008
+ return next
1009
+ })
1010
+ } else {
1011
+ const next = historyNext()
1012
+ if (next !== null) {
1013
+ setInput(next)
1014
+ }
1015
+ }
701
1016
  } else if (key.name === "pageup") {
702
1017
  markScrollUnpinned()
703
1018
  setScrollOffset((offset) => Math.min(maxScrollOffset, offset + pageSize))
@@ -710,19 +1025,36 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
710
1025
  return next
711
1026
  })
712
1027
  } else if (key.name === "home") {
713
- markScrollUnpinned()
714
- setScrollOffset(maxScrollOffset)
1028
+ if (scrollTranscript) {
1029
+ markScrollUnpinned()
1030
+ setScrollOffset(maxScrollOffset)
1031
+ }
715
1032
  } else if (key.name === "end") {
716
- markScrollPinned()
1033
+ if (scrollTranscript) {
1034
+ markScrollPinned()
1035
+ }
717
1036
  }
718
1037
  }
719
1038
  })
720
1039
 
721
1040
  const selectCommandOption = (_index: number, option: SelectOption | null) => {
722
1041
  const item = option as CommandSelectItem | null
723
- if (item?.value) {
724
- void runCommand(item.value)
1042
+ if (!item?.value || !slashMenuModule) {
1043
+ return
1044
+ }
1045
+ const command = commandMenu.items.find((entry) => entry.name === item.commandName)
1046
+ if (command && slashMenuModule.commandRequiresArg(command)) {
1047
+ setInput(`/${command.name} `)
1048
+ setMenuDismissed(false)
1049
+ setMode("input")
1050
+ return
725
1051
  }
1052
+ void runCommand(item.value)
1053
+ }
1054
+
1055
+ const trackCommandSelection = (index: number) => {
1056
+ commandSelectIndexRef.current = index
1057
+ setCommandSelectIndex(index)
726
1058
  }
727
1059
 
728
1060
  const selectModelOption = (_index: number, option: SelectOption | null) => {
@@ -732,6 +1064,28 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
732
1064
  }
733
1065
  }
734
1066
 
1067
+ const selectReasoningOption = (_index: number, option: SelectOption | null) => {
1068
+ const item = option as ModelSelectItem | null
1069
+ if (item?.value) {
1070
+ void selectReasoning(item.value as ModelReasoningLevel)
1071
+ }
1072
+ }
1073
+
1074
+ const footerHint = buildInteractiveFooterHint({
1075
+ busy,
1076
+ slashMenuOpen: slashPaletteOpen,
1077
+ pickerMode:
1078
+ mode === "model"
1079
+ ? "model"
1080
+ : mode === "model-reasoning"
1081
+ ? "model-reasoning"
1082
+ : null,
1083
+ modelPickerPreferences:
1084
+ mode === "model"
1085
+ ? { thinking: modelThinking, fast: modelFast }
1086
+ : undefined,
1087
+ })
1088
+
735
1089
  if (bootError) {
736
1090
  return createElement(
737
1091
  "box",
@@ -788,32 +1142,36 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
788
1142
  attributes: TextAttributes.DIM,
789
1143
  })
790
1144
  : null,
791
- mode === "command"
1145
+ mode === "model-reasoning"
792
1146
  ? createElement(
793
1147
  "box",
794
1148
  {
795
1149
  border: true,
796
1150
  borderStyle: "single",
797
- borderColor: "cyan",
1151
+ borderColor: "magenta",
798
1152
  flexDirection: "column",
799
1153
  marginTop: 1,
800
1154
  paddingX: 1,
801
1155
  },
802
1156
  createElement("text", {
803
- content: "Commands",
1157
+ content: "Select reasoning effort",
804
1158
  attributes: TextAttributes.BOLD,
805
1159
  }),
806
1160
  createElement("text", {
807
- content: "Use arrows and Enter, or Escape to cancel.",
1161
+ content: `${formatModelDisplayName(pendingModelEntry?.id ?? currentModel)} · ↑↓ or j/k · 1-9 · Enter · Esc`,
808
1162
  fg: "gray",
809
1163
  }),
810
- createElement("select", {
811
- focused: mode === "command",
812
- height: commandSelectRows,
813
- options: commandItems,
814
- onSelect: selectCommandOption,
815
- showDescription: false,
816
- })
1164
+ reasoningItems.length === 0
1165
+ ? createElement("text", { content: "No reasoning levels.", fg: "yellow" })
1166
+ : createElement("select", {
1167
+ focused: mode === "model-reasoning",
1168
+ height: modelSelectRows,
1169
+ options: reasoningItems,
1170
+ selectedIndex: reasoningSelectIndex,
1171
+ onSelect: selectReasoningOption,
1172
+ showDescription: true,
1173
+ showScrollIndicator: true,
1174
+ })
817
1175
  )
818
1176
  : mode === "model"
819
1177
  ? createElement(
@@ -827,100 +1185,173 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
827
1185
  paddingX: 1,
828
1186
  },
829
1187
  createElement("text", {
830
- content: "Select a model",
1188
+ content: "Select Model and Effort",
831
1189
  attributes: TextAttributes.BOLD,
832
1190
  }),
833
1191
  createElement("text", {
834
- content: `Type to search · T thinking ${modelThinking ? "on" : "off"} · F fast ${modelFast ? "on" : "off"} · Enter choose · Escape cancel`,
1192
+ content: `Type to search · ${formatModelPickerPreferenceHint(modelThinking, modelFast)} · ↑↓ or j/k · 1-9 · Enter · Esc`,
835
1193
  fg: "gray",
836
1194
  }),
837
1195
  createElement("text", {
838
1196
  content: `Search: ${modelSearch || "all models"}`,
839
1197
  fg: "gray",
840
1198
  }),
1199
+ createElement("text", {
1200
+ content: "Access other models by running patze -m <model> or in config",
1201
+ fg: "gray",
1202
+ }),
841
1203
  modelItems.length === 0
842
1204
  ? createElement("text", { content: "No matching models.", fg: "yellow" })
843
1205
  : createElement("select", {
844
1206
  focused: mode === "model",
845
1207
  height: modelSelectRows,
846
1208
  options: modelItems,
1209
+ selectedIndex: modelSelectIndex,
847
1210
  onSelect: selectModelOption,
1211
+ showDescription: true,
848
1212
  showScrollIndicator: true,
849
1213
  })
850
1214
  )
851
1215
  : createElement(
852
1216
  "box",
853
- {
854
- border: true,
855
- borderStyle: "single",
856
- borderColor: busy ? "yellow" : "green",
857
- flexDirection: "column",
858
- marginTop: 1,
859
- paddingX: 1,
860
- },
1217
+ { flexDirection: "column", flexShrink: 0 },
1218
+ slashPaletteOpen && commandItems.length > 0
1219
+ ? createElement(
1220
+ "box",
1221
+ {
1222
+ border: true,
1223
+ borderStyle: "single",
1224
+ borderColor: "cyan",
1225
+ flexDirection: "column",
1226
+ marginTop: 1,
1227
+ paddingX: 1,
1228
+ },
1229
+ createElement("text", {
1230
+ content: "Commands",
1231
+ attributes: TextAttributes.BOLD,
1232
+ }),
1233
+ createElement("text", {
1234
+ content:
1235
+ commandMenu.advancedCount > 0
1236
+ ? `${commandMenu.advancedCount} advanced · Tab · ↑↓ or j/k · 1-9 · Enter · Esc`
1237
+ : "Tab · ↑↓ or j/k · 1-9 jump · Enter · Esc dismiss",
1238
+ fg: "gray",
1239
+ }),
1240
+ createElement("select", {
1241
+ focused: false,
1242
+ height: commandSelectRows,
1243
+ options: commandItems,
1244
+ selectedIndex: commandSelectIndex,
1245
+ onChange: trackCommandSelection,
1246
+ onSelect: selectCommandOption,
1247
+ showDescription: true,
1248
+ showScrollIndicator: true,
1249
+ })
1250
+ )
1251
+ : null,
861
1252
  createElement(
862
1253
  "box",
863
- { flexDirection: "row", alignItems: "center" },
864
- createElement("text", {
865
- content: "> ",
866
- attributes: TextAttributes.BOLD,
867
- }),
868
- createElement(TuiInput, {
869
- focused: ready && !busy,
870
- placeholder: busy
871
- ? activityLabel
872
- ? `${["", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][activityFrame % 10]} ${activityLabel}… · Ctrl+C cancel`
873
- : "Ctrl+C cancel · waiting for agent…"
874
- : ready
875
- ? composerPlaceholder
876
- : "Loading Patze Code…",
877
- value: input,
878
- onInput: (value: string) => {
879
- setInput(value)
880
- if (busy) {
881
- return
882
- }
883
- if (value.startsWith("/") && !value.includes(" ")) {
884
- setMode("command")
885
- } else {
886
- setMode("input")
887
- }
888
- },
889
- onSubmit: submitInput,
890
- })
891
- ),
892
- createElement("text", {
893
- content: `${composerModelLabel} · ${composerPathLabel}`,
894
- attributes: TextAttributes.DIM,
895
- })
1254
+ {
1255
+ border: true,
1256
+ borderStyle: "single",
1257
+ borderColor: busy ? "yellow" : "green",
1258
+ flexDirection: "column",
1259
+ marginTop: 1,
1260
+ paddingX: 1,
1261
+ },
1262
+ createElement(
1263
+ "box",
1264
+ { flexDirection: "row", alignItems: "center" },
1265
+ createElement("text", {
1266
+ content: "> ",
1267
+ attributes: TextAttributes.BOLD,
1268
+ }),
1269
+ createElement(TuiInput, {
1270
+ focused: ready && !busy,
1271
+ placeholder: busy
1272
+ ? activityLabel
1273
+ ? `${["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][activityFrame % 10]} ${activityLabel}… · Ctrl+C cancel`
1274
+ : "Ctrl+C cancel · waiting for agent…"
1275
+ : ready
1276
+ ? composerPlaceholder
1277
+ : "Loading Patze Code…",
1278
+ value: input,
1279
+ onInput: (value: string) => {
1280
+ setInput(value)
1281
+ resetInputHistoryNav()
1282
+ if (busy) {
1283
+ return
1284
+ }
1285
+ if (!value.startsWith("/")) {
1286
+ setMenuDismissed(false)
1287
+ }
1288
+ if (value.startsWith("/") && !value.includes(" ") && !menuDismissed) {
1289
+ setCommandSelectIndex(0)
1290
+ commandSelectIndexRef.current = 0
1291
+ }
1292
+ },
1293
+ onSubmit: submitInput,
1294
+ })
1295
+ )
1296
+ )
896
1297
  ),
897
1298
  createElement("text", {
898
- content: busy
899
- ? "Esc exit · Ctrl+C cancel run · agent executing"
900
- : "Esc exit · Ctrl+C quit · / commands · ctrl+t thought · ctrl+d diff · /status",
1299
+ content: footerHint,
901
1300
  attributes: TextAttributes.DIM,
902
1301
  })
903
1302
  )
904
1303
  }
905
1304
 
1305
+ function applyOpenTuiListNav(
1306
+ key: KeyEvent,
1307
+ input: {
1308
+ count: number
1309
+ index: number
1310
+ setIndex: (index: number) => void
1311
+ onEnter: () => void
1312
+ }
1313
+ ): boolean {
1314
+ if (input.count <= 0) {
1315
+ return false
1316
+ }
1317
+
1318
+ const digitSource =
1319
+ key.sequence && key.sequence.length === 1 ? key.sequence : (key.name ?? "")
1320
+ const jump = jumpListSelection(digitSource, input.count)
1321
+ if (jump !== null) {
1322
+ input.setIndex(jump)
1323
+ return true
1324
+ }
1325
+
1326
+ const nav = mapListNavKey(key.name, key.sequence)
1327
+ if (nav === "up") {
1328
+ input.setIndex(moveListSelection(input.index, -1, input.count))
1329
+ return true
1330
+ }
1331
+ if (nav === "down") {
1332
+ input.setIndex(moveListSelection(input.index, 1, input.count))
1333
+ return true
1334
+ }
1335
+ if (nav === "enter") {
1336
+ input.onEnter()
1337
+ return true
1338
+ }
1339
+ return false
1340
+ }
1341
+
906
1342
  function trimComposerPrompt(value: string): string {
907
1343
  return value.replace(/^\s+|\s+$/g, "")
908
1344
  }
909
1345
 
910
- function shortenComposerPath(path: string): string {
911
- const normalized = String(path || "").trim().replace(/\\/g, "/")
912
- if ([...normalized].length <= 36) {
913
- return normalized
914
- }
915
- const parts = normalized.split("/").filter(Boolean)
916
- if (parts.length >= 2) {
917
- const tail = `${parts[parts.length - 2]}/${parts[parts.length - 1]}`
918
- const candidate = `…/${tail}`
919
- if ([...candidate].length <= 36) {
920
- return candidate
921
- }
1346
+ function getModelPickerSearchCharacter(key: KeyEvent): string {
1347
+ if (key.ctrl || key.meta || key.name.length !== 1) {
1348
+ return ""
922
1349
  }
923
- return `…${normalized.slice(-35)}`
1350
+ return key.shift ? key.name.toUpperCase() : key.name
1351
+ }
1352
+
1353
+ function isModelPickerSearchInput(input: string): boolean {
1354
+ return input.length > 0 && !/[\u0000-\u001F\u007F]/.test(input)
924
1355
  }
925
1356
 
926
1357
  function HeaderLine({ line }: { line: string }) {
@@ -985,31 +1416,8 @@ function partToAttributes(part: TranscriptPart) {
985
1416
  return attributes
986
1417
  }
987
1418
 
988
- function getInputCharacter(key: KeyEvent): string {
989
- if (key.sequence && key.sequence.length === 1 && !key.ctrl && !key.meta) {
990
- return key.sequence
991
- }
992
- return ""
993
- }
994
-
995
- function isSearchInput(character: string): boolean {
996
- return Boolean(character && character !== " " && character.charCodeAt(0) >= 32)
997
- }
998
-
999
1419
  type SlashMenuModule = typeof import("../../dist/cli/interactive/slash-menu.js")
1000
1420
 
1001
1421
  let slashMenuModule: SlashMenuModule | null = null
1002
1422
 
1003
- function getCommandItems(input: string): CommandSelectItem[] {
1004
- if (!slashMenuModule) {
1005
- return []
1006
- }
1007
- const menu = slashMenuModule.computeSlashMenuItems(input)
1008
- return menu.items.map((command) => ({
1009
- name: `/${command.name}`,
1010
- description: command.description,
1011
- value: `/${command.name}`,
1012
- }))
1013
- }
1014
-
1015
1423
  export { parseCwdArg }