@patze/code-cli 0.43.3 → 0.55.0

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 (181) hide show
  1. package/CHANGELOG.md +231 -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-edit-keys.d.ts +21 -0
  78. package/dist/cli/interactive/composer-edit-keys.d.ts.map +1 -0
  79. package/dist/cli/interactive/composer-edit-keys.js +57 -0
  80. package/dist/cli/interactive/composer-edit-keys.js.map +1 -0
  81. package/dist/cli/interactive/ensure-execute-auth.d.ts.map +1 -1
  82. package/dist/cli/interactive/ensure-execute-auth.js +8 -13
  83. package/dist/cli/interactive/ensure-execute-auth.js.map +1 -1
  84. package/dist/cli/interactive/header-refresh-triggers.d.ts +3 -0
  85. package/dist/cli/interactive/header-refresh-triggers.d.ts.map +1 -0
  86. package/dist/cli/interactive/header-refresh-triggers.js +16 -0
  87. package/dist/cli/interactive/header-refresh-triggers.js.map +1 -0
  88. package/dist/cli/interactive/header-refresh.d.ts +4 -0
  89. package/dist/cli/interactive/header-refresh.d.ts.map +1 -0
  90. package/dist/cli/interactive/header-refresh.js +33 -0
  91. package/dist/cli/interactive/header-refresh.js.map +1 -0
  92. package/dist/cli/interactive/header.d.ts +3 -2
  93. package/dist/cli/interactive/header.d.ts.map +1 -1
  94. package/dist/cli/interactive/header.js +23 -14
  95. package/dist/cli/interactive/header.js.map +1 -1
  96. package/dist/cli/interactive/interactive-footer-hints.d.ts +12 -0
  97. package/dist/cli/interactive/interactive-footer-hints.d.ts.map +1 -0
  98. package/dist/cli/interactive/interactive-footer-hints.js +32 -0
  99. package/dist/cli/interactive/interactive-footer-hints.js.map +1 -0
  100. package/dist/cli/interactive/interactive-model-apply.d.ts +15 -0
  101. package/dist/cli/interactive/interactive-model-apply.d.ts.map +1 -0
  102. package/dist/cli/interactive/interactive-model-apply.js +28 -0
  103. package/dist/cli/interactive/interactive-model-apply.js.map +1 -0
  104. package/dist/cli/interactive/interactive-ui.d.ts +13 -0
  105. package/dist/cli/interactive/interactive-ui.d.ts.map +1 -0
  106. package/dist/cli/interactive/interactive-ui.js +64 -0
  107. package/dist/cli/interactive/interactive-ui.js.map +1 -0
  108. package/dist/cli/interactive/launcher.d.ts.map +1 -1
  109. package/dist/cli/interactive/launcher.js +24 -17
  110. package/dist/cli/interactive/launcher.js.map +1 -1
  111. package/dist/cli/interactive/line-editor.d.ts +6 -0
  112. package/dist/cli/interactive/line-editor.d.ts.map +1 -1
  113. package/dist/cli/interactive/line-editor.js +102 -8
  114. package/dist/cli/interactive/line-editor.js.map +1 -1
  115. package/dist/cli/interactive/model-catalog-resolve.d.ts +11 -0
  116. package/dist/cli/interactive/model-catalog-resolve.d.ts.map +1 -0
  117. package/dist/cli/interactive/model-catalog-resolve.js +49 -0
  118. package/dist/cli/interactive/model-catalog-resolve.js.map +1 -0
  119. package/dist/cli/interactive/model-picker-panel.d.ts +22 -2
  120. package/dist/cli/interactive/model-picker-panel.d.ts.map +1 -1
  121. package/dist/cli/interactive/model-picker-panel.js +99 -16
  122. package/dist/cli/interactive/model-picker-panel.js.map +1 -1
  123. package/dist/cli/interactive/model-selector.d.ts.map +1 -1
  124. package/dist/cli/interactive/model-selector.js +3 -2
  125. package/dist/cli/interactive/model-selector.js.map +1 -1
  126. package/dist/cli/interactive/model-tty-picker.d.ts +3 -1
  127. package/dist/cli/interactive/model-tty-picker.d.ts.map +1 -1
  128. package/dist/cli/interactive/model-tty-picker.js +154 -24
  129. package/dist/cli/interactive/model-tty-picker.js.map +1 -1
  130. package/dist/cli/interactive/plain-codex-feed.d.ts.map +1 -1
  131. package/dist/cli/interactive/plain-codex-feed.js +48 -0
  132. package/dist/cli/interactive/plain-codex-feed.js.map +1 -1
  133. package/dist/cli/interactive/run-cancel.d.ts +23 -0
  134. package/dist/cli/interactive/run-cancel.d.ts.map +1 -0
  135. package/dist/cli/interactive/run-cancel.js +67 -0
  136. package/dist/cli/interactive/run-cancel.js.map +1 -0
  137. package/dist/cli/interactive/select-list-nav.d.ts +10 -0
  138. package/dist/cli/interactive/select-list-nav.d.ts.map +1 -0
  139. package/dist/cli/interactive/select-list-nav.js +45 -0
  140. package/dist/cli/interactive/select-list-nav.js.map +1 -0
  141. package/dist/cli/interactive/session-controller.d.ts +1 -0
  142. package/dist/cli/interactive/session-controller.d.ts.map +1 -1
  143. package/dist/cli/interactive/session-controller.js +5 -1
  144. package/dist/cli/interactive/session-controller.js.map +1 -1
  145. package/dist/cli/interactive/session-hints.d.ts +1 -0
  146. package/dist/cli/interactive/session-hints.d.ts.map +1 -1
  147. package/dist/cli/interactive/session-hints.js +24 -8
  148. package/dist/cli/interactive/session-hints.js.map +1 -1
  149. package/dist/cli/interactive/shell.d.ts.map +1 -1
  150. package/dist/cli/interactive/shell.js +17 -5
  151. package/dist/cli/interactive/shell.js.map +1 -1
  152. package/dist/cli/interactive/slash-dispatch.d.ts +2 -0
  153. package/dist/cli/interactive/slash-dispatch.d.ts.map +1 -1
  154. package/dist/cli/interactive/slash-dispatch.js +55 -28
  155. package/dist/cli/interactive/slash-dispatch.js.map +1 -1
  156. package/dist/cli/interactive/slash-menu.d.ts.map +1 -1
  157. package/dist/cli/interactive/slash-menu.js +29 -2
  158. package/dist/cli/interactive/slash-menu.js.map +1 -1
  159. package/dist/cli/interactive/slash-registry.js +2 -2
  160. package/dist/cli/interactive/slash-registry.js.map +1 -1
  161. package/dist/cli/interactive/smoke-trust-fixture.d.ts +10 -0
  162. package/dist/cli/interactive/smoke-trust-fixture.d.ts.map +1 -0
  163. package/dist/cli/interactive/smoke-trust-fixture.js +66 -0
  164. package/dist/cli/interactive/smoke-trust-fixture.js.map +1 -0
  165. package/dist/cli/interactive/tools-catalog.d.ts.map +1 -1
  166. package/dist/cli/interactive/tools-catalog.js +17 -12
  167. package/dist/cli/interactive/tools-catalog.js.map +1 -1
  168. package/dist/config/config.d.ts +2 -0
  169. package/dist/config/config.d.ts.map +1 -1
  170. package/dist/config/config.js +8 -0
  171. package/dist/config/config.js.map +1 -1
  172. package/dist/config/web-ui.d.ts +5 -0
  173. package/dist/config/web-ui.d.ts.map +1 -0
  174. package/dist/config/web-ui.js +12 -0
  175. package/dist/config/web-ui.js.map +1 -0
  176. package/docs/beta-release-handoff.md +56 -29
  177. package/docs/internal-beta.md +6 -3
  178. package/docs/known-limitations.md +32 -19
  179. package/docs/release-checklist.md +11 -3
  180. package/opentui/src/App.tsx +636 -213
  181. 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,23 @@ 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
