@patze/code-cli 0.24.0 → 0.41.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 (177) hide show
  1. package/CHANGELOG.md +162 -0
  2. package/VERSION +1 -1
  3. package/dist/backend/agent-trace.d.ts +1 -0
  4. package/dist/backend/agent-trace.d.ts.map +1 -1
  5. package/dist/backend/agent-trace.js +1 -0
  6. package/dist/backend/agent-trace.js.map +1 -1
  7. package/dist/backend/execute-client.d.ts +2 -0
  8. package/dist/backend/execute-client.d.ts.map +1 -1
  9. package/dist/backend/execute-client.js +2 -0
  10. package/dist/backend/execute-client.js.map +1 -1
  11. package/dist/backend/run-record.d.ts +2 -0
  12. package/dist/backend/run-record.d.ts.map +1 -1
  13. package/dist/backend/run-record.js +21 -0
  14. package/dist/backend/run-record.js.map +1 -1
  15. package/dist/backend/run-stream-client.d.ts +6 -0
  16. package/dist/backend/run-stream-client.d.ts.map +1 -1
  17. package/dist/backend/run-stream-client.js +10 -0
  18. package/dist/backend/run-stream-client.js.map +1 -1
  19. package/dist/cli/commands/agent.d.ts.map +1 -1
  20. package/dist/cli/commands/agent.js +22 -25
  21. package/dist/cli/commands/agent.js.map +1 -1
  22. package/dist/cli/commands/exec.d.ts.map +1 -1
  23. package/dist/cli/commands/exec.js +46 -8
  24. package/dist/cli/commands/exec.js.map +1 -1
  25. package/dist/cli/commands/trust-loop-snapshot.d.ts.map +1 -1
  26. package/dist/cli/commands/trust-loop-snapshot.js +19 -0
  27. package/dist/cli/commands/trust-loop-snapshot.js.map +1 -1
  28. package/dist/cli/help.d.ts.map +1 -1
  29. package/dist/cli/help.js +9 -8
  30. package/dist/cli/help.js.map +1 -1
  31. package/dist/cli/interactive/agent-event-feed.d.ts +12 -2
  32. package/dist/cli/interactive/agent-event-feed.d.ts.map +1 -1
  33. package/dist/cli/interactive/agent-event-feed.js +182 -81
  34. package/dist/cli/interactive/agent-event-feed.js.map +1 -1
  35. package/dist/cli/interactive/agent-event-parse.d.ts +4 -0
  36. package/dist/cli/interactive/agent-event-parse.d.ts.map +1 -0
  37. package/dist/cli/interactive/agent-event-parse.js +92 -0
  38. package/dist/cli/interactive/agent-event-parse.js.map +1 -0
  39. package/dist/cli/interactive/agent-events.d.ts +6 -0
  40. package/dist/cli/interactive/agent-events.d.ts.map +1 -1
  41. package/dist/cli/interactive/agent-events.js +39 -2
  42. package/dist/cli/interactive/agent-events.js.map +1 -1
  43. package/dist/cli/interactive/agent-execute-turn.d.ts +3 -0
  44. package/dist/cli/interactive/agent-execute-turn.d.ts.map +1 -1
  45. package/dist/cli/interactive/agent-execute-turn.js +13 -0
  46. package/dist/cli/interactive/agent-execute-turn.js.map +1 -1
  47. package/dist/cli/interactive/agent-stream-format.d.ts +4 -0
  48. package/dist/cli/interactive/agent-stream-format.d.ts.map +1 -1
  49. package/dist/cli/interactive/agent-stream-format.js +44 -5
  50. package/dist/cli/interactive/agent-stream-format.js.map +1 -1
  51. package/dist/cli/interactive/agent-turn-classify.d.ts.map +1 -1
  52. package/dist/cli/interactive/agent-turn-classify.js +1 -3
  53. package/dist/cli/interactive/agent-turn-classify.js.map +1 -1
  54. package/dist/cli/interactive/agent-turn-context.d.ts.map +1 -1
  55. package/dist/cli/interactive/agent-turn-context.js +7 -1
  56. package/dist/cli/interactive/agent-turn-context.js.map +1 -1
  57. package/dist/cli/interactive/agent-turn-format.js +2 -2
  58. package/dist/cli/interactive/agent-turn-format.js.map +1 -1
  59. package/dist/cli/interactive/agent-turn.d.ts +1 -0
  60. package/dist/cli/interactive/agent-turn.d.ts.map +1 -1
  61. package/dist/cli/interactive/agent-turn.js +40 -28
  62. package/dist/cli/interactive/agent-turn.js.map +1 -1
  63. package/dist/cli/interactive/auth-gate.d.ts +6 -0
  64. package/dist/cli/interactive/auth-gate.d.ts.map +1 -0
  65. package/dist/cli/interactive/auth-gate.js +29 -0
  66. package/dist/cli/interactive/auth-gate.js.map +1 -0
  67. package/dist/cli/interactive/codex-diff-feed.d.ts +14 -0
  68. package/dist/cli/interactive/codex-diff-feed.d.ts.map +1 -0
  69. package/dist/cli/interactive/codex-diff-feed.js +76 -0
  70. package/dist/cli/interactive/codex-diff-feed.js.map +1 -0
  71. package/dist/cli/interactive/codex-feed-demo.d.ts +9 -0
  72. package/dist/cli/interactive/codex-feed-demo.d.ts.map +1 -0
  73. package/dist/cli/interactive/codex-feed-demo.js +72 -0
  74. package/dist/cli/interactive/codex-feed-demo.js.map +1 -0
  75. package/dist/cli/interactive/codex-feed-format.d.ts +35 -0
  76. package/dist/cli/interactive/codex-feed-format.d.ts.map +1 -0
  77. package/dist/cli/interactive/codex-feed-format.js +142 -0
  78. package/dist/cli/interactive/codex-feed-format.js.map +1 -0
  79. package/dist/cli/interactive/codex-feed-writer.d.ts +18 -0
  80. package/dist/cli/interactive/codex-feed-writer.d.ts.map +1 -0
  81. package/dist/cli/interactive/codex-feed-writer.js +144 -0
  82. package/dist/cli/interactive/codex-feed-writer.js.map +1 -0
  83. package/dist/cli/interactive/codex-preview-feed.d.ts +13 -0
  84. package/dist/cli/interactive/codex-preview-feed.d.ts.map +1 -0
  85. package/dist/cli/interactive/codex-preview-feed.js +130 -0
  86. package/dist/cli/interactive/codex-preview-feed.js.map +1 -0
  87. package/dist/cli/interactive/composer-chrome.d.ts +20 -0
  88. package/dist/cli/interactive/composer-chrome.d.ts.map +1 -0
  89. package/dist/cli/interactive/composer-chrome.js +77 -0
  90. package/dist/cli/interactive/composer-chrome.js.map +1 -0
  91. package/dist/cli/interactive/composer-keys.d.ts +2 -1
  92. package/dist/cli/interactive/composer-keys.d.ts.map +1 -1
  93. package/dist/cli/interactive/composer-keys.js +9 -2
  94. package/dist/cli/interactive/composer-keys.js.map +1 -1
  95. package/dist/cli/interactive/composer-session-chrome.d.ts +4 -0
  96. package/dist/cli/interactive/composer-session-chrome.d.ts.map +1 -0
  97. package/dist/cli/interactive/composer-session-chrome.js +22 -0
  98. package/dist/cli/interactive/composer-session-chrome.js.map +1 -0
  99. package/dist/cli/interactive/cookbook-feed-writer.d.ts +3 -0
  100. package/dist/cli/interactive/cookbook-feed-writer.d.ts.map +1 -0
  101. package/dist/cli/interactive/cookbook-feed-writer.js +3 -0
  102. package/dist/cli/interactive/cookbook-feed-writer.js.map +1 -0
  103. package/dist/cli/interactive/cookbook-tool-feed.d.ts +13 -0
  104. package/dist/cli/interactive/cookbook-tool-feed.d.ts.map +1 -0
  105. package/dist/cli/interactive/cookbook-tool-feed.js +67 -0
  106. package/dist/cli/interactive/cookbook-tool-feed.js.map +1 -0
  107. package/dist/cli/interactive/header.d.ts +4 -1
  108. package/dist/cli/interactive/header.d.ts.map +1 -1
  109. package/dist/cli/interactive/header.js +61 -1
  110. package/dist/cli/interactive/header.js.map +1 -1
  111. package/dist/cli/interactive/line-editor.d.ts +11 -0
  112. package/dist/cli/interactive/line-editor.d.ts.map +1 -1
  113. package/dist/cli/interactive/line-editor.js +83 -11
  114. package/dist/cli/interactive/line-editor.js.map +1 -1
  115. package/dist/cli/interactive/opentui-agent-event.d.ts +10 -0
  116. package/dist/cli/interactive/opentui-agent-event.d.ts.map +1 -0
  117. package/dist/cli/interactive/opentui-agent-event.js +12 -0
  118. package/dist/cli/interactive/opentui-agent-event.js.map +1 -0
  119. package/dist/cli/interactive/plain-agent-render.d.ts +5 -4
  120. package/dist/cli/interactive/plain-agent-render.d.ts.map +1 -1
  121. package/dist/cli/interactive/plain-agent-render.js +12 -44
  122. package/dist/cli/interactive/plain-agent-render.js.map +1 -1
  123. package/dist/cli/interactive/plain-codex-feed.d.ts +11 -0
  124. package/dist/cli/interactive/plain-codex-feed.d.ts.map +1 -0
  125. package/dist/cli/interactive/plain-codex-feed.js +37 -0
  126. package/dist/cli/interactive/plain-codex-feed.js.map +1 -0
  127. package/dist/cli/interactive/reset-agent.d.ts +8 -0
  128. package/dist/cli/interactive/reset-agent.d.ts.map +1 -0
  129. package/dist/cli/interactive/reset-agent.js +23 -0
  130. package/dist/cli/interactive/reset-agent.js.map +1 -0
  131. package/dist/cli/interactive/run-replay.d.ts.map +1 -1
  132. package/dist/cli/interactive/run-replay.js +3 -0
  133. package/dist/cli/interactive/run-replay.js.map +1 -1
  134. package/dist/cli/interactive/session-controller.d.ts +1 -0
  135. package/dist/cli/interactive/session-controller.d.ts.map +1 -1
  136. package/dist/cli/interactive/session-controller.js +24 -10
  137. package/dist/cli/interactive/session-controller.js.map +1 -1
  138. package/dist/cli/interactive/session-hints.d.ts +4 -0
  139. package/dist/cli/interactive/session-hints.d.ts.map +1 -0
  140. package/dist/cli/interactive/session-hints.js +28 -0
  141. package/dist/cli/interactive/session-hints.js.map +1 -0
  142. package/dist/cli/interactive/session.d.ts +3 -1
  143. package/dist/cli/interactive/session.d.ts.map +1 -1
  144. package/dist/cli/interactive/session.js +8 -2
  145. package/dist/cli/interactive/session.js.map +1 -1
  146. package/dist/cli/interactive/shell.d.ts.map +1 -1
  147. package/dist/cli/interactive/shell.js +61 -2
  148. package/dist/cli/interactive/shell.js.map +1 -1
  149. package/dist/cli/interactive/slash-dispatch.d.ts +6 -0
  150. package/dist/cli/interactive/slash-dispatch.d.ts.map +1 -1
  151. package/dist/cli/interactive/slash-dispatch.js +29 -5
  152. package/dist/cli/interactive/slash-dispatch.js.map +1 -1
  153. package/dist/cli/interactive/slash-menu-core.d.ts +10 -0
  154. package/dist/cli/interactive/slash-menu-core.d.ts.map +1 -0
  155. package/dist/cli/interactive/slash-menu-core.js +34 -0
  156. package/dist/cli/interactive/slash-menu-core.js.map +1 -0
  157. package/dist/cli/interactive/slash-menu.d.ts +3 -0
  158. package/dist/cli/interactive/slash-menu.d.ts.map +1 -1
  159. package/dist/cli/interactive/slash-menu.js +16 -4
  160. package/dist/cli/interactive/slash-menu.js.map +1 -1
  161. package/dist/cli/interactive/slash-registry.d.ts.map +1 -1
  162. package/dist/cli/interactive/slash-registry.js +4 -1
  163. package/dist/cli/interactive/slash-registry.js.map +1 -1
  164. package/dist/cli/interactive/trace-glyphs.d.ts +1 -0
  165. package/dist/cli/interactive/trace-glyphs.d.ts.map +1 -1
  166. package/dist/cli/interactive/trace-glyphs.js +47 -1
  167. package/dist/cli/interactive/trace-glyphs.js.map +1 -1
  168. package/dist/cli/interactive/transcript-upsert.d.ts.map +1 -1
  169. package/dist/cli/interactive/transcript-upsert.js +10 -6
  170. package/dist/cli/interactive/transcript-upsert.js.map +1 -1
  171. package/dist/cli/one-shot-args.d.ts +1 -0
  172. package/dist/cli/one-shot-args.d.ts.map +1 -1
  173. package/dist/cli/one-shot-args.js +3 -2
  174. package/dist/cli/one-shot-args.js.map +1 -1
  175. package/opentui/src/App.tsx +233 -85
  176. package/opentui/src/transcript-render.ts +144 -2
  177. package/package.json +1 -1
