# pane


<!-- WARNING: THIS FILE WAS AUTOGENERATED! DO NOT EDIT! -->

While a turn runs, the chat shows the answer as it streams and one line
per tool call. It does not show, at a glance, which step the turn is on,
how far the plan has got, what each sub-agent is doing, or which files
have changed so far. The now pane shows all of that in a narrow column
beside the chat. It is called the now pane because it shows only the
present: what is running at this moment and what this turn has done.

The pane runs as a separate program, `ramabana-pane`, in a tmux split.
tmux is a terminal multiplexer: it divides one terminal window into
panes, each running its own program. Because the pane is a separate
process, the terminal session hands it information through a file.
Several times a second the session writes a snapshot, a JSON file called
`now.json` in the session’s run folder, and the pane reads that file and
draws it. This page covers the three parts in order: taking a snapshot,
laying it out as rows of text, and the viewer that draws those rows and
answers keys and clicks.

## The snapshot

`now_snapshot(agent)` returns a plain dict that `json.dumps` can write.
Its keys are:

- `root`, the foreground turn: how many steps it has taken, how long it
  has run, its state, the call it is waiting on (`current`), and its
  `status`, the last line the model wrote before calling a tool.
- `plan`, the agent’s checklist, with each item’s text and status.
- `subs`, one row per sub-agent. A sub-agent is a second agent the turn
  starts to do part of the work. The list holds every delegation this
  turn started, and every background delegation that is running or has
  finished since the turn began. A row carries the sub-agent’s status,
  its last `CALLS_KEPT` calls with their results cut short, and its
  answer, cut to `ANSWER_CHARS`, once it has finished. A finished
  sub-agent stays listed until the next turn starts.
- `files`, the files this turn wrote: path, lines added and removed, and
  the diff cut to `DIFF_LINES` lines.
- `notes`, what the model wrote before each of this turn’s calls, in
  full and with a timestamp.
- `background`, the work running behind the turn: background
  delegations, commands started with `run_shell_bg`, and folder watches.
  Each row has a kind, an id, a label, a state and the seconds it has
  been running. A finished shell command or a watch has no clock, so its
  `elapsed` is `None`.

The session writes a snapshot up to five times a second, so taking one
has to be cheap when nothing has changed. The list of files is worked
out again only after a call finishes. The state of a background shell
command is asked of the host at most once every `SHELL_EVERY` seconds
while it runs, and never again once it has ended.