163
  const [composerPlaceholder, setComposerPlaceholder] = useState("Describe a task… · / for commands")
164
+ const [composerChromeLeft, setComposerChromeLeft] = useState("Patze")
165
+ const [composerChromeRight, setComposerChromeRight] = useState("")
129
166
  const [emptySessionHints, setEmptySessionHints] = useState<string[]>([])
130
167
 
131
168
  const followTranscript = useCallback(() => {
@@ -143,6 +180,53 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
143
180
  setScrollOffset(0)
144
181
  }, [])
145
182
 
183
+ const resetInputHistoryNav = useCallback(() => {
184
+ historyIndexRef.current = -1
185
+ stashedInputRef.current = ""
186
+ }, [])
187
+
188
+ const pushInputHistory = useCallback((value: string) => {
189
+ if (!isHistorySafe(value)) {
190
+ return
191
+ }
192
+ const history = inputHistoryRef.current
193
+ if (history[history.length - 1] !== value) {
194
+ inputHistoryRef.current = [...history, value]
195
+ }
196
+ resetInputHistoryNav()
197
+ }, [resetInputHistoryNav])
198
+
199
+ const historyPrev = useCallback((): string | null => {
200
+ const history = inputHistoryRef.current
201
+ if (!history.length) {
202
+ return null
203
+ }
204
+ if (historyIndexRef.current === -1) {
205
+ stashedInputRef.current = input
206
+ historyIndexRef.current = history.length - 1
207
+ } else {
208
+ historyIndexRef.current = Math.max(0, historyIndexRef.current - 1)
209
+ }
210
+ return history[historyIndexRef.current] ?? null
211
+ }, [input])
212
+
213
+ const historyNext = useCallback((): string | null => {
214
+ if (historyIndexRef.current === -1) {
215
+ return null
216
+ }
217
+ const history = inputHistoryRef.current
218
+ if (historyIndexRef.current >= history.length - 1) {
219
+ historyIndexRef.current = -1
220
+ return stashedInputRef.current
221
+ }
222
+ historyIndexRef.current += 1
223
+ return history[historyIndexRef.current] ?? null
224
+ }, [])
225
+
226
+ const transcriptScrollActive = useCallback(() => {
227
+ return !scrollPinnedRef.current || scrollOffset > 0
228
+ }, [scrollOffset])
229
+
146
230
  const nextId = useCallback(() => {
147
231
  const id = nextIdRef.current
148
232
  nextIdRef.current += 1
@@ -239,24 +323,12 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
239
323
  setExecutionMode(controller.session.getExecutionMode())
240
324
  setModelThinking(controller.session.getModelThinking())
241
325
  setModelFast(controller.session.getModelFast())
326
+ setModelReasoningLevel(controller.session.getModelReasoningLevel())
242
327
 
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)
328
+ const catalogResolve = await importPatzeDist<
329
+ typeof import("../../dist/cli/interactive/model-catalog-resolve.js")
330
+ >("cli/interactive/model-catalog-resolve.js")
331
+ setPickerCatalog(await catalogResolve.resolvePickerModelCatalog())
260
332
 
261
333
  const cloudGit = await importPatzeDist<
262
334
  typeof import("../../dist/cli/interactive/cloud-git.js")
@@ -278,7 +350,19 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
278
350
  const sessionHints = await importPatzeDist<
279
351
  typeof import("../../dist/cli/interactive/session-hints.js")
280
352
  >("cli/interactive/session-hints.js")
281
- setEmptySessionHints(sessionHints.buildEmptySessionHints(controller.cwd))
353
+ setEmptySessionHints(
354
+ sessionHints.shouldShowEmptySessionHints(controller.cwd)
355
+ ? sessionHints.buildEmptySessionHints(controller.cwd)
356
+ : []
357
+ )
358
+
359
+ const composerChrome = await importPatzeDist<
360
+ typeof import("../../dist/cli/interactive/composer-session-chrome.js")
361
+ >("cli/interactive/composer-session-chrome.js")
362
+ const chrome = composerChrome.buildComposerChromeForSession(controller as never)
363
+ setComposerChromeLeft(chrome.left)
364
+ setComposerChromeRight(chrome.right)
365
+ setStartupHeader(controller.renderHeaderLines())
282
366
  }, [])