@@ -12,7 +12,6 @@ import {
12
12
  } from "@opentui/react"
13
13
  import { createElement, useCallback, useEffect, useMemo, useRef, useState } from "react"
14
14
 
15
- import { parseTranscriptLine } from "./line-parse.js"
16
15
  import { importPatzeDist } from "./patze-dist.js"
17
16
  import {
18
17
  buildTranscriptLines,
@@ -23,7 +22,7 @@ import {
23
22
  } from "./transcript-render.js"
24
23
  import { classifyPlainOutput, createTranscriptSink, inferErrorKind } from "./tui-sink.js"
25
24
  import { upsertTranscriptEntry } from "../../dist/cli/interactive/transcript-upsert.js"
26
- import { applyAgentEvent } from "../../dist/cli/interactive/agent-event-feed.js"
25
+ import { applyOpenTuiAgentEvent } from "../../dist/cli/interactive/opentui-agent-event.js"
27
26
  import type { AgentEvent } from "../../dist/cli/interactive/agent-events.js"
28
27
 
29
28
  extend({ "tui-input": InputRenderable })
@@ -58,6 +57,10 @@ type InteractiveController = {
58
57
  session: {
59
58
  getModelOverride: () => string | null
60
59
  setModelOverride: (value: string | null) => void
60
+ getModelThinking: () => boolean | null
61
+ setModelThinking: (value: boolean | null) => void
62
+ getModelFast: () => boolean | null
63
+ setModelFast: (value: boolean | null) => void
61
64
  getExecutionMode: () => "local" | "cloud"
62
65
  snapshot: () => Array<{ input: string; lines: string[] }>
63
66
  }
@@ -73,7 +76,7 @@ type InteractiveController = {
73
76
  useActivitySpinner?: boolean
74
77
  onAgentEvent?: (event: AgentEvent) => void
75
78
  }
76
- ) => Promise<{ exitShell: boolean; exitCode: number; streamed?: boolean }>
79
+ ) => Promise<{ exitShell: boolean; exitCode: number; streamed?: boolean; clearVisibleTranscript?: boolean }>
77
80
  }
