@patze/code-cli 0.16.1 → 0.17.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 (44) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/VERSION +1 -1
  3. package/dist/backend/run-stream-client.d.ts +1 -0
  4. package/dist/backend/run-stream-client.d.ts.map +1 -1
  5. package/dist/backend/run-stream-client.js +17 -0
  6. package/dist/backend/run-stream-client.js.map +1 -1
  7. package/dist/cli/interactive/agent-execute-turn.d.ts +1 -0
  8. package/dist/cli/interactive/agent-execute-turn.d.ts.map +1 -1
  9. package/dist/cli/interactive/agent-execute-turn.js +24 -1
  10. package/dist/cli/interactive/agent-execute-turn.js.map +1 -1
  11. package/dist/cli/interactive/agent-turn.d.ts +1 -0
  12. package/dist/cli/interactive/agent-turn.d.ts.map +1 -1
  13. package/dist/cli/interactive/agent-turn.js +1 -0
  14. package/dist/cli/interactive/agent-turn.js.map +1 -1
  15. package/dist/cli/interactive/composer-keys.d.ts +11 -0
  16. package/dist/cli/interactive/composer-keys.d.ts.map +1 -0
  17. package/dist/cli/interactive/composer-keys.js +19 -0
  18. package/dist/cli/interactive/composer-keys.js.map +1 -0
  19. package/dist/cli/interactive/line-editor.d.ts +4 -0
  20. package/dist/cli/interactive/line-editor.d.ts.map +1 -1
  21. package/dist/cli/interactive/line-editor.js +51 -17
  22. package/dist/cli/interactive/line-editor.js.map +1 -1
  23. package/dist/cli/interactive/session-controller.d.ts +4 -1
  24. package/dist/cli/interactive/session-controller.d.ts.map +1 -1
  25. package/dist/cli/interactive/session-controller.js +2 -1
  26. package/dist/cli/interactive/session-controller.js.map +1 -1
  27. package/dist/cli/interactive/slash-dispatch.d.ts +1 -0
  28. package/dist/cli/interactive/slash-dispatch.d.ts.map +1 -1
  29. package/dist/cli/interactive/slash-dispatch.js +1 -0
  30. package/dist/cli/interactive/slash-dispatch.js.map +1 -1
  31. package/dist/cli/interactive/transcript-upsert.d.ts +12 -0
  32. package/dist/cli/interactive/transcript-upsert.d.ts.map +1 -0
  33. package/dist/cli/interactive/transcript-upsert.js +33 -0
  34. package/dist/cli/interactive/transcript-upsert.js.map +1 -0
  35. package/opentui/package.json +18 -0
  36. package/opentui/scripts/assert-parse.mjs +21 -0
  37. package/opentui/src/App.tsx +806 -0
  38. package/opentui/src/line-parse.ts +54 -0
  39. package/opentui/src/main.tsx +45 -0
  40. package/opentui/src/patze-dist.ts +11 -0
  41. package/opentui/src/transcript-render.ts +218 -0
  42. package/opentui/src/tui-sink.ts +78 -0
  43. package/opentui/tsconfig.json +13 -0
  44. package/package.json +6 -2