283
367
 
284
368
  useEffect(() => {
@@ -387,7 +471,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
387
471
  const result = await controller.processLine(value, sink, {
388
472
  signal: abort.signal,
389
473
  onActivity: setActivityLabel,
390
- useActivitySpinner: true,
474
+ useActivitySpinner: false,
391
475
  onAgentEvent: (event) => {
392
476
  setActivityLabel(null)
393
477
  setTranscript((items) =>
@@ -403,6 +487,9 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
403
487
  setTranscript([])
404
488
  markScrollPinned()
405
489
  }
490
+ if (result.refreshHeader) {
491
+ setStartupHeader(controller.renderHeaderLines())
492
+ }
406
493
  await refreshStatusMeta()
407
494
  if (result.exitShell) {
408
495
  exitApp()
@@ -430,32 +517,44 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
430
517
  )
431
518
 
432
519
  const openModelPicker = useCallback(() => {
520
+ setPendingModelEntry(null)
433
521
  setModelSearch("")
522
+ const filtered = filterModelCatalogEntries("", pickerCatalog)
523
+ setModelSelectIndex(indexForFilteredCatalogEntry(filtered, currentModel))
434
524
  setMode("model")
435
- }, [])
525
+ }, [currentModel, pickerCatalog])
436
526
 
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
- }, [])
527
+ const toggleModelPickerPreference = useCallback(
528
+ (preference: "thinking" | "fast") => {
529
+ const controller = controllerRef.current
530
+ if (!controller) {
531
+ return
532
+ }
533
+ void (async () => {
534
+ const prefs = await importPatzeDist<
535
+ typeof import("../../dist/config/model-preferences.js")
536
+ >("config/model-preferences.js")
537
+ const current = preference === "thinking" ? modelThinking : modelFast
538
+ const next = prefs.resolveModelPreferenceToggle(current, "toggle")
539
+ if (preference === "thinking") {
540
+ controller.session.setModelThinking(next)
541
+ setModelThinking(next)
542
+ } else {
543
+ controller.session.setModelFast(next)
544
+ setModelFast(next)
545
+ }
546
+ controller.persistSession()
547
+ })()
548
+ },
549
+ [modelFast, modelThinking]
550
+ )
453
551
 
454
552
  const runCommand = useCallback(
455
553
  async (rawCommand: string) => {
456
554
  const trimmed = rawCommand.trim()
457
555
  setMode("input")
458
556
  setInput("")
557
+ setMenuDismissed(false)
459
558
 
460
559
  if (!trimmed) {
461
560
  return
@@ -492,7 +591,30 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
492
591
  const submitInput = useCallback(
493
592
  (value: string) => {
494
593
  const prompt = trimComposerPrompt(value)
594
+
595
+ if (
596
+ prompt.startsWith("/") &&
597
+ !prompt.includes(" ") &&
598
+ !menuDismissed &&
599
+ slashMenuModule &&
600
+ commandMenu.items.length > 0
601
+ ) {
602
+ const command = commandMenu.items[commandSelectIndexRef.current]
603
+ if (command) {
604
+ setInput("")
605
+ resetInputHistoryNav()
606
+ if (slashMenuModule.commandRequiresArg(command)) {
607
+ setInput(`/${command.name} `)
608
+ setMenuDismissed(false)
609
+ return
610
+ }
611
+ void runCommand(`/${command.name}`)
612
+ return
613
+ }
614
+ }
615
+
495
616
  setInput("")
617
+ resetInputHistoryNav()
496
618
 
497
619
  if (!prompt || busy) {
498
620
  return
@@ -503,9 +625,19 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
503
625
  return
504
626
  }
505
627
 
628
+ pushInputHistory(prompt)
506
629
  void submitToController(prompt)
507
630
  },
508
- [busy, runCommand, submitToController]
631
+ [
632
+ busy,
633
+ commandMenu.items,
634
+ menuDismissed,
635
+ pushInputHistory,
636
+ resetInputHistoryNav,
637
+ runCommand,
638
+ slashMenuModule,
639
+ submitToController,
640
+ ]
509
641
  )
510
642
 
511
643
  const selectModel = useCallback(
@@ -515,45 +647,118 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
515
647
  return
516
648
  }
517
649
 
518
- const modelCommand = await importPatzeDist<
519
- typeof import("../../dist/cli/interactive/model-command.js")
520
- >("cli/interactive/model-command.js")
650
+ const modelApply = await importPatzeDist<
651
+ typeof import("../../dist/cli/interactive/interactive-model-apply.js")
652
+ >("cli/interactive/interactive-model-apply.js")
653
+
654
+ const catalogEntry =
655
+ pickerCatalog.find((entry) => entry.id.toLowerCase() === modelId.toLowerCase()) ?? null
656
+
657
+ const result = modelApply.applyInteractiveModelChoice(
658
+ controller.session as never,
659
+ modelId,
660
+ undefined,
661
+ catalogEntry
662
+ )
663
+ if (result.pendingReasoning) {
664
+ setPendingModelEntry(result.pendingReasoning)
665
+ setReasoningSelectIndex(
666
+ indexForReasoningLevel(
667
+ getReasoningLevelsForModelId(result.pendingReasoning.id),
668
+ currentModel.toLowerCase() === result.pendingReasoning.id.toLowerCase()
669
+ ? modelReasoningLevel
670
+ : null,
671
+ result.pendingReasoning.id
672
+ )
673
+ )
674
+ setMode("model-reasoning")
675
+ return
676
+ }
677
+
678
+ for (const line of result.lines) {
679
+ addEntry(inferErrorKind(line), "model", line)
680
+ }
681
+ controller.persistSession()
682
+ setStartupHeader(controller.renderHeaderLines())
683
+ await refreshStatusMeta()
684
+ setMode("input")
685
+ },
686
+ [addEntry, currentModel, modelReasoningLevel, pickerCatalog, refreshStatusMeta]
687
+ )
688
+
689
+ const selectReasoning = useCallback(
690
+ async (level: ModelReasoningLevel) => {
691
+ const controller = controllerRef.current
692
+ if (!controller || !pendingModelEntry) {
693
+ return
694
+ }
695
+
696
+ const modelApply = await importPatzeDist<
697
+ typeof import("../../dist/cli/interactive/interactive-model-apply.js")
698
+ >("cli/interactive/interactive-model-apply.js")
521
699
 
522
- const result = modelCommand.dispatchModelCommand(controller.session as never, modelId)
700
+ const result = modelApply.applyInteractiveModelReasoning(
701
+ controller.session as never,
702
+ pendingModelEntry,
703
+ level
704
+ )
523
705
  for (const line of result.lines) {
524
706
  addEntry(inferErrorKind(line), "model", line)
525
707
  }
708
+ controller.persistSession()
709
+ setPendingModelEntry(null)
710
+ setStartupHeader(controller.renderHeaderLines())
526
711
  await refreshStatusMeta()
527
712
  setMode("input")
528
713
  },
529
- [addEntry, refreshStatusMeta]
714
+ [addEntry, pendingModelEntry, refreshStatusMeta]
530
715
  )
531
716
 
532
717
  const commandSelectRows = Math.min(6, Math.max(3, rows - 10))
533
718
  const modelSelectRows = Math.min(8, Math.max(3, rows - 10))
534
719
  const commandPanelRows = 2 + commandSelectRows
535
- const modelPanelRows = 3 + modelSelectRows
720
+ const modelPanelRows = 4 + modelSelectRows
536
721
  const headerRows = Math.max(1, startupHeader.length)
537
722
  const composerChromeRows = 5
538
723
  const bottomHintRows = 1
539
724
 
725
+ const commandMenu = useMemo(() => {
726
+ if (!slashMenuReady || !slashMenuModule || !input.startsWith("/") || input.includes(" ") || menuDismissed) {
727
+ return { items: [], advancedCount: 0 }
728
+ }
729
+ return slashMenuModule.computeSlashMenuItems(input)
730
+ }, [input, menuDismissed, slashMenuReady])
731
+
732
+ const commandItems = useMemo(() => {
733
+ return commandMenu.items.map((command) => ({
734
+ name: `/${command.name}`,
735
+ description: command.description,
736
+ value: `/${command.name}`,
737
+ commandName: command.name,
738
+ }))
739
+ }, [commandMenu.items])
740
+
741
+ const slashPaletteOpen =
742
+ mode === "input" &&
743
+ input.startsWith("/") &&
744
+ !input.includes(" ") &&
745
+ !menuDismissed &&
746
+ commandMenu.items.length > 0
747
+
540
748
  const transcriptViewportRows = Math.max(
541
749
  4,
542
750
  rows -
543
751
  headerRows -
544
752
  bottomHintRows -
545
- (mode === "model"
753
+ (mode === "model-reasoning"
546
754
  ? modelPanelRows + 2
547
- : mode === "command"
548
- ? commandPanelRows + 2
549
- : composerChromeRows)
755
+ : mode === "model"
756
+ ? modelPanelRows + 2
757
+ : composerChromeRows + (slashPaletteOpen ? commandPanelRows + 2 : 0))
550
758
  )
551
759
 
552
- const composerModelLabel = formatModelDisplay(currentModel, {
553
- thinking: modelThinking,
554
- fast: modelFast,
555
- })
556
- const composerPathLabel = shortenComposerPath(cwd)
760
+ const composerModelLabel = composerChromeLeft
761
+ const composerPathLabel = composerChromeRight
557
762
 
558
763
  const scrollableEntries = useMemo(() => transcript, [transcript])
559
764
 
@@ -575,40 +780,53 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
575
780
  setScrollOffset((offset) => Math.min(offset, maxScrollOffset))
576
781
  }, [maxScrollOffset])