78
81
 
79
82
  function parseCwdArg(): string {
@@ -107,10 +110,15 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
107
110
  const [scrollOffset, setScrollOffset] = useState(0)
108
111
  const [transcript, setTranscript] = useState<TranscriptEntry[]>([])
109
112
  const [currentModel, setCurrentModel] = useState("composer-2.5-fast")
113
+ const [modelThinking, setModelThinking] = useState<boolean | null>(null)
114
+ const [modelFast, setModelFast] = useState<boolean | null>(null)
110
115
  const [executionMode, setExecutionMode] = useState<"local" | "cloud">("local")
111
116
  const [executionTarget, setExecutionTarget] = useState<string | null>(null)
112
117
  const [supportedModels, setSupportedModels] = useState<string[]>([])
113
118
  const [slashMenuReady, setSlashMenuReady] = useState(false)
119
+ const [startupHeader, setStartupHeader] = useState<string[]>([])
120
+ const [composerPlaceholder, setComposerPlaceholder] = useState("Describe a task… · / for commands")
121
+ const [emptySessionHints, setEmptySessionHints] = useState<string[]>([])
114
122
 
115
123
  const followTranscript = useCallback(() => {
116
124
  if (scrollPinnedRef.current) {
@@ -194,6 +202,14 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
194
202
  streamingIdRef.current = null
195
203
  }, [])
196
204
 
205
+ const openTuiStreamHandlers = useMemo(
206
+ () => ({
207
+ onAssistantDelta: upsertStreaming,
208
+ onClearAssistantStream: clearStreaming,
209
+ }),
210
+ [clearStreaming, upsertStreaming]
211
+ )
212
+
197
213
  const refreshStatusMeta = useCallback(async () => {
198
214
  const controller = controllerRef.current
199
215
  if (!controller) {
@@ -213,7 +229,26 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
213
229
  })
214
230
  )
215
231
  setExecutionMode(controller.session.getExecutionMode())
216
- setSupportedModels([...models.PATZE_SUPPORTED_MODELS])
232
+ setModelThinking(controller.session.getModelThinking())
233
+ setModelFast(controller.session.getModelFast())
234
+
235
+ let modelIds = [...models.PATZE_SUPPORTED_MODELS]
236
+ const apiKey = process.env.CURSOR_API_KEY?.trim()
237
+ if (apiKey) {
238
+ try {
239
+ const { Cursor } = await import("@cursor/sdk")
240
+ const listed = await Cursor.models.list({ apiKey })
241
+ const ids = listed
242
+ .map((item) => String(item.id ?? "").trim())
243
+ .filter(Boolean)
244
+ if (ids.length > 0) {
245
+ modelIds = ids
246
+ }
247
+ } catch {
248
+ // keep bundled fallback list
249
+ }
250
+ }
251
+ setSupportedModels(modelIds)
217
252
 
218
253
  const cloudGit = await importPatzeDist<
219
254
  typeof import("../../dist/cli/interactive/cloud-git.js")
@@ -226,6 +261,16 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
226
261
  } else {
227
262
  setExecutionTarget(null)
228
263
  }
264
+
265
+ const authGate = await importPatzeDist<
266
+ typeof import("../../dist/cli/interactive/auth-gate.js")
267
+ >("cli/interactive/auth-gate.js")
268
+ setComposerPlaceholder(authGate.resolveComposerPlaceholder(controller.cwd))
269
+
270
+ const sessionHints = await importPatzeDist<
271
+ typeof import("../../dist/cli/interactive/session-hints.js")
272
+ >("cli/interactive/session-hints.js")
273
+ setEmptySessionHints(sessionHints.buildEmptySessionHints(controller.cwd))
229
274
  }, [])