@@ -0,0 +1,54 @@
1
+ import type { TranscriptEntryKind } from "./transcript-render.js"
2
+
3
+ const ANSI_PATTERN = /\x1b\[[0-9;]*m/g
4
+ const LABEL_WIDTH = 7
5
+
6
+ const ROLE_MAP: Record<string, TranscriptEntryKind> = {
7
+ you: "user",
8
+ agent: "assistant",
9
+ tool: "tool",
10
+ run: "status",
11
+ }
12
+
13
+ export function stripAnsi(value: string): string {
14
+ return value.replace(ANSI_PATTERN, "")
15
+ }
16
+
17
+ export function parseTranscriptLine(raw: string): {
18
+ kind: TranscriptEntryKind
19
+ label: string
20
+ text: string
21
+ continuation: boolean
22
+ } | null {
23
+ const line = stripAnsi(raw)
24
+ if (!line.trim()) {
25
+ return null
26
+ }
27
+
28
+ if (line.startsWith(" ".repeat(LABEL_WIDTH))) {
29
+ return {
30
+ kind: "assistant",
31
+ label: "",
32
+ text: line.slice(LABEL_WIDTH),
33
+ continuation: true,
34
+ }
35
+ }
36
+
37
+ const match = line.match(/^(you|agent|tool|run)\s*(.*)$/i)
38
+ if (!match) {
39
+ return {
40
+ kind: "status",
41
+ label: "info",
42
+ text: line,
43
+ continuation: false,
44
+ }
45
+ }
46
+
47
+ const role = match[1].toLowerCase()
48
+ return {
49
+ kind: ROLE_MAP[role] ?? "status",
50
+ label: role,
51
+ text: match[2] ?? "",
52
+ continuation: false,
53
+ }
54
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Patze Code OpenTUI shell (cookbook-style TUI).
3
+ * Default when Bun + deps are available; opt out with PATZE_USE_JS=1 or PATZE_USE_OPENTUI=0.
4
+ */
5
+ import { CliRenderEvents, type CliRenderer, createCliRenderer } from "@opentui/core"
6
+ import { createRoot } from "@opentui/react"
7
+ import { createElement } from "react"
8
+
9
+ import { App, parseCwdArg } from "./App.tsx"
10
+
11
+ function waitUntilDestroyed(renderer: CliRenderer) {
12
+ if (renderer.isDestroyed) {
13
+ return Promise.resolve()
14
+ }
15
+
16
+ return new Promise<void>((resolve) => {
17
+ renderer.once(CliRenderEvents.DESTROY, () => resolve())
18
+ })
19
+ }
20
+
21
+ async function main() {
22
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
23
+ throw new Error("OpenTUI requires a TTY. Use PATZE_USE_JS=1 for piped/non-TTY shells.")
24
+ }
25
+
26
+ const cwd = parseCwdArg()
27
+ const renderer = await createCliRenderer({
28
+ exitOnCtrlC: false,
29
+ maxFps: 30,
30
+ screenMode: "alternate-screen",
31
+ })
32
+ const root = createRoot(renderer)
33
+
34
+ try {
35
+ root.render(createElement(App, { cwd }))
36
+ await waitUntilDestroyed(renderer)
37
+ } finally {
38
+ root.unmount()
39
+ if (!renderer.isDestroyed) {
40
+ renderer.destroy()
41
+ }
42
+ }
43
+ }
44
+
45
+ await main()
@@ -0,0 +1,11 @@
1
+ import { resolve } from "node:path"
2
+ import { pathToFileURL } from "node:url"
3
+
4
+ export function resolvePatzePackageRoot(): string {
5
+ return process.env.PATZE_CLI_PACKAGE_ROOT || resolve(import.meta.dir, "../..")
6
+ }
7
+
8
+ export async function importPatzeDist<T>(relativePath: string): Promise<T> {
9
+ const modulePath = resolve(resolvePatzePackageRoot(), "dist", relativePath)
10
+ return import(pathToFileURL(modulePath).href) as Promise<T>
11
+ }
@@ -0,0 +1,218 @@
1
+ export type TranscriptEntryKind =
2
+ | "assistant"
3
+ | "error"
4
+ | "meta"
5
+ | "status"
6
+ | "tool"
7
+ | "user"
8
+
9
+ export type TranscriptEntry = {
10
+ id: string
11
+ kind: TranscriptEntryKind
12
+ label: string
13
+ text: string
14
+ upsertKey?: string
15
+ }
16
+
17
+ export type TranscriptPart = {
18
+ text: string
19
+ bold?: boolean
20
+ color?: "blue" | "cyan" | "gray" | "green" | "magenta" | "red" | "white" | "yellow"
21
+ dimColor?: boolean
22
+ }
23
+
24
+ export type TranscriptLine = {
25
+ id: string
26
+ kind: TranscriptEntryKind
27
+ label: string
28
+ parts: TranscriptPart[]
29
+ }
30
+
31
+ export function buildTranscriptLines(entries: TranscriptEntry[], columns: number): TranscriptLine[] {
32
+ const labelWidth = 7
33
+ const textWidth = Math.max(20, columns - labelWidth - 4)
34
+
35
+ return entries.flatMap((entry) => {
36
+ const renderedLines = trimTrailingBlankLines(
37
+ entry.kind === "assistant"
38
+ ? renderMarkdownLines(entry.text || "...", textWidth)
39
+ : wrapText(entry.text || "...", textWidth).map((text) => ({
40
+ parts: [{ text }],
41
+ }))
42
+ )
43
+
44
+ return renderedLines.map((line, index) => ({
45
+ id: `${entry.id}-${index}`,
46
+ kind: entry.kind,
47
+ label: index === 0 ? entry.label : "",
48
+ parts: line.parts,
49
+ }))
50
+ })
51
+ }
52
+
53
+ function trimTrailingBlankLines(lines: Array<{ parts: TranscriptPart[] }>) {
54
+ let end = lines.length
55
+
56
+ while (end > 1 && isBlankRenderedLine(lines[end - 1])) {
57
+ end -= 1
58
+ }
59
+
60
+ return lines.slice(0, end)
61
+ }
62
+
63
+ function isBlankRenderedLine(line: { parts: TranscriptPart[] }) {
64
+ return line.parts.every((part) => part.text.trim() === "")
65
+ }
66
+
67
+ function renderMarkdownLines(value: string, width: number) {
68
+ const lines: Array<{ parts: TranscriptPart[] }> = []
69
+ let inCodeBlock = false
70
+
71
+ for (const rawLine of value.split("\n")) {
72
+ const line = rawLine.trimEnd()
73
+ const fence = line.match(/^```\s*(.*)$/)
74
+
75
+ if (fence) {
76
+ inCodeBlock = !inCodeBlock
77
+ const language = fence[1]?.trim()
78
+ lines.push({
79
+ parts: [
80
+ {
81
+ text: inCodeBlock
82
+ ? `--- code${language ? `: ${language}` : ""}`
83
+ : "--- end code",
84
+ color: "gray",
85
+ dimColor: true,
86
+ },
87
+ ],
88
+ })
89
+ continue
90
+ }
91
+
92
+ if (inCodeBlock) {
93
+ for (const text of wrapText(line || " ", width - 2)) {
94
+ lines.push({
95
+ parts: [{ text: `| ${text}`, color: "gray" }],
96
+ })
97
+ }
98
+ continue
99
+ }
100
+
101
+ const heading = line.match(/^(#{1,6})\s+(.+)$/)
102
+ if (heading) {
103
+ for (const text of wrapText(heading[2] ?? "", width - 2)) {
104
+ lines.push({
105
+ parts: [{ text: `# ${text}`, bold: true, color: "cyan" }],
106
+ })
107
+ }
108
+ continue
109
+ }
110
+
111
+ const blockquote = line.match(/^>\s?(.*)$/)
112
+ if (blockquote) {
113
+ for (const text of wrapText(blockquote[1] || " ", width - 2)) {
114
+ lines.push({
115
+ parts: [
116
+ { text: "> ", color: "gray", dimColor: true },
117
+ ...parseInlineMarkdown(text),
118
+ ],
119
+ })
120
+ }
121
+ continue
122
+ }
123
+
124
+ const unordered = line.match(/^\s*[-*+]\s+(.+)$/)
125
+ if (unordered) {
126
+ for (const [index, text] of wrapText(unordered[1] ?? "", width - 2).entries()) {
127
+ lines.push({
128
+ parts: [
129
+ { text: index === 0 ? "- " : " ", color: "cyan" },
130
+ ...parseInlineMarkdown(text),
131
+ ],
132
+ })
133
+ }
134
+ continue
135
+ }
136
+
137
+ const ordered = line.match(/^\s*(\d+)[.)]\s+(.+)$/)
138
+ if (ordered) {
139
+ const marker = `${ordered[1]}. `
140
+ for (const [index, text] of wrapText(ordered[2] ?? "", width - marker.length).entries()) {
141
+ lines.push({
142
+ parts: [
143
+ { text: index === 0 ? marker : " ".repeat(marker.length), color: "cyan" },
144
+ ...parseInlineMarkdown(text),
145
+ ],
146
+ })
147
+ }
148
+ continue
149
+ }
150
+
151
+ for (const text of wrapText(line, width)) {
152
+ lines.push({ parts: parseInlineMarkdown(text) })
153
+ }
154
+ }
155
+
156
+ return lines
157
+ }
158
+
159
+ function wrapText(value: string, width: number) {
160
+ const lines: string[] = []
161
+
162
+ for (const rawLine of value.split("\n")) {
163
+ let line = rawLine
164
+
165
+ if (!line) {
166
+ lines.push("")
167
+ continue
168
+ }
169
+
170
+ while (line.length > width) {
171
+ let breakAt = line.lastIndexOf(" ", width)
172
+ if (breakAt < width * 0.5) {
173
+ breakAt = width
174
+ }
175
+
176
+ lines.push(line.slice(0, breakAt).trimEnd())
177
+ line = line.slice(breakAt).trimStart()
178
+ }
179
+
180
+ lines.push(line)
181
+ }
182
+
183
+ return lines
184
+ }
185
+
186
+ function parseInlineMarkdown(value: string): TranscriptPart[] {
187
+ const parts: TranscriptPart[] = []
188
+ const pattern = /(\*\*[^*]+\*\*|`[^`]+`|\*[^*]+\*)/g
189
+ let lastIndex = 0
190
+ let match: RegExpExecArray | null
191
+
192
+ while ((match = pattern.exec(value)) !== null) {
193
+ if (match.index > lastIndex) {
194
+ parts.push({ text: value.slice(lastIndex, match.index) })
195
+ }
196
+
197
+ const token = match[0]
198
+ if (token.startsWith("**")) {
199
+ parts.push({ text: token.slice(2, -2), bold: true })
200
+ } else if (token.startsWith("`")) {
201
+ parts.push({ text: token.slice(1, -1), color: "magenta" })
202
+ } else {
203
+ parts.push({ text: token.slice(1, -1), bold: true, color: "cyan" })
204
+ }
205
+
206
+ lastIndex = match.index + token.length
207
+ }
208
+
209
+ if (lastIndex < value.length) {
210
+ parts.push({ text: value.slice(lastIndex) })
211
+ }
212
+
213
+ if (!parts.length) {
214
+ parts.push({ text: value })
215
+ }
216
+
217
+ return parts
218
+ }
@@ -0,0 +1,78 @@
1
+ import type { TranscriptEntry, TranscriptEntryKind } from "./transcript-render.js"
2
+ import { deriveTranscriptUpsertKey } from "../../dist/cli/interactive/transcript-upsert.js"
3
+
4
+ type TextWriter = {
5
+ line: (text?: string) => void
6
+ }
7
+ import { parseTranscriptLine } from "./line-parse.js"
8
+
9
+ export type TranscriptSinkCallbacks = {
10
+ addEntry: (entry: Omit<TranscriptEntry, "id"> & { upsertKey?: string }) => void
11
+ upsertEntry: (upsertKey: string, entry: Omit<TranscriptEntry, "id">) => void
12
+ appendToLast: (text: string) => void
13
+ upsertStreaming: (text: string) => void
14
+ clearStreaming: () => void
15
+ }
16
+
17
+ export function createTranscriptSink(callbacks: TranscriptSinkCallbacks): TextWriter {
18
+ return {
19
+ line(text = "") {
20
+ callbacks.clearStreaming()
21
+ const parsed = parseTranscriptLine(text)
22
+ if (!parsed) {
23
+ return
24
+ }
25
+
26
+ if (parsed.continuation) {
27
+ callbacks.appendToLast(parsed.text)
28
+ return
29
+ }
30
+
31
+ const entry = {
32
+ kind: parsed.kind,
33
+ label: parsed.label,
34
+ text: parsed.text,
35
+ }
36
+ const upsertKey = deriveTranscriptUpsertKey(parsed.kind, parsed.label, parsed.text)
37
+ if (upsertKey) {
38
+ callbacks.upsertEntry(upsertKey, entry)
39
+ return
40
+ }
41
+
42
+ callbacks.addEntry(entry)
43
+ },
44
+ }
45
+ }
46
+
47
+ export function classifyPlainOutput(lines: string[]): Array<Omit<TranscriptEntry, "id">> {
48
+ const entries: Array<Omit<TranscriptEntry, "id">> = []
49
+
50
+ for (const raw of lines) {
51
+ const parsed = parseTranscriptLine(raw)
52
+ if (!parsed) {
53
+ continue
54
+ }
55
+
56
+ if (parsed.continuation && entries.length > 0) {
57
+ const last = entries[entries.length - 1]
58
+ last.text = `${last.text}\n${parsed.text}`
59
+ continue
60
+ }
61
+
62
+ entries.push({
63
+ kind: parsed.kind,
64
+ label: parsed.label,
65
+ text: parsed.text,
66
+ })
67
+ }
68
+
69
+ return entries
70
+ }
71
+
72
+ export function inferErrorKind(text: string): TranscriptEntryKind {
73
+ const lower = text.toLowerCase()
74
+ if (lower.includes("error") || lower.includes("failed") || lower.includes("unknown command")) {
75
+ return "error"
76
+ }
77
+ return "status"
78
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "jsx": "react-jsx",
7
+ "jsxImportSource": "@opentui/react",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true
11
+ },
12
+ "include": ["src/**/*"]
13
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patze/code-cli",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "description": "Patze Code — local terminal coding agent client for PatzeAgents",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -17,7 +17,11 @@
17
17
  "CHANGELOG.md",
18
18
  "NOTICE.md",
19
19
  "README.md",
20
- "VERSION"
20
+ "VERSION",
21
+ "opentui/package.json",
22
+ "opentui/tsconfig.json",
23
+ "opentui/scripts",
24
+ "opentui/src"
21
25
  ],
22
26
  "scripts": {
23
27
  "build": "tsc -p tsconfig.json",