577
782
 
578
- const commandItems = useMemo(() => {
579
- if (!slashMenuReady || !input.startsWith("/") || input.includes(" ")) {
783
+ useEffect(() => {
784
+ setCommandSelectIndex(0)
785
+ commandSelectIndexRef.current = 0
786
+ }, [commandMenu.items])
787
+
788
+ const modelItems = useMemo(
789
+ () => buildFilteredModelSelectOptions(currentModel, modelSearch, pickerCatalog),
790
+ [currentModel, modelSearch, pickerCatalog]
791
+ )
792
+
793
+ useEffect(() => {
794
+ if (mode !== "model") {
795
+ return
796
+ }
797
+ setModelSelectIndex((index) => {
798
+ if (modelItems.length === 0) {
799
+ return 0
800
+ }
801
+ return Math.min(index, modelItems.length - 1)
802
+ })
803
+ }, [mode, modelItems.length, modelSearch])
804
+
805
+ const reasoningItems = useMemo(() => {
806
+ if (!pendingModelEntry) {
580
807
  return []
581
808
  }
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])
809
+ const levels = getReasoningLevelsForModelId(pendingModelEntry.id)
810
+ const currentLevel =
811
+ pendingModelEntry.id.toLowerCase() === currentModel.toLowerCase()
812
+ ? modelReasoningLevel
813
+ : null
814
+ const highlightLevel = currentLevel ?? getDefaultReasoningLevel(pendingModelEntry.id)
815
+ const defaultLevel = getDefaultReasoningLevel(pendingModelEntry.id)
816
+ return levels.map((option) => {
817
+ const item = {
818
+ label: option.label,
819
+ description: option.description,
820
+ isDefault: option.id === defaultLevel,
821
+ isCurrent: option.id === highlightLevel,
822
+ }
823
+ return {
824
+ name: formatCodexListItemLabel(item),
825
+ description: option.description,
826
+ value: option.id,
827
+ }
828
+ })
829
+ }, [currentModel, modelReasoningLevel, pendingModelEntry])
612
830
 
613
831
  const toggleFeedExpand = useCallback((target: "think" | "diff") => {
614
832
  setTranscript((items) => {
@@ -631,21 +849,25 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
631
849
  }, [followTranscript])
632
850
 
633
851
  useKeyboard((key: KeyEvent) => {
634
- const character = getInputCharacter(key)
635
-
636
852
  if (key.ctrl && key.name === "c") {
637
853
  if (busy && activeAbortRef.current) {
638
854
  activeAbortRef.current.abort()
639
855
  addEntry("status", "run", "cancelled · stream stopped locally")
640
856
  return
641
857
  }
858
+ if (!busy && mode === "input" && input.trim()) {
859
+ setInput("")
860
+ return
861
+ }
642
862
  if (!busy) {
643
863
  exitApp()
644
864
  }
645
865
  return
646
866
  }
647
867
 
648
- if (mode === "model" && key.name === "escape") {
868
+ if ((mode === "model" || mode === "model-reasoning") && key.name === "escape") {
869
+ setPendingModelEntry(null)
870
+ setModelSearch("")
649
871
  setMode("input")
650
872
  return
651
873
  }
@@ -653,24 +875,86 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
653
875
  if (mode === "model") {
654
876
  if (key.name === "backspace" || key.name === "delete") {
655
877
  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}`)
878
+ return
879
+ }
880
+
881
+ const searchCharacter = getModelPickerSearchCharacter(key)
882
+ if (searchCharacter === "T") {
883
+ toggleModelPickerPreference("thinking")
884
+ return
885
+ }
886
+ if (searchCharacter === "F") {
887
+ toggleModelPickerPreference("fast")
888
+ return
889
+ }
890
+ if (isModelPickerSearchInput(searchCharacter)) {
891
+ setModelSearch((value) => `${value}${searchCharacter}`)
892
+ return
893
+ }
894
+
895
+ if (modelItems.length > 0) {
896
+ if (
897
+ applyOpenTuiListNav(key, {
898
+ count: modelItems.length,
899
+ index: modelSelectIndex,
900
+ setIndex: setModelSelectIndex,
901
+ onEnter: () => {
902
+ const item = modelItems[modelSelectIndex]
903
+ if (item) {
904
+ void selectModel(item.value)
905
+ }
906
+ },
907
+ })
908
+ ) {
909
+ return
910
+ }
662
911
  }
663
912
  return
664
913
  }
665
914
 
666
- if (mode === "command" && key.name === "escape") {
667
- setInput("")
668
- setMode("input")
669
- return
915
+ if (mode === "model-reasoning" && reasoningItems.length > 0) {
916
+ if (
917
+ applyOpenTuiListNav(key, {
918
+ count: reasoningItems.length,
919
+ index: reasoningSelectIndex,
920
+ setIndex: setReasoningSelectIndex,
921
+ onEnter: () => {
922
+ const item = reasoningItems[reasoningSelectIndex]
923
+ if (item) {
924
+ void selectReasoning(item.value as ModelReasoningLevel)
925
+ }
926
+ },
927
+ })
928
+ ) {
929
+ return
930
+ }
670
931
  }
671
932
 
672
933
  if (mode === "input") {
673
934
  const pageSize = Math.max(1, transcriptViewportRows - 1)
935
+ const paletteOpen =
936
+ input.startsWith("/") && !input.includes(" ") && !menuDismissed && commandMenu.items.length > 0
937
+
938
+ if (paletteOpen && commandItems.length > 0) {
939
+ if (
940
+ applyOpenTuiListNav(key, {
941
+ count: commandItems.length,
942
+ index: commandSelectIndex,
943
+ setIndex: (index) => {
944
+ setCommandSelectIndex(index)
945
+ commandSelectIndexRef.current = index
946
+ },
947
+ onEnter: () => {
948
+ const item = commandItems[commandSelectIndexRef.current]
949
+ if (item) {
950
+ selectCommandOption(commandSelectIndexRef.current, item)
951
+ }
952
+ },
953
+ })
954
+ ) {
955
+ return
956
+ }
957
+ }
674
958
 
675
959
  if (key.ctrl && key.name === "t") {
676
960
  toggleFeedExpand("think")
@@ -682,22 +966,64 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
682
966
  return
683
967
  }
684
968
 
685
- if (key.shift && (key.name === "return" || key.name === "enter")) {
969
+ if (key.ctrl && (key.name === "u" || key.name === "k")) {
970
+ const edit = applySimpleComposerBufferEdit(input, key)
971
+ if (edit.handled) {
972
+ setInput(edit.value)
973
+ resetInputHistoryNav()
974
+ return
975
+ }
976
+ }
977
+
978
+ if (isComposerNewlineKey(key.sequence, key)) {
686
979
  setInput((value) => `${value}\n`)
980
+ resetInputHistoryNav()
687
981
  return
688
982
  }
689
983
 
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
984
+ if (key.name === "escape") {
985
+ if (paletteOpen) {
986
+ setMenuDismissed(true)
987
+ }
988
+ return
989
+ }
990
+
991
+ if (key.name === "tab" && paletteOpen) {
992
+ const command = commandMenu.items[commandSelectIndexRef.current]
993
+ if (command) {
994
+ setInput(`/${command.name} `)
995
+ setMenuDismissed(false)
996
+ }
997
+ return
998
+ }
999
+
1000
+ const scrollTranscript = transcriptScrollActive() && !paletteOpen
1001
+
1002
+ if (key.name === "up" || (key.name === "k" && !key.ctrl && !key.meta && !paletteOpen)) {
1003
+ if (scrollTranscript) {
1004
+ markScrollUnpinned()
1005
+ setScrollOffset((offset) => Math.min(maxScrollOffset, offset + 1))
1006
+ } else {
1007
+ const previous = historyPrev()
1008
+ if (previous !== null) {
1009
+ setInput(previous)
698
1010
  }
699
- return next
700
- })
1011
+ }
1012
+ } else if (key.name === "down" || (key.name === "j" && !key.ctrl && !key.meta && !paletteOpen)) {
1013
+ if (scrollTranscript) {
1014
+ setScrollOffset((offset) => {
1015
+ const next = Math.max(0, offset - 1)
1016
+ if (next === 0) {
1017
+ scrollPinnedRef.current = true
1018
+ }
1019
+ return next
1020
+ })
1021
+ } else {
1022
+ const next = historyNext()
1023
+ if (next !== null) {
1024
+ setInput(next)
1025
+ }
1026
+ }
701
1027
  } else if (key.name === "pageup") {
702
1028
  markScrollUnpinned()
703
1029
  setScrollOffset((offset) => Math.min(maxScrollOffset, offset + pageSize))
@@ -710,19 +1036,36 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
710
1036
  return next
711
1037
  })
712
1038
  } else if (key.name === "home") {
713
- markScrollUnpinned()
714
- setScrollOffset(maxScrollOffset)
1039
+ if (scrollTranscript) {
1040
+ markScrollUnpinned()
1041
+ setScrollOffset(maxScrollOffset)
1042
+ }
715
1043
  } else if (key.name === "end") {
716
- markScrollPinned()
1044
+ if (scrollTranscript) {
1045
+ markScrollPinned()
1046
+ }
717
1047
  }
718
1048
  }
719
1049
  })
720
1050
 
721
1051
  const selectCommandOption = (_index: number, option: SelectOption | null) => {
722
1052
  const item = option as CommandSelectItem | null
723
- if (item?.value) {
724
- void runCommand(item.value)
1053
+ if (!item?.value || !slashMenuModule) {
1054
+ return
1055
+ }
1056
+ const command = commandMenu.items.find((entry) => entry.name === item.commandName)
1057
+ if (command && slashMenuModule.commandRequiresArg(command)) {
1058
+ setInput(`/${command.name} `)
1059
+ setMenuDismissed(false)
1060
+ setMode("input")
1061
+ return
725
1062
  }
1063
+ void runCommand(item.value)
1064
+ }
1065
+
1066
+ const trackCommandSelection = (index: number) => {
1067
+ commandSelectIndexRef.current = index
1068
+ setCommandSelectIndex(index)
726
1069
  }
727
1070
 
728
1071
  const selectModelOption = (_index: number, option: SelectOption | null) => {
@@ -732,6 +1075,28 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
732
1075
  }
733
1076
  }
734
1077
 
1078
+ const selectReasoningOption = (_index: number, option: SelectOption | null) => {
1079
+ const item = option as ModelSelectItem | null
1080
+ if (item?.value) {
1081
+ void selectReasoning(item.value as ModelReasoningLevel)
1082
+ }
1083
+ }
1084
+
1085
+ const footerHint = buildInteractiveFooterHint({
1086
+ busy,
1087
+ slashMenuOpen: slashPaletteOpen,
1088
+ pickerMode:
1089
+ mode === "model"
1090
+ ? "model"
1091
+ : mode === "model-reasoning"
1092
+ ? "model-reasoning"
1093
+ : null,
1094
+ modelPickerPreferences:
1095
+ mode === "model"
1096
+ ? { thinking: modelThinking, fast: modelFast }
1097
+ : undefined,
1098
+ })
1099
+
735
1100
  if (bootError) {
736
1101
  return createElement(
737
1102
  "box",
@@ -788,32 +1153,36 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
788
1153
  attributes: TextAttributes.DIM,
789
1154
  })
790
1155
  : null,
791
- mode === "command"
1156
+ mode === "model-reasoning"
792
1157
  ? createElement(
793
1158
  "box",
794
1159
  {
795
1160
  border: true,
796
1161
  borderStyle: "single",
797
- borderColor: "cyan",
1162
+ borderColor: "magenta",
798
1163
  flexDirection: "column",
799
1164
  marginTop: 1,
800
1165
  paddingX: 1,
801
1166
  },
802
1167
  createElement("text", {
803
- content: "Commands",
1168
+ content: "Select reasoning effort",
804
1169
  attributes: TextAttributes.BOLD,
805
1170
  }),
806
1171
  createElement("text", {
807
- content: "Use arrows and Enter, or Escape to cancel.",
1172
+ content: `${formatModelDisplayName(pendingModelEntry?.id ?? currentModel)} · ↑↓ or j/k · 1-9 · Enter · Esc`,
808
1173
  fg: "gray",
809
1174
  }),
810
- createElement("select", {
811
- focused: mode === "command",
812
- height: commandSelectRows,
813
- options: commandItems,
814
- onSelect: selectCommandOption,
815
- showDescription: false,
816
- })
1175
+ reasoningItems.length === 0
1176
+ ? createElement("text", { content: "No reasoning levels.", fg: "yellow" })
1177
+ : createElement("select", {
1178
+ focused: mode === "model-reasoning",
1179
+ height: modelSelectRows,
1180
+ options: reasoningItems,
1181
+ selectedIndex: reasoningSelectIndex,
1182
+ onSelect: selectReasoningOption,
1183
+ showDescription: true,
1184
+ showScrollIndicator: true,
1185
+ })
817
1186
  )
818
1187
  : mode === "model"
819
1188
  ? createElement(
@@ -827,100 +1196,177 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
827
1196
  paddingX: 1,
828
1197
  },
829
1198
  createElement("text", {
830
- content: "Select a model",
1199
+ content: "Select Model and Effort",
831
1200
  attributes: TextAttributes.BOLD,
832
1201
  }),
833
1202
  createElement("text", {
834
- content: `Type to search · T thinking ${modelThinking ? "on" : "off"} · F fast ${modelFast ? "on" : "off"} · Enter choose · Escape cancel`,
1203
+ content: `Type to search · ${formatModelPickerPreferenceHint(modelThinking, modelFast)} · ↑↓ or j/k · 1-9 · Enter · Esc`,
835
1204
  fg: "gray",
836
1205
  }),
837
1206
  createElement("text", {
838
1207
  content: `Search: ${modelSearch || "all models"}`,
839
1208
  fg: "gray",
840
1209
  }),
1210
+ createElement("text", {
1211
+ content: "Access other models by running patze -m <model> or in config",
1212
+ fg: "gray",
1213
+ }),
841
1214
  modelItems.length === 0
842
1215
  ? createElement("text", { content: "No matching models.", fg: "yellow" })
843
1216
  : createElement("select", {
844
1217
  focused: mode === "model",
845
1218
  height: modelSelectRows,
846
1219
  options: modelItems,
1220
+ selectedIndex: modelSelectIndex,
847
1221
  onSelect: selectModelOption,
1222
+ showDescription: true,
848
1223
  showScrollIndicator: true,
849
1224
  })
850
1225
  )
851
1226
  : createElement(
852
1227
  "box",
853
- {
854
- border: true,
855
- borderStyle: "single",
856
- borderColor: busy ? "yellow" : "green",
857
- flexDirection: "column",
858
- marginTop: 1,
859
- paddingX: 1,
860
- },
1228
+ { flexDirection: "column", flexShrink: 0 },
1229
+ slashPaletteOpen && commandItems.length > 0
1230
+ ? createElement(
1231
+ "box",
1232
+ {
1233
+ border: true,
1234
+ borderStyle: "single",
1235
+ borderColor: "cyan",
1236
+ flexDirection: "column",
1237
+ marginTop: 1,
1238
+ paddingX: 1,
1239
+ },
1240
+ createElement("text", {
1241
+ content: "Commands",
1242
+ attributes: TextAttributes.BOLD,
1243
+ }),
1244
+ createElement("text", {
1245
+ content:
1246
+ commandMenu.advancedCount > 0
1247
+ ? `${commandMenu.advancedCount} advanced · Tab · ↑↓ or j/k · 1-9 · Enter · Esc`
1248
+ : "Tab · ↑↓ or j/k · 1-9 jump · Enter · Esc dismiss",
1249
+ fg: "gray",
1250
+ }),
1251
+ createElement("select", {
1252
+ focused: false,
1253
+ height: commandSelectRows,
1254
+ options: commandItems,
1255
+ selectedIndex: commandSelectIndex,
1256
+ onChange: trackCommandSelection,
1257
+ onSelect: selectCommandOption,
1258
+ showDescription: true,
1259
+ showScrollIndicator: true,
1260
+ })
1261
+ )
1262
+ : null,
861
1263
  createElement(
862
1264
  "box",
863
- { flexDirection: "row", alignItems: "center" },
1265
+ {
1266
+ border: true,
1267
+ borderStyle: "single",
1268
+ borderColor: busy ? "yellow" : "green",
1269
+ flexDirection: "column",
1270
+ marginTop: 1,
1271
+ paddingX: 1,
1272
+ },
1273
+ createElement(
1274
+ "box",
1275
+ { flexDirection: "row", alignItems: "center" },
1276
+ createElement("text", {
1277
+ content: "> ",
1278
+ attributes: TextAttributes.BOLD,
1279
+ }),
1280
+ createElement(TuiInput, {
1281
+ focused: ready && !busy,
1282
+ placeholder: busy
1283
+ ? activityLabel
1284
+ ? `${["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][activityFrame % 10]} ${activityLabel}… · Ctrl+C cancel`
1285
+ : "Ctrl+C cancel · waiting for agent…"
1286
+ : ready
1287
+ ? composerPlaceholder
1288
+ : "Loading Patze Code…",
1289
+ value: input,
1290
+ onInput: (value: string) => {
1291
+ setInput(value)
1292
+ resetInputHistoryNav()
1293
+ if (busy) {
1294
+ return
1295
+ }
1296
+ if (!value.startsWith("/")) {
1297
+ setMenuDismissed(false)
1298
+ }
1299
+ if (value.startsWith("/") && !value.includes(" ") && !menuDismissed) {
1300
+ setCommandSelectIndex(0)
1301
+ commandSelectIndexRef.current = 0
1302
+ }
1303
+ },
1304
+ onSubmit: submitInput,
1305
+ })
1306
+ ),
864
1307
  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,
1308
+ content: `${composerModelLabel} · ${composerPathLabel}`,
1309
+ attributes: TextAttributes.DIM,
890
1310
  })
891
- ),
892
- createElement("text", {
893
- content: `${composerModelLabel} · ${composerPathLabel}`,
894
- attributes: TextAttributes.DIM,
895
- })
1311
+ )
896
1312
  ),
897
1313
  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",
1314
+ content: footerHint,
901
1315
  attributes: TextAttributes.DIM,
902
1316
  })