230
275
 
231
276
  useEffect(() => {
@@ -254,10 +299,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
254
299
  const controller = (await sessionMod.prepareInteractiveController({
255
300
  cwd,
256
301
  interactive: true,
257
- streamPartials: true,
258
- onPartial: (chunk: string) => {
259
- upsertStreaming(chunk)
260
- },
302
+ streamPartials: false,
261
303
  })) as InteractiveController
262
304
 
263
305
  if (cancelled) {
@@ -265,21 +307,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
265
307
  }
266
308
 
267
309
  controllerRef.current = controller
268
-
269
- const headerEntries = controller.renderHeaderLines().flatMap((line) => {
270
- const parsed = parseTranscriptLine(line)
271
- if (!parsed || parsed.continuation) {
272
- return []
273
- }
274
- return [
275
- {
276
- id: nextId(),
277
- kind: "meta" as const,
278
- label: parsed.label || "info",
279
- text: parsed.text || line,
280
- },
281
- ]
282
- })
310
+ setStartupHeader(controller.renderHeaderLines())
283
311
 
284
312
  const restored: TranscriptEntry[] = []
285
313
  for (const turn of controller.session.snapshot()) {
@@ -294,7 +322,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
294
322
  }
295
323
  }
296
324
 
297
- setTranscript([...headerEntries, ...restored])
325
+ setTranscript(restored)
298
326
  await refreshStatusMeta()
299
327
  setReady(true)
300
328
  } catch (error) {
@@ -306,7 +334,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
306
334
  return () => {
307
335
  cancelled = true
308
336
  }
309
- }, [cwd, nextId, refreshStatusMeta, upsertStreaming])
337
+ }, [cwd, nextId, refreshStatusMeta])
310
338
 
