Skip to content

Commit 6d32b4c

Browse files
fix(tools): include trailing output in error briefs and render brief as plain text (#2389)
Signed-off-by: Kai <me@kaiyi.cool> Co-authored-by: Kai <me@kaiyi.cool>
1 parent fa527f6 commit 6d32b4c

8 files changed

Lines changed: 72 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Only write entries that are worth mentioning to users.
1111

1212
## Unreleased
1313

14+
- Shell: Show trailing output in tool error briefs when commands fail
1415
## 1.46.0 (2026-05-28)
1516

1617
- Shell: Support styled text in welcome tips

docs/en/release-notes/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ This page documents the changes in each Kimi Code CLI release.
44

55
## Unreleased
66

7+
- Shell: Show trailing output in tool error briefs when commands fail
78
## 1.46.0 (2026-05-28)
89

910
- Shell: Support styled text in welcome tips

docs/zh/release-notes/changelog.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
## 未发布
66

7+
- Shell:工具执行失败时显示完整错误信息
78
## 1.46.0 (2026-05-28)
89

910
- Shell:欢迎提示支持样式化文本

src/kimi_cli/acp/tools.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,20 +142,22 @@ async def __call__(self, params: ShellParams) -> ToolReturnValue:
142142
else ""
143143
)
144144

145+
tail = builder.tail()
146+
tail_md = f"\n{tail}" if tail else ""
145147
if timed_out:
146148
return builder.error(
147149
f"Command killed by timeout ({timeout_label}){truncated_note}",
148-
brief=f"Killed by timeout ({timeout_label})",
150+
brief=f"Killed by timeout ({timeout_label}){tail_md}",
149151
)
150152
if exit_signal:
151153
return builder.error(
152154
f"Command terminated by signal: {exit_signal}.{truncated_note}",
153-
brief=f"Signal: {exit_signal}",
155+
brief=f"Signal: {exit_signal}{tail_md}",
154156
)
155157
if exit_code not in (None, 0):
156158
return builder.error(
157159
f"Command failed with exit code: {exit_code}.{truncated_note}",
158-
brief=f"Failed with exit code: {exit_code}",
160+
brief=f"Failed with exit code: {exit_code}{tail_md}",
159161
)
160162
return builder.ok(f"Command executed successfully.{truncated_note}")
161163
finally:

src/kimi_cli/tools/shell/__init__.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,9 +117,13 @@ def stderr_cb(line: bytes):
117117
if exitcode == 0:
118118
return builder.ok("Command executed successfully.")
119119
else:
120+
brief = f"Failed with exit code: {exitcode}"
121+
tail = builder.tail()
122+
if tail:
123+
brief += f"\n{tail}"
120124
return builder.error(
121125
f"Command failed with exit code: {exitcode}.",
122-
brief=f"Failed with exit code: {exitcode}",
126+
brief=brief,
123127
)
124128
except TimeoutError:
125129
return builder.error(

src/kimi_cli/tools/utils.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,26 @@ def write(self, text: str) -> int:
127127

128128
return chars_written
129129

130+
def tail(self, max_lines: int = 5, max_line_len: int = 200) -> str:
131+
"""Return the last non-empty lines from the buffer, joined with newlines.
132+
133+
Useful for surfacing actionable error context (stderr) in tool result briefs.
134+
"""
135+
collected: list[str] = []
136+
for chunk in reversed(self._buffer):
137+
for line in reversed(chunk.splitlines()):
138+
stripped = line.rstrip()
139+
if not stripped.strip():
140+
continue
141+
if len(stripped) > max_line_len:
142+
stripped = stripped[:max_line_len] + "..."
143+
collected.append(stripped)
144+
if len(collected) >= max_lines:
145+
break
146+
if len(collected) >= max_lines:
147+
break
148+
return "\n".join(reversed(collected))
149+
130150
def display(self, *blocks: DisplayBlock) -> None:
131151
"""Add display blocks to the tool result."""
132152
self._display.extend(blocks)

src/kimi_cli/ui/shell/visualize/_blocks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ def _compose(self) -> RenderableType:
518518
elif isinstance(block, BriefDisplayBlock):
519519
style = "grey50" if not self._result.is_error else "dark_red"
520520
if block.text:
521-
lines.append(Markdown(block.text, style=style))
521+
lines.append(Text(block.text.rstrip("\n"), style=style))
522522
idx += 1
523523
elif isinstance(block, TodoDisplayBlock):
524524
markdown = self._render_todo_markdown(block)

tests/utils/test_result_builder.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,41 @@ def test_empty_write():
155155
assert written == 0
156156
assert builder.n_chars == 0
157157
assert not builder.is_full
158+
159+
160+
def test_tail_empty():
161+
builder = ToolResultBuilder()
162+
assert builder.tail() == ""
163+
164+
165+
def test_tail_basic():
166+
builder = ToolResultBuilder()
167+
builder.write("first line\nsecond line\nthird line\n")
168+
assert builder.tail() == "first line\nsecond line\nthird line"
169+
170+
171+
def test_tail_skips_blank_lines():
172+
builder = ToolResultBuilder()
173+
builder.write("real error\n\n \n")
174+
assert builder.tail() == "real error"
175+
176+
177+
def test_tail_respects_max_lines():
178+
builder = ToolResultBuilder()
179+
builder.write("\n".join(f"line {i}" for i in range(10)) + "\n")
180+
assert builder.tail(max_lines=3) == "line 7\nline 8\nline 9"
181+
182+
183+
def test_tail_truncates_long_line():
184+
builder = ToolResultBuilder()
185+
builder.write("x" * 500 + "\n")
186+
tail = builder.tail(max_line_len=100)
187+
assert tail.endswith("...")
188+
assert len(tail) == 103
189+
190+
191+
def test_tail_handles_multiple_writes():
192+
builder = ToolResultBuilder()
193+
builder.write("stdout chunk\n")
194+
builder.write("stderr: permission denied\n")
195+
assert builder.tail(max_lines=2) == "stdout chunk\nstderr: permission denied"

0 commit comments

Comments
 (0)