Each recorded call (an
[`Act`](https://vedicreader.github.io/ramabana/agent.html#act)) names
the run whose model made it, so a sub-agent’s step count is the number
of calls with its run id. A step of the main turn is counted the way the
terminal counts it: a call whose `parent_action_id` does not name
another call in the activity log. A sub-agent’s calls name the
[`delegate`](https://vedicreader.github.io/ramabana/tools.html#delegate)
call that started it, so they never count as steps of the main turn.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L106"
target="_blank" style="float:right; font-size:smaller">source</a>

### now_snapshot

``` python
def now_snapshot(
    agent
):
```

*The foreground turn, its plan, sub-agents, writes and background work,
as a JSON-able dict.*

The example builds an agent with
[`fake_agent`](https://vedicreader.github.io/ramabana/testing.html#fake_agent)
and a scripted model, `Root`. The model reads a file, then asks a
sub-agent one question with `delegate_search`, and the sub-agent reads
the same file before it answers. The activity log’s `on_change` hook
takes a snapshot when the sub-agent’s read starts, which is the moment
the pane would show both agents working. The main turn has taken two
steps and the one sub-agent is running with one step:

``` python
from ramabana.runtime import current_run
from ramabana.testing import ScriptedBackend, Step, fake_agent

READ = ('view_file', {'path': '/proj/a.py'})
a, _ = fake_agent()
seen = []
a.activity.on_change = lambda act: seen.append(now_snapshot(a)) if not act.done and current_run().kind != 'root' else None
class Root(ScriptedBackend):
    def spawn(self, sp='', tools=(), **kw):
        return ScriptedBackend(self.spec, steps=[Step(tool=READ), Step('a() is defined')], token_delay=0, tools=tools)
be = Root(steps=[Step(tool=READ), Step(tool=('delegate_search', {'questions': ['what does a.py define?']})), Step('done')],
          token_delay=0, tools=a.tools, sp=a.system_prompt())
a._be = a._be_or_none = lambda job='turn': be
list(a.stream('go'))
mid = seen[0]
test_eq((mid['busy'], mid['root']['steps'], [(s['state'], s['steps']) for s in mid['subs']]), (True, 2, [('running', 1)]))
mid
```

    {'at': 1792474688.9890528,
     'busy': True,
     'root': {'turn_elapsed': 0.0,
      'steps': 2,
      'state': 'running',
      'status': '',
      'current': 'Delegate: what does a.py define?'},
     'plan': [],
     'subs': [{'id': 'run_5837d1bea07d',
       'question': 'what does a.py define?',
       'elapsed': 0.0,
       'state': 'running',
       'steps': 1,
       'status': '',
       'current': 'View /proj/a.py',
       'calls': [{'line': 'View /proj/a.py',
         'ok': True,
         'done': False,
         'out': ''}],
       'answer': ''}],
     'files': [],
     'background': [],
     'notes': []}

Once the turn ends, the sub-agent stays listed as completed. The next
turn clears it:

``` python
test_eq([s['state'] for s in now_snapshot(a)['subs']], ['completed'])
be.steps = [Step('ok')]
list(a.stream('again'))
test_eq(now_snapshot(a)['subs'], [])
```

The plan and the files this turn wrote are part of the snapshot too,
each file with its diff. Here the example sets a two-item plan and
changes `a.py` by hand:

``` python
a.plan.set('ship', ['read a.py', 'fix it']); a.plan.update(a.plan.todos[0].id, status='active')
a.before['/proj/a.py'] = 'def a(): pass\n'; a.host.files['/proj/a.py'] = 'def a(): return 1\n'
snap = now_snapshot(a)
test_eq(snap['plan'], [{'text': 'read a.py', 'status': 'active'}, {'text': 'fix it', 'status': 'pending'}])
test_eq([(f['path'], f['added'], f['removed']) for f in snap['files']], [('a.py', 1, 1)])
print(snap['files'][0]['diff'])
```

[`write_snapshot`](https://vedicreader.github.io/ramabana/pane.html#write_snapshot)
writes the snapshot to a file and
[`read_snapshot`](https://vedicreader.github.io/ramabana/pane.html#read_snapshot)
reads it back. The writer goes through a temporary file and renames it
into place, so the pane never reads half a file. The reader returns
`None` when the file is missing or does not yet hold valid JSON, and the
pane then keeps showing what it had. The example checks both cases, and
that the temporary file is gone afterwards:

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L130"
target="_blank" style="float:right; font-size:smaller">source</a>

### read_snapshot

``` python
def read_snapshot(
    path
):
```

*The snapshot at `path`, or `None` when it is missing or unreadable.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L122"
target="_blank" style="float:right; font-size:smaller">source</a>

### write_snapshot

``` python
def write_snapshot(
    agent, path
):
```

*Write `now_snapshot(agent)` to `path` atomically.*

``` python
import tempfile
with tempfile.TemporaryDirectory() as d:
    p = Path(d)/'now.json'
    test_eq(read_snapshot(p), None)
    write_snapshot(a, p)
    test_eq((read_snapshot(p)['busy'], [f.name for f in Path(d).iterdir()]), (False, ['now.json']))
    p.write_text('{"busy": tr')
    test_eq(read_snapshot(p), None)
```

## Drawing it

`board(snap, width)` lays a snapshot out as rows for a pane `width`
cells wide, one row per screen line. Each row is a `(key, Text)` pair,
where `Text` is a rich styled string. Long lines are cut at the width,
not wrapped. Use
[`board`](https://vedicreader.github.io/ramabana/pane.html#board) when
you need the rows themselves, as the viewer does to know which row a
click landed on. Use
[`render`](https://vedicreader.github.io/ramabana/pane.html#render) when
you only want something to print: it returns the same rows as one rich
renderable.

From the top, the board shows:

- A header with
  [`main`](https://vedicreader.github.io/ramabana/cli.html#main), its
  step number, its clock and its status line. The chat already shows the
  call the main agent is waiting on, so the header leaves it out. An
  idle agent gets one line.
- The plan as a checklist, with the active item highlighted.
- The sub-agents, one row each: id, status line (or its question when it
  has none), clock, and a mark: `▶` running, `✓` completed, `✗` stopped
  or failed. An open row, marked `▾` in place of `▸`, adds the question,
  each recent call with its result, and the answer once there is one.
- The files this turn wrote, each with its lines added and removed. An
  open row shows the diff, drawn by `hl.diff_rich` with the code
  coloured for the file’s language and the added and removed lines
  tinted.
- The background work: kind, label, state and clock. A row opens to a
  shell command’s last `SHELL_LINES` lines of output, a watch’s pattern
  and last review, or a background sub-agent’s detail.
- The notes, newest first, for reading back what the model said along
  the way. Each is a `▸` row with its time and first line, and opens to
  the whole note wrapped to the pane. The newest note starts open.

Empty sections are left out. A row that can open carries a key:
`('sub', id)`, `('file', path)`, `('bg', id)` or `('note', at)`. The
caller passes the keys of the open rows in `open`, so a row stays open
when the next snapshot arrives. A running clock adds the time since the
snapshot was written, so a long call does not look frozen between
writes. The colours are the terminal session’s default `github-dark`
palette, copied into `PALETTE` here because `ramabana.cli` imports this
module and this module cannot import it back.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L236"
target="_blank" style="float:right; font-size:smaller">source</a>

### render

``` python
def render(
    snap, width:int=40, now:NoneType=None, open:tuple=()
):
```

*The pane for `snap` as a rich renderable, `width` cells wide, with the
rows keyed in `open` opened.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L220"
target="_blank" style="float:right; font-size:smaller">source</a>

### board

``` python
def board(
    snap, width:int=40, now:NoneType=None, open:tuple=(), err:str=''
):
```

*The pane for `snap` as `(key, row)` pairs, one per screen line; a row
with a `key` opens and closes.*

A snapshot of a busy turn, written five seconds before `now`, with every
row closed. The turn’s clock reads 47s, its 42 seconds plus the five
since the write:

``` python
call = lambda line, ok=True, done=True, out='': {'line': line, 'ok': ok, 'done': done, 'out': out}
now = time.time()
busy = {'at': now - 5, 'busy': True,
        'root': {'turn_elapsed': 42, 'steps': 7, 'state': 'running', 'status': 'Checking the pane tests', 'current': 'Run shell: pytest'},
        'plan': [{'text': 'Read the viewer', 'status': 'done'}, {'text': 'Rewrite render', 'status': 'active'},
                 {'text': 'Wire the CLI', 'status': 'pending'}, {'text': 'Poll the file', 'status': 'cancelled'}],
        'subs': [{'id': 'run_aaa', 'question': 'which files import fastllm?', 'elapsed': 12, 'state': 'running', 'steps': 2,
                  'status': 'Searching for imports', 'current': 'Search code: fastllm',
                  'calls': [call('Search code: import fastllm', out='agent.py:12: import fastllm'), call('Search code: fastllm', None, False)],
                  'answer': ''},
                 {'id': 'run_bbb', 'question': 'is the pane tested?', 'elapsed': 8, 'state': 'completed', 'steps': 1, 'status': '',
                  'current': '', 'calls': [call('Read tests/test_pane.py')], 'answer': 'Yes, in tests/test_pane.py.'}],
        'files': [{'path': 'ramabana/pane.py', 'added': 1, 'removed': 1, 'diff': '@@ -1 +1 @@\n-from rich.live import Live\n+from teleprint.keys import Parser'}],
        'background': [{'kind': 'shell', 'id': 'cmd_1234abcd', 'label': 'pytest -x tests', 'state': 'running', 'elapsed': 30}]}
def show(snap, open=()):
    con = Console(record=True, width=40, color_system=None)
    con.print(render(snap, 40, now=now, open=open))
    return con.export_text()
out = show(busy)
assert '47s' in out and 'Run shell' not in out and '▸ run_aaa' in out and 'import fastllm' not in out
assert all(len(l) <= 40 for l in out.splitlines())
print(out)
```

Passing the keys of the running sub-agent and the file in `open` shows
the sub-agent’s calls and the diff:

``` python
out = show(busy, {('sub', 'run_aaa'), ('file', 'ramabana/pane.py')})
assert '▾ run_aaa' in out and '✓ Search code: import fastllm' in out and '+from teleprint.keys import Parser' in out
print(out)
```

In an open file the diff is drawn by `hl.diff_rich` in the pane’s
palette. The code in each hunk is coloured by the file’s language, and
added and removed lines get a tinted background. The cell checks that an
added line carries more than two styles, so the code is highlighted and
not drawn in one colour:

``` python
py = {**busy, 'files': [{'path': 'a.py', 'added': 1, 'removed': 1, 'diff': diff_text('x = 1\n', 'def f(): return 2\n', 'a.py')}]}
rows = [t for _, t in board(py, 40, now=now, open={('file', 'a.py')})]
added = next(t for t in rows if 'def f' in t.plain)
test_eq(added.plain, '  +def f(): return 2')
assert len({str(s.style) for s in added.spans}) > 2, 'the code is highlighted, not one colour'
added
```

## The viewer

`Viewer(path, tty)` is the program that runs in the pane. It reads the
snapshot at `path` and draws the board on `tty`. A `tty` is any object
with `read(timeout)`, `write`, `size` and `restore`, as teleprint’s
`RealTty` has.
[`PaneTty`](https://vedicreader.github.io/ramabana/pane.html#panetty) is
`RealTty` with a `read` that returns `None` at the end of input, so the
viewer can tell that its terminal has gone.

The viewer switches the terminal to the alternate screen, the separate
screen full-screen programs use so that the shell’s scrollback comes
back unchanged when they exit, and asks the terminal to report mouse
clicks. Each
[`tick`](https://vedicreader.github.io/ramabana/monitor.html#tick) waits
up to `every` seconds for input and acts on it. The controls are:

- A click opens or closes the row under it, and the mouse wheel scrolls.
- `j` and `k`, or the arrow keys, move the cursor over the rows that
  open. Past the first or last such row they scroll, so a board taller
  than the pane can be read to its end.
- Page up and page down scroll by a page, and `g` and `G` jump to the
  top and the bottom.
- Enter or space opens or closes the row under the cursor.
- `p` on a background shell command opens a tmux pane below this one
  that follows the command’s log with `less +F`. `q` closes it.
- `q` or ctrl+c quits.

The last line of the pane is always `HINT`, so the main keys stay on
screen however far the board scrolls.

The viewer reads the snapshot again only when its file has changed. It
lays out the board on every tick but writes only the lines that differ
from what is on screen, so a clock that moves once a second rewrites one
line a second and a board that is not changing writes nothing. When the
terminal changes size, the viewer clears it and draws everything again.

The viewer must keep running whatever a single frame does. If drawing
fails, it keeps the last good snapshot and shows the error as a row
until a good snapshot arrives. However it exits, it gives the terminal
back its normal screen and modes. When the user quits, it leaves a file
named by
[`quit_mark`](https://vedicreader.github.io/ramabana/pane.html#quit_mark)
beside the snapshot, `now.closed`. The terminal session checks for it,
so it can tell a pane the user closed, which it leaves closed, from one
that crashed. A hangup (tmux closing the pane), SIGTERM and the end of
input count as quitting too.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L266"
target="_blank" style="float:right; font-size:smaller">source</a>

### Viewer

``` python
def Viewer(
    path, tty
):
```

*The board for the snapshot at `path`, drawn on `tty`: the last good
snapshot, the open rows, the cursor and the scroll.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L256"
target="_blank" style="float:right; font-size:smaller">source</a>

### PaneTty

``` python
def PaneTty():
```

*`RealTty` whose `read` returns None at the end of input, where
`RealTty.read` would spin.*

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/pane.py#L252"
target="_blank" style="float:right; font-size:smaller">source</a>

### quit_mark

``` python
def quit_mark(
    path
):
```

*The file a viewer the user quit leaves beside the snapshot at `path`,
so the CLI keeps it closed.*

The example drives a
[`Viewer`](https://vedicreader.github.io/ramabana/pane.html#viewer) with
a test terminal, `Tty`, that replays scripted input and keeps everything
written to it. A click on the running sub-agent opens it and puts the
cursor there. A tick with nothing new writes nothing. Then `j` moves the
cursor down, space opens `run_bbb`, and `q` quits:

``` python
class Tty:
    "A terminal for tests: scripted input chunks, a size, and every write kept."
    def __init__(self, *input, size=(40, 30)): self.input, self.size, self.writes = list(input), size, []
    def read(self, timeout=0): return self.input.pop(0).encode() if self.input else b''
    def write(self, s): self.writes.append(s)
    def restore(self): pass
with tempfile.TemporaryDirectory() as d:
    (p := Path(d)/'now.json').write_text(json.dumps(busy))
    v = Viewer(p, Tty()); v.tick(now=now)
    y = next(i for i, (k, _) in enumerate(v.rows) if k == ('sub', 'run_aaa'))
    v.tty.input.append(f'\x1b[<0;3;{y + 1}M'); v.tick(now=now)
    test_eq((v.open, v.cursor), ({('sub', 'run_aaa')}, ('sub', 'run_aaa')))
    n = len(v.tty.writes); v.tick(now=now)
    test_eq(len(v.tty.writes), n)
    v.tty.input += ['j', ' ', 'q']
    for _ in range(3): v.tick(now=now)
    test_eq((v.cursor, v.open, v.done), (('sub', 'run_bbb'), {('sub', 'run_aaa'), ('sub', 'run_bbb')}, True))
```

[`main`](https://vedicreader.github.io/ramabana/cli.html#main) is the
viewer’s command line: `ramabana-pane PATH` runs the viewer on the
terminal it was started in. The terminal session starts it in a tmux
split when you type `/pane`, and prints the command when there is no
tmux, so you can run it in another terminal yourself. `--every` sets how
long each tick waits for input.

------------------------------------------------------------------------

<a
href="https://github.com/vedicreader/ramabana/blob/main/ramabana/cli.py#L2411"
target="_blank" style="float:right; font-size:smaller">source</a>

### main

``` python
def main(
    path:str, # the snapshot file the CLI writes
    every:float=0.2, # seconds to wait for input between frames
):
```

*Show the `now` snapshot at `path` as a board until q or ctrl+c.*
