[content] crowd-pilot/serialization_utils.py (python)
def session_to_bash_formatted_transcript(
    df: pd.DataFrame,
    viewport_radius: int = 10,
) -> str:
    r"""
    Serialize a session to a bash-like transcript comprised of:
      - Commands (bash fenced blocks): cat -n, sed -i 'S,Ec\...' && cat -n | sed -n 'VSTART,VENDp'
      - Outputs (<stdout>...</stdout>) that reflect the file state after each action
    Tracks per-file state and a per-file viewport. Viewport only shifts when selection moves out of bounds
    or when first initialized.
    """
    file_states: Dict[str, str] = {}
    per_file_viewport: Dict[str, Optional[Tuple[int, int]]] = {}

    parts: List[str] = []

    for i in range(len(df)):
        row = df.iloc[i]
        file_path: str = row["File"]
        event_type = row["Type"]

        match event_type:
            case "tab":
                text = row["Text"]
                if pd.notna(text):
                    content = str(text).replace("\
", "
").replace("\
", "
")
                    file_states[file_path] = content
                content = file_states.get(file_path, "")
                cmd = f"cat -n {file_path}"
                parts.append(_fenced_block(file_path, "bash", _clean_text(cmd)))
                output = _line_numbered_output(content)
                parts.append(f"<stdout>
{output}
</stdout>")

            case "content":
                offset = int(row["RangeOffset"])
                length = int(row["RangeLength"])
                new_text = row["Text"]
                before = file_states.get(file_path, "")
                after = _apply_change(before, offset, length, new_text)
                start_line, end_line, repl_lines = _compute_changed_block_lines(before, after)
                # Prepare sed replacement payload
                escaped_lines = [_escape_single_quotes_for_sed(line) for line in repl_lines]
                sed_payload = "
".join(escaped_lines)
                sed_cmd = f"sed -i '{start_line},{end_line}c\\
{sed_payload}' {file_path}"
                # Determine viewport (initialize if needed; otherwise do not move it for edits)
                total_lines = len(after.splitlines())
                vp = per_file_viewport.get(file_path)
                if not vp or vp[1] == 0:
                    center = (start_line + end_line) // 2
                    vp = _compute_viewport(total_lines, center, viewport_radius)
                    per_file_viewport[file_path] = vp
                vstart, vend = vp
                chained_cmd = f"{sed_cmd} && cat -n {file_path} | sed -n '{vstart},{vend}p'"
                parts.append(_fenced_block(file_path, "bash", _clean_text(chained_cmd)))
                # Update state after emitting command
                file_states[file_path] = after
                viewport_output = _line_numbered_output(after, vstart, vend)
                parts.append(f"<stdout>
{viewport_output}
</stdout>")

            case "selection_command" | "selection_mouse" | "selection_keyboard":
                offset = int(row["RangeOffset"])
                content = file_states.get(file_path, "")
                total_lines = len(content.splitlines())
                target_line = _offset_to_line(content, offset)
                vp = per_file_viewport.get(file_path)
                should_emit = False
                if not vp or vp[1] == 0:
                    vp = _compute_viewport(total_lines, target_line, viewport_radius)
                    per_file_viewport[file_path] = vp
                    should_emit = True
                else:
                    vstart, vend = vp
                    if target_line < vstart or target_line > vend:
                        vp = _compute_viewport(total_lines, target_line, viewport_radius)
                        per_file_viewport[file_path] = vp
                        should_emit = True
                if should_emit and vp:
                    vstart, vend = vp
                    cmd = f"cat -n {file_path} | sed -n '{vstart},{vend}p'"
                    parts.append(_fenced_block(file_path, "bash", _clean_text(cmd)))
                    viewport_output = _line_numbered_output(content, vstart, vend)
                    parts.append(f"<stdout>
{viewport_output}
</stdout>")

            case "terminal_command":
                command = row["Text"]
                command_str = str(command).replace("\
", "
").replace("\
", "
")
                parts.append(_fenced_block(file_path, "bash", _clean_text(command_str)))

            case "terminal_output":
                output = row["Text"]
                output_str = str(output).replace("\
", "
").replace("\
", "
")
                parts.append(f"<stdout>
{_clean_text(output_str)}
</stdout>")

            case "terminal_focus" | "git_branch_checkout":
                # Omit in bash-like transcript
                pass

            case _:
                # Unknown events are ignored for bash transcript
                pass

    return "
".join(parts).strip()