311
339
  const exitApp = useCallback(() => {
312
340
  controllerRef.current?.persistSession()
@@ -320,7 +348,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
320
348
  return
321
349
  }
322
350
 
323
- const value = rawLine.trim()
351
+ const value = trimComposerPrompt(rawLine)
324
352
  if (!value) {
325
353
  return
326
354
  }
@@ -351,16 +379,22 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
351
379
  const result = await controller.processLine(value, sink, {
352
380
  signal: abort.signal,
353
381
  onActivity: setActivityLabel,
354
- useActivitySpinner: false,
382
+ useActivitySpinner: true,
355
383
  onAgentEvent: (event) => {
356
384
  setActivityLabel(null)
357
- setTranscript((items) => applyAgentEvent(items, event, turnAssistantId))
385
+ setTranscript((items) =>
386
+ applyOpenTuiAgentEvent(items, event, turnAssistantId, openTuiStreamHandlers)
387
+ )
358
388
  followTranscript()
359
389
  },
360
390
  })
361
391
  if (result.streamed) {
362
392
  clearStreaming()
363
393
  }
394
+ if (result.clearVisibleTranscript) {
395
+ setTranscript([])
396
+ markScrollPinned()
397
+ }
364
398
  await refreshStatusMeta()
365
399
  if (result.exitShell) {
366
400
  exitApp()
@@ -380,6 +414,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
380
414
  followTranscript,
381
415
  markScrollPinned,
382
416
  nextId,
417
+ openTuiStreamHandlers,
383
418
  refreshStatusMeta,
384
419
  upsertTranscriptLine,
385
420
  upsertStreaming,
@@ -391,6 +426,23 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
391
426
  setMode("model")
392
427
  }, [])
393
428
 
