detached f0f836b

Infer print mode from piped stdin

commit
f0f836b5bf1ac6905fc656497798eea8a2aec6bf
author
patriceckhart · · 2026-08-07 05:23 UTC
parent
086a9fe
signature
unsigned
refs
tag: v0.3.37
4 files · +62 -2
M README.md +2 -1
@@ -201,6 +201,7 @@ The first time you launch a newer zot binary, the TUI shows the GitHub release n
201201 zot # interactive tui
202202 zot "fix the failing test" # tui, pre-filled prompt
203203 zot -p "list all go files" # print final text, exit
204+echo "list all go files" | zot # piped stdin implies print mode
204205 cat README.md | zot -p "summarize this text" # combine stdin with the prompt
205206 zot -p --stats stats.json "task" # print final text and write generation stats
206207 zot --json "refactor main.go" # newline-delimited json events, exit
@@ -253,7 +254,7 @@ When the sandbox is on (see `/jail`), all four tools refuse paths outside the se
253254 - **Interactive** (default): chat TUI with streaming output, spinner, cost meter, slash commands.
254255 - **Print**: `zot -p "prompt"` runs the agent to completion and writes only the final assistant text to stdout.
255256 - **Stream**: `zot --stream "prompt"` runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr.
256-- **Piped input**: print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example, `cat README.md | zot -p "summarize this text"`.
257+- **Piped input**: when no mode is specified, piped stdin selects print mode. Print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example, `echo "list all go files" | zot` or `cat README.md | zot -p "summarize this text"`.
257258 - **JSON**: `zot --json "prompt"` emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in [docs/rpc.md](docs/rpc.md).
258259 - **RPC**: `zot rpc` runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See [docs/rpc.md](docs/rpc.md) for the wire schema and `examples/rpc/{python,node,shell,go}` for working clients.
259260
@@ -201,6 +201,7 @@ The first time you launch a newer zot binary, the TUI shows the GitHub release n
201 zot # interactive tui201 zot # interactive tui
202 zot "fix the failing test" # tui, pre-filled prompt202 zot "fix the failing test" # tui, pre-filled prompt
203 zot -p "list all go files" # print final text, exit203 zot -p "list all go files" # print final text, exit
204+echo "list all go files" | zot # piped stdin implies print mode
204 cat README.md | zot -p "summarize this text" # combine stdin with the prompt205 cat README.md | zot -p "summarize this text" # combine stdin with the prompt
205 zot -p --stats stats.json "task" # print final text and write generation stats206 zot -p --stats stats.json "task" # print final text and write generation stats
206 zot --json "refactor main.go" # newline-delimited json events, exit207 zot --json "refactor main.go" # newline-delimited json events, exit
@@ -253,7 +254,7 @@ When the sandbox is on (see `/jail`), all four tools refuse paths outside the se
253 - **Interactive** (default): chat TUI with streaming output, spinner, cost meter, slash commands.254 - **Interactive** (default): chat TUI with streaming output, spinner, cost meter, slash commands.
254 - **Print**: `zot -p "prompt"` runs the agent to completion and writes only the final assistant text to stdout.255 - **Print**: `zot -p "prompt"` runs the agent to completion and writes only the final assistant text to stdout.
255 - **Stream**: `zot --stream "prompt"` runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr.256 - **Stream**: `zot --stream "prompt"` runs without the TUI and writes assistant text to stdout as it arrives. Tool activity goes to stderr.
256-- **Piped input**: print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example, `cat README.md | zot -p "summarize this text"`.257+- **Piped input**: when no mode is specified, piped stdin selects print mode. Print, stream, and JSON modes prepend piped stdin to the positional prompt, separated by a newline. For example, `echo "list all go files" | zot` or `cat README.md | zot -p "summarize this text"`.
257 - **JSON**: `zot --json "prompt"` emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in [docs/rpc.md](docs/rpc.md).258 - **JSON**: `zot --json "prompt"` emits one JSON object per agent event to stdout, newline-delimited. The schema is documented in [docs/rpc.md](docs/rpc.md).
258 - **RPC**: `zot rpc` runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See [docs/rpc.md](docs/rpc.md) for the wire schema and `examples/rpc/{python,node,shell,go}` for working clients.259 - **RPC**: `zot rpc` runs as a long-lived child process; commands in on stdin, events and responses out on stdout, both as NDJSON. Designed for embedding zot in third-party apps written in any language. See [docs/rpc.md](docs/rpc.md) for the wire schema and `examples/rpc/{python,node,shell,go}` for working clients.
259 260
M packages/agent/args.go +1 -0
@@ -394,6 +394,7 @@ func PrintHelp(version string) {
394394 row{"zot", "interactive tui"},
395395 row{"zot \"prompt\"", "interactive, pre-filled prompt"},
396396 row{"zot -p \"prompt\"", "print final text, exit"},
397+ row{"echo \"prompt\" | zot", "piped stdin implies print mode"},
397398 row{"zot --stream \"prompt\"", "stream assistant text live, exit"},
398399 row{"zot --json \"prompt\"", "newline-delimited json events, exit"},
399400 row{"zot rpc", "json-rpc loop on stdin/stdout (see docs/rpc.md)"},
@@ -394,6 +394,7 @@ func PrintHelp(version string) {
394 row{"zot", "interactive tui"},394 row{"zot", "interactive tui"},
395 row{"zot \"prompt\"", "interactive, pre-filled prompt"},395 row{"zot \"prompt\"", "interactive, pre-filled prompt"},
396 row{"zot -p \"prompt\"", "print final text, exit"},396 row{"zot -p \"prompt\"", "print final text, exit"},
397+ row{"echo \"prompt\" | zot", "piped stdin implies print mode"},
397 row{"zot --stream \"prompt\"", "stream assistant text live, exit"},398 row{"zot --stream \"prompt\"", "stream assistant text live, exit"},
398 row{"zot --json \"prompt\"", "newline-delimited json events, exit"},399 row{"zot --json \"prompt\"", "newline-delimited json events, exit"},
399 row{"zot rpc", "json-rpc loop on stdin/stdout (see docs/rpc.md)"},400 row{"zot rpc", "json-rpc loop on stdin/stdout (see docs/rpc.md)"},
M packages/agent/cli.go +10 -0
@@ -222,6 +222,9 @@ func runWithArgsRaw(rawArgs []string, version string) error {
222222 rawArgs = append([]string{"--rpc"}, rawArgs[1:]...)
223223 }
224224
225+ if fi, err := os.Stdin.Stat(); err == nil {
226+ rawArgs = argsForStdin(rawArgs, fi.Mode())
227+ }
225228 args, err := ParseArgs(rawArgs)
226229 if err != nil {
227230 PrintHelp(version)
@@ -1571,6 +1574,13 @@ func writeNewTranscriptLocked(ag *core.Agent, sess *core.Session, from int) {
15711574 _ = sess.AppendUsage(cum, cum)
15721575 }
15731576
1577+func argsForStdin(rawArgs []string, mode os.FileMode) []string {
1578+ if mode&os.ModeCharDevice != 0 {
1579+ return rawArgs
1580+ }
1581+ return append([]string{"--print"}, rawArgs...)
1582+}
1583+
15741584 func combinePromptInput(stdin, positional string) string {
15751585 parts := make([]string, 0, 2)
15761586 if stdin = strings.TrimSpace(stdin); stdin != "" {
@@ -222,6 +222,9 @@ func runWithArgsRaw(rawArgs []string, version string) error {
222 rawArgs = append([]string{"--rpc"}, rawArgs[1:]...)222 rawArgs = append([]string{"--rpc"}, rawArgs[1:]...)
223 }223 }
224 224
225+ if fi, err := os.Stdin.Stat(); err == nil {
226+ rawArgs = argsForStdin(rawArgs, fi.Mode())
227+ }
225 args, err := ParseArgs(rawArgs)228 args, err := ParseArgs(rawArgs)
226 if err != nil {229 if err != nil {
227 PrintHelp(version)230 PrintHelp(version)
@@ -1571,6 +1574,13 @@ func writeNewTranscriptLocked(ag *core.Agent, sess *core.Session, from int) {
1571 _ = sess.AppendUsage(cum, cum)1574 _ = sess.AppendUsage(cum, cum)
1572 }1575 }
1573 1576
1577+func argsForStdin(rawArgs []string, mode os.FileMode) []string {
1578+ if mode&os.ModeCharDevice != 0 {
1579+ return rawArgs
1580+ }
1581+ return append([]string{"--print"}, rawArgs...)
1582+}
1583+
1574 func combinePromptInput(stdin, positional string) string {1584 func combinePromptInput(stdin, positional string) string {
1575 parts := make([]string, 0, 2)1585 parts := make([]string, 0, 2)
1576 if stdin = strings.TrimSpace(stdin); stdin != "" {1586 if stdin = strings.TrimSpace(stdin); stdin != "" {
M packages/agent/prompt_input_test.go +49 -1
@@ -1,6 +1,54 @@
11 package agent
22
3-import "testing"
3+import (
4+ "os"
5+ "testing"
6+)
7+
8+func TestArgsForStdinSelectsPrintModeForPipe(t *testing.T) {
9+ args, err := ParseArgs(argsForStdin([]string{"summarize"}, os.ModeNamedPipe))
10+ if err != nil {
11+ t.Fatal(err)
12+ }
13+ if args.Mode != ModePrint || args.Prompt != "summarize" {
14+ t.Fatalf("Mode=%q Prompt=%q, want print mode with prompt", args.Mode, args.Prompt)
15+ }
16+}
17+
18+func TestArgsForStdinPreservesTerminalAndExplicitModes(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ in []string
22+ mode os.FileMode
23+ want Mode
24+ }{
25+ {name: "terminal stays interactive", mode: os.ModeCharDevice, want: ModeInteractive},
26+ {name: "explicit stream overrides pipe default", in: []string{"--stream", "prompt"}, mode: os.ModeNamedPipe, want: ModeStream},
27+ {name: "explicit json overrides redirected file default", in: []string{"--json", "prompt"}, want: ModeJSON},
28+ {name: "explicit rpc overrides pipe default", in: []string{"--rpc"}, mode: os.ModeNamedPipe, want: ModeRPC},
29+ }
30+ for _, tc := range tests {
31+ t.Run(tc.name, func(t *testing.T) {
32+ args, err := ParseArgs(argsForStdin(tc.in, tc.mode))
33+ if err != nil {
34+ t.Fatal(err)
35+ }
36+ if args.Mode != tc.want {
37+ t.Fatalf("Mode=%q, want %q", args.Mode, tc.want)
38+ }
39+ })
40+ }
41+}
42+
43+func TestArgsForStdinAllowsPrintStats(t *testing.T) {
44+ args, err := ParseArgs(argsForStdin([]string{"--stats", "stats.json"}, os.ModeNamedPipe))
45+ if err != nil {
46+ t.Fatal(err)
47+ }
48+ if args.Mode != ModePrint || args.StatsPath != "stats.json" {
49+ t.Fatalf("Mode=%q StatsPath=%q, want print mode with stats path", args.Mode, args.StatsPath)
50+ }
51+}
452
553 func TestCombinePromptInput(t *testing.T) {
654 tests := []struct {
@@ -1,6 +1,54 @@
1 package agent1 package agent
2 2
3-import "testing"3+import (
4+ "os"
5+ "testing"
6+)
7+
8+func TestArgsForStdinSelectsPrintModeForPipe(t *testing.T) {
9+ args, err := ParseArgs(argsForStdin([]string{"summarize"}, os.ModeNamedPipe))
10+ if err != nil {
11+ t.Fatal(err)
12+ }
13+ if args.Mode != ModePrint || args.Prompt != "summarize" {
14+ t.Fatalf("Mode=%q Prompt=%q, want print mode with prompt", args.Mode, args.Prompt)
15+ }
16+}
17+
18+func TestArgsForStdinPreservesTerminalAndExplicitModes(t *testing.T) {
19+ tests := []struct {
20+ name string
21+ in []string
22+ mode os.FileMode
23+ want Mode
24+ }{
25+ {name: "terminal stays interactive", mode: os.ModeCharDevice, want: ModeInteractive},
26+ {name: "explicit stream overrides pipe default", in: []string{"--stream", "prompt"}, mode: os.ModeNamedPipe, want: ModeStream},
27+ {name: "explicit json overrides redirected file default", in: []string{"--json", "prompt"}, want: ModeJSON},
28+ {name: "explicit rpc overrides pipe default", in: []string{"--rpc"}, mode: os.ModeNamedPipe, want: ModeRPC},
29+ }
30+ for _, tc := range tests {
31+ t.Run(tc.name, func(t *testing.T) {
32+ args, err := ParseArgs(argsForStdin(tc.in, tc.mode))
33+ if err != nil {
34+ t.Fatal(err)
35+ }
36+ if args.Mode != tc.want {
37+ t.Fatalf("Mode=%q, want %q", args.Mode, tc.want)
38+ }
39+ })
40+ }
41+}
42+
43+func TestArgsForStdinAllowsPrintStats(t *testing.T) {
44+ args, err := ParseArgs(argsForStdin([]string{"--stats", "stats.json"}, os.ModeNamedPipe))
45+ if err != nil {
46+ t.Fatal(err)
47+ }
48+ if args.Mode != ModePrint || args.StatsPath != "stats.json" {
49+ t.Fatalf("Mode=%q StatsPath=%q, want print mode with stats path", args.Mode, args.StatsPath)
50+ }
51+}
4 52
5 func TestCombinePromptInput(t *testing.T) {53 func TestCombinePromptInput(t *testing.T) {
6 tests := []struct {54 tests := []struct {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close