Skip to content

Commit 3d5835b

Browse files
committed
merge: markdown rendering, date awareness, and agent output tracking into unified branch
# Conflicts: # docs/superpowers/plans/2026-05-26-agent-live-tool-stream.md # src/pythinker_code/ui/shell/components/markdown.py # src/pythinker_code/ui/shell/visualize/_blocks.py # src/pythinker_code/ui/shell/visualize/_live_view.py # tests/ui/test_shell_markdown.py # tests/ui_and_conv/test_tool_call_block.py
2 parents 539d365 + 999ec63 commit 3d5835b

6 files changed

Lines changed: 257 additions & 3 deletions

File tree

src/pythinker_code/agents/default/system.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ The operating environment is not in a sandbox. Any actions you do will immediate
188188

189189
## Date and Time
190190

191-
The current date and time in ISO format is `${PYTHINKER_NOW}`. This is only a reference for you when searching the web, or checking file modification time, etc. If you need the exact time, use Shell tool with proper command.
191+
The current date and time in ISO format is `${PYTHINKER_NOW}`. Treat this as the authoritative present — it reflects the real "now", which is later than your training data suggests. Anchor all reasoning about the current date, the year, recency, and what counts as the "latest" version or release to `${PYTHINKER_NOW}`; do not fall back on an earlier year you might assume from training. Use it as your reference when searching the web or checking file modification times. If you need the exact time, use the Shell tool with a proper command.
192192

193193
## Working Directory
194194

@@ -261,6 +261,15 @@ Identify the skills that are likely to be useful for the tasks you are currently
261261

262262
Only read skill details when needed to conserve the context window.
263263

264+
# Output Formatting
265+
266+
Your responses are rendered as Markdown in a terminal. Emit well-formed Markdown so it renders cleanly:
267+
268+
- **Tables:** put the header row on its own line, the `|---|---|` delimiter row on the immediately following line (no blank line between them), and one row per line. Never glue a table onto adjacent prose (e.g. `Findings| Col |`) and never cram multiple rows onto one line. Leave a blank line before and after the table.
269+
- Prefer a short bullet list over a table when there are only a few items or any cell is long; reserve tables for genuinely tabular data with short cells.
270+
- **Code fences are for code only.** Use triple-backtick blocks (tagged with the language, e.g. ```python, ```toml) solely for source, config, or commands — one snippet per block. Never wrap a prose report, finding list, checklist, or ASCII box in a fence to align or frame it; write it as normal Markdown (headings, bullets, tables) so it renders cleanly.
271+
- **Status icons sparingly.** A check/cross/dot can mark a single headline result, but do not prefix every line with one. Use plain words for severity and outcomes (e.g. `High`, `PASS`, `0 findings`). The terminal renders icons as calm monochrome glyphs only outside code fences — another reason not to box reports.
272+
264273
# Ultimate Reminders
265274

266275
At any time, you should be HELPFUL, CONCISE, and ACCURATE. Be thorough in your actions — test what you build, verify what you change — not in your explanations.

src/pythinker_code/ui/shell/components/markdown.py

Lines changed: 166 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,11 @@
7676
)
7777
_PRIORITY_MATRIX_SEVERITIES = ("CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO")
7878
_TABLE_SEPARATOR_RE = re.compile(r"^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$")
79+
# A GFM table delimiter run, e.g. ``|---|:--:|---|``. Two or more dashes per
80+
# cell keeps stray inline ``|-|`` out of the match.
81+
_DELIM_RUN_RE = re.compile(r"\|(?:\s*:?-{2,}:?\s*\|)+")
82+
# A header line: optional prose prefix, then a trailing run of pipe cells.
83+
_HEADER_RE = re.compile(r"^(?P<prefix>.*?)(?P<cells>(?:\|[^\n|]*)+\|)\s*$")
7984

8085