903
1317
  )
904
1318
  }
905
1319
 
1320
+ function applyOpenTuiListNav(
1321
+ key: KeyEvent,
1322
+ input: {
1323
+ count: number
1324
+ index: number
1325
+ setIndex: (index: number) => void
1326
+ onEnter: () => void
1327
+ }
1328
+ ): boolean {
1329
+ if (input.count <= 0) {
1330
+ return false
1331
+ }
1332
+
1333
+ const digitSource =
1334
+ key.sequence && key.sequence.length === 1 ? key.sequence : (key.name ?? "")
1335
+ const jump = jumpListSelection(digitSource, input.count)
1336
+ if (jump !== null) {
1337
+ input.setIndex(jump)
1338
+ return true
1339
+ }
1340
+
1341
+ const nav = mapListNavKey(key.name, key.sequence)
1342
+ if (nav === "up") {
1343
+ input.setIndex(moveListSelection(input.index, -1, input.count))
1344
+ return true
1345
+ }
1346
+ if (nav === "down") {
1347
+ input.setIndex(moveListSelection(input.index, 1, input.count))
1348
+ return true
1349
+ }
1350
+ if (nav === "enter") {
1351
+ input.onEnter()
1352
+ return true
1353
+ }
1354
+ return false
1355
+ }
1356
+
906
1357
  function trimComposerPrompt(value: string): string {
907
1358
  return value.replace(/^\s+|\s+$/g, "")
908
1359
  }
909
1360
 
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
- }
1361
+ function getModelPickerSearchCharacter(key: KeyEvent): string {
1362
+ if (key.ctrl || key.meta || key.name.length !== 1) {
1363
+ return ""
922
1364
  }
923
- return `…${normalized.slice(-35)}`
1365
+ return key.shift ? key.name.toUpperCase() : key.name
1366
+ }
1367
+
1368
+ function isModelPickerSearchInput(input: string): boolean {
1369
+ return input.length > 0 && !/[\u0000-\u001F\u007F]/.test(input)
924
1370
  }
925
1371
 
926
1372
  function HeaderLine({ line }: { line: string }) {
@@ -985,31 +1431,8 @@ function partToAttributes(part: TranscriptPart) {
985
1431
  return attributes
986
1432
  }
987
1433
 
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
1434
  type SlashMenuModule = typeof import("../../dist/cli/interactive/slash-menu.js")
1000
1435
 
1001
1436
  let slashMenuModule: SlashMenuModule | null = null
1002
1437
 
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
1438
  export { parseCwdArg }