429
+ const toggleModelPreference = useCallback((kind: "thinking" | "fast") => {
430
+ const controller = controllerRef.current
431
+ if (!controller) {
432
+ return
433
+ }
434
+ if (kind === "thinking") {
435
+ const next = controller.session.getModelThinking() === true ? null : true
436
+ controller.session.setModelThinking(next)
437
+ setModelThinking(next)
438
+ } else {
439
+ const next = controller.session.getModelFast() === true ? null : true
440
+ controller.session.setModelFast(next)
441
+ setModelFast(next)
442
+ }
443
+ controller.persistSession()
444
+ }, [])
445
+
394
446
  const runCommand = useCallback(
395
447
  async (rawCommand: string) => {
396
448
  const trimmed = rawCommand.trim()
@@ -407,8 +459,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
407
459
 
408
460
  switch (name?.toLowerCase()) {
409
461
  case "help":
410
- setInput("/")
411
- setMode("command")
462
+ await submitToController("/help")
412
463
  return
413
464
  case "model":
414
465
  if (!args.trim()) {
@@ -431,7 +482,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
431
482
 
432
483
  const submitInput = useCallback(
433
484
  (value: string) => {
434
- const prompt = value.trim()
485
+ const prompt = trimComposerPrompt(value)
435
486
  setInput("")
436
487
 
437
488
  if (!prompt || busy) {
@@ -473,41 +524,26 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
473
524
  const modelSelectRows = Math.min(8, Math.max(3, rows - 10))
474
525
  const commandPanelRows = 2 + commandSelectRows
475
526
  const modelPanelRows = 3 + modelSelectRows
527
+ const headerRows = Math.max(1, startupHeader.length)
528
+ const composerChromeRows = 5
529
+ const bottomHintRows = 1
476
530
 
477
531
  const transcriptViewportRows = Math.max(
478
532
  4,
479
- rows - (mode === "model" ? modelPanelRows + 4 : mode === "command" ? commandPanelRows + 4 : 6)
533
+ rows -
534
+ headerRows -
535
+ bottomHintRows -
536
+ (mode === "model"
537
+ ? modelPanelRows + 2
538
+ : mode === "command"
539
+ ? commandPanelRows + 2
540
+ : composerChromeRows)
480
541
  )
481
542
 
482
- const scrollableEntries = useMemo(
483
- () => [
484
- { id: "status-cwd", kind: "meta" as const, label: "cwd", text: cwd },
485
- {
486
- id: "status-mode",
487
- kind: "meta" as const,
488
- label: "mode",
489
- text: executionMode,
490
- },
491
- ...(executionTarget
492
- ? [
493
- {
494
- id: "status-target",
495
- kind: "meta" as const,
496
- label: "target",
497
- text: executionTarget,
498
- },
499
- ]
500
- : []),
501
- {
502
- id: "status-model",
503
- kind: "meta" as const,
504
- label: "model",
505
- text: currentModel,
506
- },
507
- ...transcript,
508
- ],
509
- [cwd, currentModel, executionMode, executionTarget, transcript]
510
- )
543
+ const composerModelLabel = `${currentModel}${modelThinking ? " · thinking" : ""}${modelFast ? " · fast" : ""}`
544
+ const composerPathLabel = shortenComposerPath(cwd)
545
+
546
+ const scrollableEntries = useMemo(() => transcript, [transcript])
511
547
 
512
548
  const transcriptLines = useMemo(
513
549
  () => buildTranscriptLines(scrollableEntries, columns),
@@ -548,6 +584,26 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
548
584
  }))
549
585
  }, [currentModel, modelSearch, supportedModels])
550
586
 
587
+ const toggleFeedExpand = useCallback((target: "think" | "diff") => {
588
+ setTranscript((items) => {
589
+ for (let index = items.length - 1; index >= 0; index -= 1) {
590
+ const entry = items[index]
591
+ const matchesThink = target === "think" && entry.label === "think" && entry.detailText
592
+ const matchesDiff =
593
+ target === "diff" && entry.kind === "meta" && entry.label === "diff" && entry.detailText
594
+ if (matchesThink || matchesDiff) {
595
+ return items.map((candidate, candidateIndex) =>
596
+ candidateIndex === index
597
+ ? { ...candidate, expanded: !candidate.expanded }
598
+ : candidate
599
+ )
600
+ }
601
+ }
602
+ return items
603
+ })
604
+ followTranscript()
605
+ }, [followTranscript])
606
+
551
607
  useKeyboard((key: KeyEvent) => {
552
608
  const character = getInputCharacter(key)
553
609
 
@@ -571,6 +627,10 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
571
627
  if (mode === "model") {
572
628
  if (key.name === "backspace" || key.name === "delete") {
573
629
  setModelSearch((value) => value.slice(0, -1))
630
+ } else if (character === "T") {
631
+ toggleModelPreference("thinking")
632
+ } else if (character === "F") {
633
+ toggleModelPreference("fast")
574
634
  } else if (isSearchInput(character)) {
575
635
  setModelSearch((value) => `${value}${character}`)
576
636
  }
@@ -586,6 +646,21 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
586
646
  if (mode === "input") {
587
647
  const pageSize = Math.max(1, transcriptViewportRows - 1)
588
648
 
649
+ if (key.ctrl && key.name === "t") {
650
+ toggleFeedExpand("think")
651
+ return
652
+ }
653
+
654
+ if (key.ctrl && key.name === "d") {
655
+ toggleFeedExpand("diff")
656
+ return
657
+ }
658
+
659
+ if (key.shift && (key.name === "return" || key.name === "enter")) {
660
+ setInput((value) => `${value}\n`)
661
+ return
662
+ }
663
+
589
664
  if (key.name === "up") {
590
665
  markScrollUnpinned()
591
666
  setScrollOffset((offset) => Math.min(maxScrollOffset, offset + 1))
@@ -646,9 +721,27 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
646
721
  return createElement(
647
722
  "box",
648
723
  { flexDirection: "column", height: rows, paddingX: 1 },
724
+ startupHeader.length
725
+ ? createElement(
726
+ "box",
727
+ { flexDirection: "column", flexShrink: 0, marginBottom: 1 },
728
+ startupHeader.map((line, index) =>
729
+ createElement(HeaderLine, { key: `header-${index}`, line })
730
+ )
731
+ )
732
+ : null,
649
733
  createElement(
650
734
  "box",
651
735
  { flexDirection: "column", height: transcriptViewportRows },
736
+ transcript.length === 0 && emptySessionHints.length
737
+ ? emptySessionHints.map((hint, index) =>
738
+ createElement("text", {
739
+ key: `hint-${index}`,
740
+ content: hint,
741
+ attributes: TextAttributes.DIM,
742
+ })
743
+ )
744
+ : null,
652
745
  visibleTranscriptLines.map((line) =>
653
746
  createElement(TranscriptLine, { key: line.id, line })
654
747
  )
@@ -662,6 +755,13 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
662
755
  }`
663
756
  )
664
757
  : null,
758
+ busy && activityLabel
759
+ ? createElement("text", {
760
+ content: `${["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][activityFrame % 10]} ${activityLabel}…`,
761
+ fg: "yellow",
762
+ attributes: TextAttributes.DIM,
763
+ })
764
+ : null,
665
765
  mode === "command"
666
766
  ? createElement(
667
767
  "box",
@@ -705,7 +805,7 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
705
805
  attributes: TextAttributes.BOLD,
706
806
  }),
707
807
  createElement("text", {
708
- content: "Type to search · Enter choose · Escape cancel",
808
+ content: `Type to search · T thinking ${modelThinking ? "on" : "off"} · F fast ${modelFast ? "on" : "off"} · Enter choose · Escape cancel`,
709
809
  fg: "gray",
710
810
  }),
711
811
  createElement("text", {
@@ -728,42 +828,90 @@ export function App({ cwd: cwdArg }: { cwd?: string }) {
728
828
  border: true,
729
829
  borderStyle: "single",
730
830
  borderColor: busy ? "yellow" : "green",
831
+ flexDirection: "column",
731
832
  marginTop: 1,
732
833
  paddingX: 1,
733
834
  },
734
- createElement(TuiInput, {
735
- focused: ready && !busy,
736
- placeholder: busy
737
- ? activityLabel
738
- ? `${["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][activityFrame % 10]} ${activityLabel}… · Ctrl+C cancel`
739
- : "Ctrl+C cancel · waiting for agent…"
740
- : ready
741
- ? "Ask or type /help"
742
- : "Loading Patze Code…",
743
- value: input,
744
- onInput: (value: string) => {
745
- setInput(value)
746
- if (busy) {
747
- return
748
- }
749
- if (value.startsWith("/") && !value.includes(" ")) {
750
- setMode("command")
751
- } else {
752
- setMode("input")
753
- }
754
- },
755
- onSubmit: submitInput,
835
+ createElement(
836
+ "box",
837
+ { flexDirection: "row", alignItems: "center" },
838
+ createElement("text", {
839
+ content: "> ",
840
+ attributes: TextAttributes.BOLD,
841
+ }),
842
+ createElement(TuiInput, {
843
+ focused: ready && !busy,
844
+ placeholder: busy
845
+ ? activityLabel
846
+ ? `${["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"][activityFrame % 10]} ${activityLabel}… · Ctrl+C cancel`
847
+ : "Ctrl+C cancel · waiting for agent…"
848
+ : ready
849
+ ? composerPlaceholder
850
+ : "Loading Patze Code…",
851
+ value: input,
852
+ onInput: (value: string) => {
853
+ setInput(value)
854
+ if (busy) {
855
+ return
856
+ }
857
+ if (value.startsWith("/") && !value.includes(" ")) {
858
+ setMode("command")
859
+ } else {
860
+ setMode("input")
861
+ }
862
+ },
863
+ onSubmit: submitInput,
864
+ })
865
+ ),
866
+ createElement("text", {
867
+ content: `${composerModelLabel} · ${composerPathLabel}`,
868
+ attributes: TextAttributes.DIM,
756
869
  })
757
870
  ),
758
871
  createElement("text", {
759
872
  content: busy
760
873
  ? "Esc exit · Ctrl+C cancel run · agent executing"
761
- : "Esc exit · Ctrl+C quit · /diff · /approve · /apply when patch ready",
874
+ : "Esc exit · Ctrl+C quit · / commands · ctrl+t thought · ctrl+d diff · /status",
762
875
  attributes: TextAttributes.DIM,
763
876
  })
764
877
  )
765
878
  }
766
879
 
880
+ function trimComposerPrompt(value: string): string {
881
+ return value.replace(/^\s+|\s+$/g, "")
882
+ }
883
+
884
+ function shortenComposerPath(path: string): string {
885
+ const normalized = String(path || "").trim().replace(/\\/g, "/")
886
+ if ([...normalized].length <= 36) {
887
+ return normalized
888
+ }
889
+ const parts = normalized.split("/").filter(Boolean)
890
+ if (parts.length >= 2) {
891
+ const tail = `${parts[parts.length - 2]}/${parts[parts.length - 1]}`
892
+ const candidate = `…/${tail}`
893
+ if ([...candidate].length <= 36) {
894
+ return candidate
895
+ }
896
+ }
897
+ return `…${normalized.slice(-35)}`
898
+ }
899
+
900
+ function HeaderLine({ line }: { line: string }) {
901
+ const plain = stripAnsi(line)
902
+ const dim = line.includes("\x1b[2m") || plain.startsWith("Tip:") || plain.includes("Beta ·")
903
+ const warn = line.includes("\x1b[33m") || plain.startsWith("Heads up:")
904
+ return createElement("text", {
905
+ content: plain,
906
+ fg: warn ? "yellow" : dim ? "gray" : undefined,
907
+ attributes: dim ? TextAttributes.DIM : TextAttributes.NONE,
908
+ })
909
+ }
910
+
911
+ function stripAnsi(text: string): string {
912
+ return text.replace(/\x1b\[[0-9;]*m/g, "")
913
+ }
914
+
767
915
  function TranscriptLine({ line }: { line: TranscriptLine }) {
768
916
  const color = {
769
917
  assistant: "white",
@@ -12,6 +12,9 @@ export type TranscriptEntry = {
12
12
  label: string
13
13
  text: string
14
14
  upsertKey?: string
15
+ /** Full thinking body when collapsed summary is truncated. */
16
+ detailText?: string
17
+ expanded?: boolean
15
18
  }
16
19
 
17
20
  export type TranscriptPart = {
@@ -37,17 +40,65 @@ export function buildTranscriptLines(entries: TranscriptEntry[], columns: number
37
40
  entry.kind === "assistant"
38
41
  ? renderMarkdownLines(entry.text || "...", textWidth)
39
42
  : wrapText(entry.text || "...", textWidth).map((text) => ({
40
- parts: [{ text }],
43
+ parts: parseCodexFeedParts(entry.kind, entry.label, text),
41
44
  }))
42
45
  )
43
46
 
44
- return renderedLines.map((line, index) => ({
47
+ const lines = renderedLines.map((line, index) => ({
45
48
  id: `${entry.id}-${index}`,
46
49
  kind: entry.kind,
47
50
  label: index === 0 ? entry.label : "",
48
51
  parts: line.parts,
49
52
  }))
53
+
54
+ if (entry.label === "think" && entry.detailText) {
55
+ return appendExpandableDetailLines(lines, entry, textWidth, {
56
+ hint: ` (+${entry.detailText.length} chars · ctrl+t expand)`,
57
+ renderDetailLine: (text) => [{ text: ` ${text}`, color: "gray" as const, dimColor: true }],
58
+ })
59
+ }
60
+
61
+ if (entry.kind === "meta" && entry.label === "diff" && entry.detailText) {
62
+ return appendExpandableDetailLines(lines, entry, textWidth, {
63
+ hint: ` (+${entry.detailText.split("\n").length} lines · ctrl+d expand)`,
64
+ renderDetailLine: (text) => parseCodexDiffParts(text.startsWith(" ") ? text : ` ${text}`),
65
+ })
66
+ }
67
+
68
+ return lines
69
+ })
70
+ }
71
+
72
+ function appendExpandableDetailLines(
73
+ lines: TranscriptLine[],
74
+ entry: TranscriptEntry,
75
+ textWidth: number,
76
+ options: {
77
+ hint: string
78
+ renderDetailLine: (text: string) => TranscriptPart[]
79
+ }
80
+ ): TranscriptLine[] {
81
+ if (entry.expanded) {
82
+ const detailLines = entry.detailText!
83
+ .split("\n")
84
+ .flatMap((rawLine, index) =>
85
+ wrapText(rawLine, textWidth - 2).map((text, wrapIndex) => ({
86
+ id: `${entry.id}-detail-${index}-${wrapIndex}`,
87
+ kind: entry.kind,
88
+ label: "",
89
+ parts: options.renderDetailLine(text),
90
+ }))
91
+ )
92
+ return [...lines, ...detailLines]
93
+ }
94
+
95
+ lines.push({
96
+ id: `${entry.id}-expand-hint`,
97
+ kind: entry.kind,
98
+ label: "",
99
+ parts: [{ text: options.hint, color: "gray", dimColor: true }],
50
100
  })
101
+ return lines
51
102
  }
52
103
 
53
104
  function trimTrailingBlankLines(lines: Array<{ parts: TranscriptPart[] }>) {
@@ -156,6 +207,97 @@ function renderMarkdownLines(value: string, width: number) {
156
207
  return lines
157
208
  }
158
209
 
210
+ function parseCodexFeedParts(kind: TranscriptEntryKind, label: string, text: string): TranscriptPart[] {
211
+ if (kind === "meta" && label === "diff") {
212
+ return parseCodexDiffParts(text)
213
+ }
214
+
215
+ if (kind === "tool") {
216
+ const match = text.match(/^([…✓✗])\s+(.*)$/)
217
+ if (match) {
218
+ const glyph = match[1]
219
+ const body = match[2] ?? ""
220
+ const glyphColor =
221
+ glyph === "✓" ? "green" : glyph === "✗" ? "red" : ("yellow" as const)
222
+ const bodyParts = parseCodexToolBodyParts(body)
223
+ return [{ text: `${glyph} `, color: glyphColor, bold: true }, ...bodyParts]
224
+ }
225
+ return parseCodexToolBodyParts(text)
226
+ }
227
+
228
+ if (kind === "status" || (kind === "meta" && label === "done")) {
229
+ if (text.startsWith("→ ")) {
230
+ return [{ text, color: "cyan" }]
231
+ }
232
+ if (text.startsWith("…")) {
233
+ return [{ text, color: "gray", dimColor: true }]
234
+ }
235
+ if (text.startsWith("✓")) {
236
+ return [{ text, color: "green" }]
237
+ }
238
+ if (text.startsWith("✗")) {
239
+ return [{ text, color: "red" }]
240
+ }
241
+ if (text.startsWith("○")) {
242
+ return [{ text, color: "yellow" }]
243
+ }
244
+ if (text.startsWith("●")) {
245
+ return [{ text, color: "cyan", bold: true }]
246
+ }
247
+ }
248
+
249
+ if (kind === "status" && label === "think") {
250
+ if (text.startsWith("…")) {
251
+ return [{ text, color: "gray", dimColor: true }]
252
+ }
253
+ if (text.startsWith("✓")) {
254
+ return [{ text, color: "green" }]
255
+ }
256
+ }
257
+
258
+ if (kind === "meta") {
259
+ if (text.startsWith("✓ completed")) {
260
+ return [{ text, color: "green", bold: true }]
261
+ }
262
+ if (text.startsWith("deliverables")) {
263
+ return [{ text, color: "cyan" }]
264
+ }
265
+ }
266
+
267
+ return [{ text }]
268
+ }
269
+
270
+ function parseCodexToolBodyParts(body: string): TranscriptPart[] {
271
+ if (body.startsWith("+ create")) {
272
+ return [{ text: body, color: "green" }]
273
+ }
274
+ if (body.startsWith("~ modify")) {
275
+ return [{ text: body, color: "yellow" }]
276
+ }
277
+ if (body.startsWith("- delete")) {
278
+ return [{ text: body, color: "red" }]
279
+ }
280
+ if (body.startsWith("▸")) {
281
+ return [{ text: body, color: "magenta" }]
282
+ }
283
+ return [{ text: body }]
284
+ }
285
+
286
+ function parseCodexDiffParts(text: string): TranscriptPart[] {
287
+ const trimmed = text.trim()
288
+ if (trimmed === "…") {
289
+ return [{ text: " …", color: "gray", dimColor: true }]
290
+ }
291
+ const line = text.startsWith(" ") ? text : ` ${text}`
292
+ if (line.startsWith(" - ")) {
293
+ return [{ text: line, color: "red" }]
294
+ }
295
+ if (line.startsWith(" + ")) {
296
+ return [{ text: line, color: "green" }]
297
+ }
298
+ return [{ text: line, color: "gray", dimColor: true }]
299
+ }
300
+
159
301
  function wrapText(value: string, width: number) {
160
302
  const lines: string[] = []
161
303