8186
__all__ = [
@@ -344,6 +349,165 @@ def _simplify_markdown_report_icons(markup: str) -> str:
344349
return "".join(lines)
345350

346351

352+
def _split_pipe_cells(segment: str) -> list[str]:
353+
"""Split a ``| a | b |`` run into stripped inner cells (drops the frame)."""
354+
parts = re.split(r"(?<!\\)\|", segment)
355+
if parts and parts[0].strip() == "":
356+
parts = parts[1:]
357+
if parts and parts[-1].strip() == "":
358+
parts = parts[:-1]
359+
return [part.strip() for part in parts]
360+
361+
362+
def _is_pipe_row(line: str) -> bool:
363+
stripped = line.strip()
364+
return stripped.startswith("|") and stripped.count("|") >= 2
365+
366+
367+
def _delimiter_markers(run: str) -> list[str]:
368+
"""Return per-column alignment markers (``---``, ``:---``, ``---:``, ``:---:``)."""
369+
markers: list[str] = []
370+
for cell in _split_pipe_cells(run):
371+
left = cell.startswith(":")
372+
right = cell.endswith(":")
373+
if left and right:
374+
markers.append(":---:")
375+
elif right:
376+
markers.append("---:")
377+
elif left:
378+
markers.append(":---")
379+
else:
380+
markers.append("---")
381+
return markers
382+
383+
384+
def _normalize_table_block(text: str) -> str:
385+
"""Repair malformed GFM tables in a fence-free block of markdown.
386+
387+
Models occasionally glue a table header onto preceding prose, drop the
388+
newline between the header and the ``|---|`` delimiter, or cram data rows
389+
onto the delimiter line — markdown-it then renders the whole thing as raw
390+
text. Anchored on the delimiter run, this rebuilds each region it is
391+
*confident* is a table (delimiter at line start, header and data cell counts
392+
both equal to the delimiter's column count) and passes everything else
393+
through untouched. Well-formed tables are rebuilt to identical-rendering
394+
markdown, so the pass is safe to apply unconditionally.
395+
"""
396+
out = ""
397+
while True:
398+
match = _DELIM_RUN_RE.search(text)
399+
if match is None:
400+
return out + text
401+
markers = _delimiter_markers(match.group(0))
402+
n_cols = len(markers)
403+
head = text[: match.start()]
404+
tail = text[match.end() :]
405+
406+
# The delimiter must start its own line — guards against inline ``|-|``.
407+
line_prefix = head[head.rfind("\n") + 1 :]
408+
if n_cols < 2 or line_prefix.strip() != "":
409+
out += text[: match.end()]
410+
text = tail
411+
continue
412+
413+
head_lines = head.split("\n")
414+
while head_lines and head_lines[-1] == "":
415+
head_lines.pop()
416+
header_match = _HEADER_RE.match(head_lines[-1]) if head_lines else None
417+
header_cells = _split_pipe_cells(header_match.group("cells")) if header_match else []
418+
if header_match is None or len(header_cells) != n_cols:
419+
out += text[: match.end()]
420+
text = tail
421+
continue
422+
423+
# Data rows: the same-line remainder after the delimiter plus any
424+
# following pipe rows, re-chunked into rows of ``n_cols`` cells.
425+
tail_lines = tail.split("\n")
426+
data_segments = [tail_lines[0]] if tail_lines[0].strip() else []
427+
consumed = 1
428+
for line in tail_lines[1:]:
429+
if _is_pipe_row(line):
430+
data_segments.append(line)
431+
consumed += 1
432+
else:
433+
break
434+
data_rows: list[list[str]] = []
435+
bail = False
436+
for segment in data_segments:
437+
cells = _split_pipe_cells(segment)
438+
if not cells:
439+
continue
440+
if len(cells) % n_cols != 0:
441+
bail = True # ambiguous (e.g. glued rows with empty cells) — leave as-is
442+
break
443+
for i in range(0, len(cells), n_cols):
444+
data_rows.append(cells[i : i + n_cols])
445+
if bail:
446+
out += text[: match.end()]
447+
text = tail
448+
continue
449+
450+
preamble = head_lines[:-1]
451+
prose = header_match.group("prefix").rstrip()
452+
if preamble:
453+
out += "\n".join(preamble) + "\n"
454+
if prose:
455+
out += prose + "\n"
456+
# A GFM table must be preceded by a blank line (it cannot interrupt a
457+
# paragraph), so ensure one before emitting the header.
458+
if out and not out.endswith("\n\n"):
459+
out += "\n" if out.endswith("\n") else "\n\n"
460+
out += "| " + " | ".join(header_cells) + " |\n"
461+
out += "| " + " | ".join(markers) + " |\n"
462+
for row in data_rows:
463+
out += "| " + " | ".join(row) + " |\n"
464+
465+
remainder = "\n".join(tail_lines[consumed:])
466+
if not remainder.strip():
467+
return out
468+
text = remainder if remainder.startswith("\n") else "\n" + remainder
469+
470+
471+
def _normalize_markdown_tables(markup: str) -> str:
472+
"""Apply :func:`_normalize_table_block` to every fence-free span of markup."""
473+
if "|" not in markup or "-" not in markup:
474+
return markup
475+
476+
out: list[str] = []
477+
buffer: list[str] = []
478+
in_fence = False
479+
fence_char = ""
480+
fence_len = 0
481+
482+
def flush() -> None:
483+
if buffer:
484+
out.append(_normalize_table_block("\n".join(buffer)))
485+
buffer.clear()
486+
487+
for line in markup.splitlines():
488+
match = _FENCE_RE.match(line)
489+
if in_fence:
490+
fence = match.group("fence") if match else ""
491+
if fence and fence[0] == fence_char and len(fence) >= fence_len:
492+
in_fence = False
493+
out.append(line)
494+
continue
495+
if match:
496+
flush()
497+
in_fence = True
498+
fence_char = match.group("fence")[0]
499+
fence_len = len(match.group("fence"))
500+
out.append(line)
501+
continue
502+
buffer.append(line)
503+
flush()
504+
505+
result = "\n".join(out)
506+
if markup.endswith("\n") and not result.endswith("\n"):
507+
result += "\n"
508+
return result
509+
510+
347511
class PythinkerMarkdown(Markdown):
348512
"""Drop-in replacement for ``rich.markdown.Markdown`` with the Pythinker palette.
349513
@@ -358,7 +522,8 @@ class PythinkerMarkdown(Markdown):
358522
def __init__(self, markup: str, *args: Any, **kwargs: Any) -> None:
359523
safe_markup = sanitize_ansi(markup)
360524
repaired_markup = _repair_crammed_markdown_tables(safe_markup)
361-
super().__init__(_simplify_markdown_report_icons(repaired_markup), *args, **kwargs)
525+
normalized_markup = _normalize_markdown_tables(repaired_markup)
526+
super().__init__(_simplify_markdown_report_icons(normalized_markup), *args, **kwargs)
362527

363528
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
364529
overrides = _markdown_style_overrides()

tests/core/test_default_agent.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@ async def test_default_agent(runtime: Runtime):
204204
205205
## Date and Time
206206
207-
The current date and time in ISO format is `1970-01-01T00:00:00+00:00`. This is only a reference for you when searching the web, or checking file modification time, etc. If you need the exact time, use Shell tool with proper command.
207+
The current date and time in ISO format is `1970-01-01T00:00:00+00:00`. Treat this as the authoritative present — it reflects the real "now", which is later than your training data suggests. Anchor all reasoning about the current date, the year, recency, and what counts as the "latest" version or release to `1970-01-01T00:00:00+00:00`; do not fall back on an earlier year you might assume from training. Use it as your reference when searching the web or checking file modification times. If you need the exact time, use the Shell tool with a proper command.
208208
209209
## Working Directory
210210
@@ -269,6 +269,15 @@ async def test_default_agent(runtime: Runtime):
269269
270270
Only read skill details when needed to conserve the context window.
271271
272+
# Output Formatting
273+
274+
Your responses are rendered as Markdown in a terminal. Emit well-formed Markdown so it renders cleanly:
275+
276+
- **Tables:** put the header row on its own line, the `|---|---|` delimiter row on the immediately following line (no blank line between them), and one row per line. Never glue a table onto adjacent prose (e.g. `Findings| Col |`) and never cram multiple rows onto one line. Leave a blank line before and after the table.
277+
- Prefer a short bullet list over a table when there are only a few items or any cell is long; reserve tables for genuinely tabular data with short cells.
278+
- **Code fences are for code only.** Use triple-backtick blocks (tagged with the language, e.g. ```python, ```toml) solely for source, config, or commands — one snippet per block. Never wrap a prose report, finding list, checklist, or ASCII box in a fence to align or frame it; write it as normal Markdown (headings, bullets, tables) so it renders cleanly.
279+
- **Status icons sparingly.** A check/cross/dot can mark a single headline result, but do not prefix every line with one. Use plain words for severity and outcomes (e.g. `High`, `PASS`, `0 findings`). The terminal renders icons as calm monochrome glyphs only outside code fences — another reason not to box reports.
280+
272281
# Ultimate Reminders
273282
274283
At any time, you should be HELPFUL, CONCISE, and ACCURATE. Be thorough in your actions — test what you build, verify what you change — not in your explanations.

tests/core/test_load_agent.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,24 @@ def test_system_prompt_contains_platform_info(builtin_args: BuiltinSystemPromptA
5555
assert builtin_args.PYTHINKER_SHELL in prompt
5656

5757

58+
def test_system_prompt_treats_injected_date_as_authoritative(
59+
builtin_args: BuiltinSystemPromptArgs,
60+
):
61+
"""The injected date must be framed as authoritative so the model anchors
62+
its sense of 'now' to it instead of a training-era year."""
63+
from pythinker_code.agentspec import DEFAULT_AGENT_FILE
64+
65+
prompt = _load_system_prompt(
66+
DEFAULT_AGENT_FILE.parent / "system.md",
67+
{"ROLE_ADDITIONAL": ""},
68+
builtin_args,
69+
)
70+
71+
assert builtin_args.PYTHINKER_NOW in prompt
72+
assert "authoritative present" in prompt
73+
assert "do not fall back on an earlier year" in prompt
74+
75+
5876
def test_system_prompt_enforces_context_first_orchestration(
5977
builtin_args: BuiltinSystemPromptArgs,
6078
):
@@ -74,6 +92,27 @@ def test_system_prompt_enforces_context_first_orchestration(
7492
assert "Treat subagent claims as leads, not proof" in prompt
7593

7694

95+
def test_system_prompt_includes_markdown_table_formatting_guidance(
96+
builtin_args: BuiltinSystemPromptArgs,
97+
):
98+
"""Default prompt must reach the model with table-formatting rules so it
99+
stops emitting headers glued to prose (which render as raw text)."""
100+
from pythinker_code.agentspec import DEFAULT_AGENT_FILE
101+
102+
prompt = _load_system_prompt(
103+
DEFAULT_AGENT_FILE.parent / "system.md",
104+
{"ROLE_ADDITIONAL": ""},
105+
builtin_args,
106+
)
107+
108+
assert "# Output Formatting" in prompt
109+
assert "glue a table onto adjacent prose" in prompt
110+
# Reports must not be wrapped in code fences (that is what preserves raw
111+
# emoji and breaks column alignment), and status icons should be sparing.
112+
assert "Code fences are for code only" in prompt
113+
assert "Status icons sparingly" in prompt
114+
115+
77116
def test_default_subagent_prompts_keep_robust_contracts():
78117
"""Specialist subagents should retain evidence, planning, and verification gates."""
79118
from pythinker_code.agentspec import DEFAULT_AGENT_FILE, load_agent_spec

tests/ui/test_shell_markdown.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,37 @@ def test_shell_markdown_renders_priority_matrix_as_grouped_rows() -> None:
6363
assert "────────────────" not in output
6464

6565

66+
def test_shell_markdown_repairs_glued_table_header() -> None:
67+
# The model sometimes glues the header onto preceding prose and drops the
68+
# newline before the |---| delimiter; markdown-it then renders it as raw
69+
# text. The normalizer should rebuild a real table.
70+
output = _render_text(
71+
PythinkerMarkdown(
72+
"● LOW — Various| Category | Issue | Locations |\n\n"
73+
"|----------|-------|-----------| | Error handling | Bare except | 12 files |\n"
74+
)
75+
)
76+
# Header text on its own line, no raw delimiter pipes left in the output.
77+
assert "Category" in output and "Locations" in output
78+
assert "Error handling" in output and "12 files" in output
79+
assert "---" not in output
80+
assert "|----------|" not in output
81+
82+
83+
def test_shell_markdown_leaves_inline_pipes_alone() -> None:
84+
# A stray inline |-| in prose must not be mistaken for a table delimiter.
85+
text = "Use the `a | b` operator. See |--| inline here."
86+
output = _render_text(PythinkerMarkdown(text))
87+
assert "operator" in output and "inline here" in output
88+
89+
90+
def test_shell_markdown_keeps_table_like_pipes_in_code_fence() -> None:
91+
output = _render_text(PythinkerMarkdown("```\n| not | a | table |\n|-----|---|-------|\n```\n"))
92+
# Inside a fence the pipes and delimiter survive verbatim.
93+
assert "| not | a | table |" in output
94+
assert "|-----|---|-------|" in output
95+
96+
6697
def test_shell_markdown_pads_code_block_with_blank_rows() -> None:
6798
# The code block should read as a distinct section, with a blank row framing
6899
# the panel above and below so it never crowds the surrounding prose.

tests/utils/test_pyinstaller_utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@ def test_pyinstaller_hiddenimports():
284284
"pythinker_code.tools.todo",
285285
"pythinker_code.tools.utils",
286286
"pythinker_code.tools.web",
287+
"pythinker_code.tools.web._allowlist",
287288
"pythinker_code.tools.web.fetch",
288289
"pythinker_code.tools.web.search",
289290
"setproctitle",

0 commit comments

Comments
 (0)