From e5fb98a07270f97bb2b1cb71ed43b9ce0177d37e Mon Sep 17 00:00:00 2001 From: Thibault Dody Date: Thu, 29 Oct 2026 18:43:57 -0400 Subject: [PATCH 1/6] notify: a done banner says the transcript's reply (w.last_reply) before recap and last_line (THI-416) Co-Authored-By: Claude Opus 5.5 (1M context) --- static/src/notify/__tests__/copy.test.js | 12 ++++++++++++ static/src/notify/copy.js | 4 +++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/static/src/notify/__tests__/copy.test.js b/static/src/notify/__tests__/copy.test.js index 4f25035f..0ad8eda2 100644 --- a/static/src/notify/__tests__/copy.test.js +++ b/static/src/notify/__tests__/copy.test.js @@ -54,6 +54,18 @@ describe("payloadFor", () => { expect(payloadFor({ kind: "done", w: w() }).body).toBe("Finished."); }); + it("says a done pane's reply from its transcript over anything read off its screen (THI-416)", () => { + // last_line is a screen scrape: the turn's `✻ … for 42s` line, a footer + // statusline when the composer goes unrecognised. + const status = "📁 periscope 🌿 main ✎6 | 🧠███░░░░░░░ 39% 📨 2 📤 403 | session: 387.5k in / 403 out 💰 $121.06 🤖 opus"; + const body = (over) => payloadFor({ kind: "done", w: w(over) }).body; + expect(body({ last_reply: "I updated FDY-8473. It now covers the call.", recap: "An older recap.", last_line: status })) + .toBe("I updated FDY-8473."); + expect(body({ recap: "Opened the PR. Tests pass.", last_line: status })).toBe("Opened the PR."); + expect(body({ last_line: "Merged it. Prod restarted." })).toBe("Merged it."); + expect(body({ last_reply: "", recap: null, last_line: "" })).toBe("Finished."); + }); + it("lists the failing checks first and links the PR", () => { const p = payloadFor({ kind: "ci_failed", diff --git a/static/src/notify/copy.js b/static/src/notify/copy.js index c8cce49d..73e4dc16 100644 --- a/static/src/notify/copy.js +++ b/static/src/notify/copy.js @@ -96,7 +96,9 @@ export function payloadFor(ev, ctx = {}) { // The linked PR only: an auto-detected one is just the branch's, often // days old, and would ride every finished turn. subtitle: w.pr && w.pr_linked ? `${w.session} · PR #${w.pr}` : where(w), - body: firstSentence(w.recap || w.last_line) || "Finished.", + // The reply from the transcript first: recap and last_line are read off + // the screen, where the footer's statusline passed for the reply (THI-416). + body: firstSentence(w.last_reply || w.recap || w.last_line) || "Finished.", category: "done_seen", data: { open: "pane", pid: w.pid, session: w.session }, }; case "ci_failed": { From ac8309216e6f78403d1764e2c93614a36718941e Mon Sep 17 00:00:00 2001 From: Thibault Dody Date: Thu, 29 Oct 2026 18:47:26 -0400 Subject: [PATCH 2/6] turns: publish a done pane's last reply from its transcript as last_reply, read backward from the end and cached per session (THI-416) Co-Authored-By: Claude Opus 5.5 (1M context) --- periscope/turns.py | 112 ++++++++++++++++++++++++++++++ periscope/window_view.py | 8 ++- tests/test_turns.py | 141 ++++++++++++++++++++++++++++++++++++++ tests/test_window_view.py | 21 ++++++ 4 files changed, 281 insertions(+), 1 deletion(-) diff --git a/periscope/turns.py b/periscope/turns.py index e6817a80..3acea0bd 100644 --- a/periscope/turns.py +++ b/periscope/turns.py @@ -13,6 +13,11 @@ Imports only periscope.* / history.* — never `from server import`.""" +import json +import re +import threading +from pathlib import Path + import periscope.activity as activity import periscope.session_status as session_status from history.search import messages_from_jsonl @@ -56,6 +61,113 @@ def jsonl_for_session_prefix(prefix: str): return matches[0] if matches else None +# ── last_reply: what Claude last said, for a done banner (THI-416) ─────── +# The screen can't say it: the footer's statusline, the turn's `✻ Brewed for +# 42s` line and tool results all passed for the reply. The transcript holds it +# as the newest assistant text, a few KB from the end under Claude's +# bookkeeping rows (turn_duration, artifact-autoreact-ledger, mode, …). +_REPLY_CHUNK = 64 * 1024 +# Bounds the read for a turn that ended in a long run of tool calls. +_REPLY_REACH = 1024 * 1024 +# The banner shows the first sentence, clipped at 140; this rides every poll. +_REPLY_CHARS = 400 +# sid -> (path, size, mtime_ns, reply). A transcript never moves, so the glob +# runs once per session; a done pane's file sits still between polls, so the +# read runs once per turn. Cleared past _REPLY_SESSIONS (one row per session +# the server has seen finish). +_reply_cache: dict[str, tuple[Path, int, int, str | None]] = {} +_reply_lock = threading.Lock() +_REPLY_SESSIONS = 256 + +_FENCE_RE = re.compile(r"^\s*(`{3,}|~{3,}).*?^\s*\1[^\n]*$", re.MULTILINE | re.DOTALL) +_HEADING_RE = re.compile(r"^\s*#{1,6}\s.*$", re.MULTILINE) +_LINE_MARK_RE = re.compile(r"^\s*(?:>\s?|[-*+]\s+|\d+[.)]\s+)", re.MULTILINE) +_LINK_RE = re.compile(r"!?\[([^\]]*)\]\([^)]*\)") +# `**` and backticks only: `__`/`_`/`*` also sit inside identifiers. +_EMPHASIS_RE = re.compile(r"\*\*|`+") + + +def _message_text(msg) -> str: + """An event's text, "" for a tool call or result, thinking, or no message.""" + if not isinstance(msg, dict): + return "" + content = msg.get("content") + if isinstance(content, str): + return content + if not isinstance(content, list): + return "" + return "\n".join(b["text"] for b in content if isinstance(b, dict) and b.get("type") == "text" and b.get("text")) + + +def _plain(md: str) -> str: + """A reply's markdown as one line of prose: a banner shows it raw, where a + link's URL would eat the 140 characters. Headings and code blocks go; they + are never the sentence.""" + text = _FENCE_RE.sub("", md) + text = _HEADING_RE.sub("", text) + text = _LINE_MARK_RE.sub("", text) + text = _LINK_RE.sub(r"\1", text) + text = _EMPHASIS_RE.sub("", text) + return " ".join(text.split())[:_REPLY_CHARS] + + +def last_reply(path) -> str | None: + """The newest assistant text in transcript `path`, as plain prose; None when + a prompt came after it (the new turn has said nothing yet), none sits in the + last _REPLY_REACH bytes, or the file can't be read. Meta and sidechain + events are skipped: a channel push, a subagent's words.""" + reach = _REPLY_CHUNK + try: + with open(path, "rb") as f: + size = f.seek(0, 2) + while True: + start = max(0, size - reach) + f.seek(start) + rows = f.read(size - start).split(b"\n") + for row in reversed(rows[1:] if start else rows): # rows[0] starts mid-row + try: + ev = json.loads(row) + except ValueError: + continue + if not isinstance(ev, dict) or ev.get("isMeta") or ev.get("isSidechain"): + continue + text = _message_text(ev.get("message")) + if text.strip() and ev.get("type") == "assistant": + return _plain(text) or None + if text.strip() and ev.get("type") == "user": + return None + if not start or reach >= _REPLY_REACH: + return None + reach = min(reach * 4, _REPLY_REACH) + except OSError: + return None + + +def last_reply_for_session(session_id: str | None) -> str | None: + """last_reply of the session's transcript, re-read only when it changes.""" + if not session_id: + return None + with _reply_lock: + hit = _reply_cache.get(session_id) + path = hit[0] if hit else jsonl_for_session(session_id) + if path is None: + return None + try: + st = path.stat() + except OSError: + with _reply_lock: + _reply_cache.pop(session_id, None) + return None + if hit and hit[1:3] == (st.st_size, st.st_mtime_ns): + return hit[3] + reply = last_reply(path) + with _reply_lock: + if len(_reply_cache) >= _REPLY_SESSIONS: + _reply_cache.clear() + _reply_cache[session_id] = (path, st.st_size, st.st_mtime_ns, reply) + return reply + + # Delivered channel pushes render into the recipient's context as a meta user # turn opening with this marker (see channels.emit_channel_event). CHANNEL_MARKER = ' str | None: "acted_at": acked, "completed_at": completed, "needs_since": needs_since, + # What a done banner says: the reply, from the transcript, where + # last_line is the screen's guess (THI-416). Only a done pane notifies, + # and a working pane's file grows every poll. + "last_reply": last_reply_for_session(sid) + if parsed.get("state") == "done" and parsed.get("agent") == "claude" + else None, # The request a banner's Allow / Deny would answer (or null: Open only). "pending_permission": pending_permission, "mem": mem_signal(claude_proc_for(sid)), diff --git a/tests/test_turns.py b/tests/test_turns.py index 13c178a5..12d206a3 100644 --- a/tests/test_turns.py +++ b/tests/test_turns.py @@ -172,6 +172,147 @@ def test_none_when_no_transcript(tmp_path, monkeypatch): assert turns.get_turns_for_pane("main", 0) is None +# ── last_reply: what a done banner says (THI-416) ──────────────────────── +# Shapes from real 2.1.x transcripts: each content block is its own event, +# and the reply is followed by bookkeeping rows that carry no message. + + +def _prompt(text): + return {"type": "user", "message": {"role": "user", "content": text}} + + +def _said(*blocks, **extra): + return {"type": "assistant", "message": {"role": "assistant", "content": list(blocks)}, **extra} + + +def _text(t): + return {"type": "text", "text": t} + + +_THINKING = _said({"type": "thinking", "thinking": "hmm"}) +_TOOL_USE = _said({"type": "tool_use", "id": "t1", "name": "Bash", "input": {"command": "ls"}}) +_TOOL_RESULT = {"type": "user", "message": {"role": "user", "content": [{"type": "tool_result", "content": "ok"}]}} +_BOOKKEEPING = [ + {"type": "system", "subtype": "turn_duration", "durationMs": 42000}, + {"type": "artifact-autoreact-ledger", "entries": []}, + {"type": "mode", "mode": "auto"}, +] + + +def _transcript(tmp_path, *events, name="s1.jsonl"): + path = tmp_path / name + path.write_text("".join(json.dumps(e) + "\n" for e in events)) + return path + + +def test_last_reply_is_the_newest_assistant_text_under_the_bookkeeping(tmp_path): + path = _transcript( + tmp_path, + _prompt("fix the banner"), + _said(_text("Looking at copy.js first.")), + _TOOL_USE, + _TOOL_RESULT, + _THINKING, + _said(_text("Fixed it. The banner now says the reply.")), + *_BOOKKEEPING, + ) + assert turns.last_reply(path) == "Fixed it. The banner now says the reply." + + +def test_last_reply_answers_a_notification_turn_too(tmp_path): + # A background agent's starts a turn like a prompt does. + path = _transcript( + tmp_path, + _said(_text("Waiting on the agent.")), + _prompt("\na7\n"), + _said(_text("That's the report I summarized above.")), + *_BOOKKEEPING, + ) + assert turns.last_reply(path) == "That's the report I summarized above." + + +def test_last_reply_is_none_once_a_prompt_follows_it(tmp_path): + # The new turn has no words yet; the previous turn's would describe the + # wrong thing. + path = _transcript(tmp_path, _said(_text("Done with the first ask.")), _prompt("now the second"), _TOOL_USE) + assert turns.last_reply(path) is None + + +def test_last_reply_reads_past_meta_and_sidechain_events(tmp_path): + path = _transcript( + tmp_path, + _said(_text("The real reply.")), + {"type": "user", "isMeta": True, "message": {"role": "user", "content": 'hi'}}, + _said(_text("A subagent's words."), isSidechain=True), + *_BOOKKEEPING, + ) + assert turns.last_reply(path) == "The real reply." + + +def test_last_reply_flattens_markdown_for_a_banner(tmp_path): + reply = ( + "## Summary\n\n" + "I updated [FDY-8473](https://linear.app/x/issue/FDY-8473/grade) and **merged** `#534`.\n\n" + "```sh\nbin/periscope restart\n```\n\n" + "- first item\n" + "1. second item\n" + "> quoted" + ) + path = _transcript(tmp_path, _said(_text(reply))) + assert turns.last_reply(path) == "I updated FDY-8473 and merged #534. first item second item quoted" + + +def test_last_reply_is_capped(tmp_path): + path = _transcript(tmp_path, _said(_text("word " * 500))) + assert len(turns.last_reply(path)) == turns._REPLY_CHARS + + +def test_last_reply_reads_back_past_a_chunk_of_bookkeeping(tmp_path): + ledger = {"type": "artifact-autoreact-ledger", "pad": "x" * 1000} + rows = [ledger] * (2 * turns._REPLY_CHUNK // 1000) + path = _transcript(tmp_path, _said(_text("Still found.")), *rows) + assert turns.last_reply(path) == "Still found." + + +def test_last_reply_gives_up_past_its_reach(tmp_path, monkeypatch): + monkeypatch.setattr(turns, "_REPLY_REACH", 4 * turns._REPLY_CHUNK) + ledger = {"type": "artifact-autoreact-ledger", "pad": "x" * 1000} + rows = [ledger] * (5 * turns._REPLY_CHUNK // 1000) + path = _transcript(tmp_path, _said(_text("Too far back.")), *rows) + assert turns.last_reply(path) is None + + +def test_last_reply_skips_a_torn_or_foreign_row(tmp_path): + path = tmp_path / "s1.jsonl" + path.write_text(json.dumps(_said(_text("Survives."))) + "\n[1, 2]\n" + '{"type": "assist') + assert turns.last_reply(path) == "Survives." + + +def test_last_reply_for_session_finds_the_file_and_caches_by_size(tmp_path, monkeypatch): + monkeypatch.setattr(activity, "_PROJECTS_DIR", tmp_path) + monkeypatch.setattr(turns, "_reply_cache", {}) + (tmp_path / "-Users-tom-dev-x").mkdir() + path = _transcript(tmp_path / "-Users-tom-dev-x", _said(_text("First.")), name="sid-R.jsonl") + reads = [] + real = turns.last_reply + monkeypatch.setattr(turns, "last_reply", lambda p: reads.append(p) or real(p)) + + assert turns.last_reply_for_session("sid-R") == "First." + assert turns.last_reply_for_session("sid-R") == "First." + assert len(reads) == 1 # a done pane's file sits still between polls + with path.open("a") as f: + f.write(json.dumps(_said(_text("Second."))) + "\n") + assert turns.last_reply_for_session("sid-R") == "Second." + assert len(reads) == 2 + + +def test_last_reply_for_session_is_none_without_a_transcript(tmp_path, monkeypatch): + monkeypatch.setattr(activity, "_PROJECTS_DIR", tmp_path) + monkeypatch.setattr(turns, "_reply_cache", {}) + assert turns.last_reply_for_session(None) is None + assert turns.last_reply_for_session("sid-gone") is None + + # ── pane_session_hook producer (SessionStart / UserPromptSubmit) ───────── diff --git a/tests/test_window_view.py b/tests/test_window_view.py index d10a1a70..f31acb6d 100644 --- a/tests/test_window_view.py +++ b/tests/test_window_view.py @@ -162,6 +162,27 @@ def test_view_done_refinement_promotes_idle_to_done_after_busy(mocker, clean_sta assert stamp == (pid, 5000, 0, 0) +def test_view_publishes_a_done_panes_reply_from_its_transcript(mocker, clean_state): + # THI-416: the done banner's body. Read only for a done pane, the one + # state that notifies, so a working pane's growing file isn't re-read. + from periscope import panes + from periscope.window_view import build_window_view + + mocker.patch("periscope.window_view.parse_pane", return_value={"agent": "claude", "state": "idle", "spinner": None}) + _stub_subsystems(mocker, pane_content="x") + mocker.patch("periscope.window_view.session_id_for_pane", return_value="sid-1") + reply = mocker.patch("periscope.window_view.last_reply_for_session", return_value="Fixed it.") + + view, _ = build_window_view(_window(), now_ts=4000) + assert (view["state"], view["last_reply"]) == ("idle", None) + reply.assert_not_called() + + panes._prev_state["abc12345"] = "working" + view, _ = build_window_view(_window(activity=1), now_ts=5000) + assert (view["state"], view["last_reply"]) == ("done", "Fixed it.") + reply.assert_called_once_with("sid-1") + + def test_view_stamps_when_a_pane_started_waiting(mocker, clean_state): """needs_since holds the needs-input edge across polls and clears when the pane stops waiting, so a question's age doesn't reset on a reload.""" From f948141c06d3bf513cb8a793b981a72ed9b4bfed Mon Sep 17 00:00:00 2001 From: Thibault Dody Date: Thu, 29 Oct 2026 18:51:45 -0400 Subject: [PATCH 3/6] =?UTF-8?q?panes:=20last=5Fline=20is=20the=20reply=20w?= =?UTF-8?q?hen=20the=20transcript=20can't=20say:=20a=20named=20session's?= =?UTF-8?q?=20composer,=20the=20turn's=20=E2=9C=BB/=E2=8E=BF=20lines,=20ri?= =?UTF-8?q?ght-aligned=20hints=20and=20OSC=208=20links=20no=20longer=20pas?= =?UTF-8?q?s=20for=20it=20(THI-416)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 (1M context) --- periscope/panes.py | 53 ++++++++++++++++++++++++-------- tests/test_panes.py | 75 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 111 insertions(+), 17 deletions(-) diff --git a/periscope/panes.py b/periscope/panes.py index 625a5cd1..acf3dd4f 100644 --- a/periscope/panes.py +++ b/periscope/panes.py @@ -452,13 +452,14 @@ def list_windows() -> list[dict]: def _split_buffers(content: str) -> tuple[list[str], list[str], list[str]]: """Split capture() output into (raw_rows, plain_rows, lines). - `content` includes SGR escape sequences (capture -e). `raw_rows` keeps + `content` includes escape sequences (capture -e). `raw_rows` keeps them — the prompt-line ghost-text check needs the fg-color info to tell real input from Claude's greyed-out suggestion. `plain_rows` strips - them; `lines` is the non-empty plain rows. + them, OSC 8 hyperlinks included (a linkified ticket reached the done + banner as `\\x1b]8;id=…`, THI-416); `lines` is the non-empty plain rows. """ raw_rows = content.rstrip("\n").split("\n") - plain_rows = [_ANSI_SGR_RE.sub("", row) for row in raw_rows] + plain_rows = [_strip_escapes(row) for row in raw_rows] lines = [p for p in plain_rows if p.strip() != ""] return raw_rows, plain_rows, lines @@ -814,6 +815,12 @@ def _dialog_above(rows: list[str]) -> bool: return False +# The composer's top rule: plain, or a named session's with the name at its +# right end (`──── thi-416-banner ─`), which hid the composer and handed the +# footer to last_line (THI-416). The lower rule and a dialog's stay plain. +_COMPOSER_TOP_RE = re.compile(r"^─{20,}(?: [^─]+ ─+)?\s*$") + + def _composer_rows(content: str, footer_reach: int = _FOOTER_REACH) -> list[tuple[str, str]] | None: """(raw, plain) of the non-empty rows between the LAST two full-width rules, where Claude draws its composer, or None when there is no such @@ -822,14 +829,18 @@ def _composer_rows(content: str, footer_reach: int = _FOOTER_REACH) -> list[tupl raw_rows = content.rstrip("\n").split("\n") plain_rows = [_strip_escapes(row).rstrip() for row in raw_rows] rules = [i for i, row in enumerate(plain_rows) if DIALOG_RULE_RE.match(row)] - if len(rules) < 2: + if not rules: + return None + bottom = rules[-1] + top = next((i for i in reversed(range(bottom)) if _COMPOSER_TOP_RE.match(plain_rows[i])), None) + if top is None: return None - footer = [row for row in plain_rows[rules[-1] + 1 :] if row.strip()] + footer = [row for row in plain_rows[bottom + 1 :] if row.strip()] if len(footer) > footer_reach or any(_EXITED_RE.match(row) for row in footer): return None - if _dialog_above(plain_rows[: rules[-2]]): + if _dialog_above(plain_rows[:top]): return None - return [(raw_rows[i], plain_rows[i]) for i in range(rules[-2] + 1, rules[-1]) if plain_rows[i].strip()] + return [(raw_rows[i], plain_rows[i]) for i in range(top + 1, bottom) if plain_rows[i].strip()] def _detect_composer(content: str) -> bool: @@ -914,18 +925,36 @@ def _extract_recap(lines: list[str]) -> str | None: return re.sub(r"\s+", " ", recap)[:400] +# Claude draws hints like `new task? /clear to save 433.4k tokens` flush right, +# just above the composer. A reply's prose is indented 2 under its ⏺, a list a +# few more. +_RIGHT_ALIGNED_INDENT = 20 + + +def _is_turn_chrome(line: str) -> bool: + """True for the turn's own lines that aren't the reply: its `✻ Brewed for + 42s · done 1:27 PM` or `✻ Waiting for 1 background agent` line, a `⎿` tool + result or tip, a right-aligned hint. Not in _is_chrome_line: the asked- + question check reads the line above the `✻` indicator through it.""" + s = line.strip() + if s.startswith(("✻", "⎿")) or IDLE_INDICATOR_RE.match(line): + return True + return len(line) - len(line.lstrip()) >= _RIGHT_ALIGNED_INDENT + + def _last_meaningful_line(lines: list[str]) -> str: - """The closest "real" content line from the bottom — recent prose, a - subtask line, or a past-tense indicator — skipping TUI chrome. Used for - shell panes and the card snippet fallback. "" if nothing real. + """The closest line of the reply from the bottom, skipping TUI chrome and + the turn's own (_is_turn_chrome): what a done banner says when the + transcript can't (THI-416). "" if nothing real. """ for line in reversed(lines): s = line.strip() if not s: continue - if _is_chrome_line(line): + if _is_chrome_line(line) or _is_turn_chrome(line): continue - return s[:200] + # A reply's first row opens with Claude's ⏺; the banner wants the words. + return s.removeprefix("⏺").lstrip()[:200] return "" diff --git a/tests/test_panes.py b/tests/test_panes.py index 4917f3a7..6ac76368 100644 --- a/tests/test_panes.py +++ b/tests/test_panes.py @@ -105,6 +105,10 @@ ) # The full-width rules Claude draws above and below its composer. RULE = "─" * 60 +# A named session's top rule carries the name at its right end (2.1.x, -e). +NAMED_RULE = f"\x1b[38;5;244m{'─' * 50}\x1b[39m \x1b[38;5;244mthi-416-banner\x1b[39m \x1b[38;5;244m─" +# An OSC 8 hyperlink, how a reply's or statusline's link reaches capture -e. +OSC_LINK = "\x1b]8;id=1;https://linear.app/t/issue/THI-416\x1b\\THI-416\x1b]8;;\x1b\\" PARSE_CASES: list[tuple[str, str, str, str | None]] = [ # The screenshot Tom hit: multi-word phrase + trailing `…` + ↓-arrow @@ -570,8 +574,10 @@ # ── last_line filter cases ───────────────────────────────────────────── -# Each row: (tag, content, expected_last_line). These verify the card -# snippet doesn't surface TUI chrome (auto-mode footer, title bar, etc.). +# Each row: (tag, content, expected_last_line). last_line is what a done +# banner falls back to when the transcript can't say (THI-416), so it must be +# the reply: no TUI chrome (footer, title bar) and none of the turn's own (its +# `✻ … for 31s` line, `⎿` results and tips, right-aligned hints). LAST_LINE_CASES: list[tuple[str, str, str]] = [ # Auto-mode footer at the bottom — must be filtered. Falls through to @@ -586,7 +592,7 @@ {STATUS_LINE} ⏵⏵ auto mode on (shift+tab to cycle) """), - "✻ Brewed for 31s", + "Some recent response text from the assistant.", ), # Auto-mode footer with the "← for agents" suffix — same filter. ( @@ -598,7 +604,7 @@ {STATUS_LINE} ⏵⏵ auto mode on (shift+tab to cycle) · ← for agents """), - "✻ Cooked for 1m 15s", + "Some recent response text.", ), # Title bar at top + auto-mode at bottom — both filtered. ( @@ -611,7 +617,7 @@ {STATUS_LINE} ⏵⏵ auto mode on (shift+tab to cycle) """), - "✻ Brewed for 5s", + "Assistant said something useful here.", ), # Active spinner line shouldn't surface as snippet (duplicate of state # label). Falls through to whatever's below the spinner. @@ -627,6 +633,45 @@ """), "◻ Task 2.4: another subtask", ), + # THI-416, shapes from live 2.1.x panes. A named session's composer: + # Claude prints the name in the top rule, which used to hide the composer + # and hand the footer (statusline, ⧉ row) to last_line. The reply's + # linkified ticket is an OSC 8 hyperlink in the -e capture. + ( + "named-session-footer", + "⏺ I fixed the banner.\n" + f" It now reads the reply for {OSC_LINK}, not the footer.\n\n" + "✻ Brewed for 37s · done 2:19 PM\n\n" + f"{NAMED_RULE}\n\x1b[39m❯\xa0\x1b[2mmerge it\x1b[0m\n{RULE}\n" + f"{CUSTOM_STATUS_LINE} {OSC_LINK}\n" + " ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents\n" + " ⧉ dashboard-latest · client-health-preview\n", + "It now reads the reply for THI-416, not the footer.", + ), + # A tool result or tip under the reply, and the turn's waiting line. + ( + "tool-result-and-tip", + textwrap.dedent(f"""\ + ⏺ Pushed the branch. + ⎿ Allowed by auto mode classifier + ⎿ Tip: You can control how big a workflow is just by prompting. + ✻ Waiting for 1 background agent to finish + {RULE} + ❯ + {RULE} + {CUSTOM_STATUS_LINE} + """), + "Pushed the branch.", + ), + # The right-aligned hint Claude draws just above the composer. + ( + "clear-hint-above-composer", + "⏺ Which option do you want?\n\n" + "✻ Cogitated for 5m 33s · done 3:22 PM\n" + f"{' ' * 62}new task? /clear to save 433.4k tokens\n" + f"{RULE}\n❯\n{RULE}\n{CUSTOM_STATUS_LINE}\n", + "Which option do you want?", + ), ] @@ -1615,6 +1660,26 @@ def test_a_claude_with_no_status_line_parses_as_claude(): assert parsed["context_pct"] is None +def _named(screen: str) -> str: + # The capture's top rule as a named session draws it: the name at its + # right end (live 2.1.x pane, escapes kept). + lines = screen.split("\n") + assert lines[5].startswith("\x1b[38;2;136;136;136m─") + grey = "\x1b[38;2;136;136;136m" + lines[5] = f"{grey}{'─' * 100}\x1b[39m {grey}thi-416-banner\x1b[39m {grey}─\x1b[39m" + return "\n".join(lines) + + +def test_a_named_sessions_composer_is_still_the_composer(): + # THI-416: the name hid the composer, so with no session file to say + # otherwise the pane read as a shell, its footer as the reply, and a reply + # got a 409. The draft check still applies under a named rule. + screen = _named(_composer_capture("idle.txt")) + assert parse_pane(screen)["agent"] == "claude" + assert composer_ready(screen) + assert not composer_ready(_named(_with_composer_row("\x1b[39m❯\xa0half a thought"))) + + def test_a_footer_of_a_few_rows_still_reads_as_ready(): # Live 2.1.280 panes show 2-4 footer rows: mode hint, warnings, a status line. extra = "\n 17% | ↑30k ↓74 | $0.50 | Opus 4.7 (1M context)\n ⎿ 2 background tasks\n" From ba9e33840dc2e088b2c8b19b202d710600739bcb Mon Sep 17 00:00:00 2001 From: Thibault Dody Date: Thu, 29 Oct 2026 18:52:07 -0400 Subject: [PATCH 4/6] docs: the done banner's last_reply (channels, notifications spec) and the named composer rule (invariant 2) (THI-416) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/channels.md | 12 ++++++++++++ docs/invariants.md | 5 ++++- .../specs/2026-10-21-native-notifications-design.md | 2 +- 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/docs/channels.md b/docs/channels.md index 335b7a87..98c1cea5 100644 --- a/docs/channels.md +++ b/docs/channels.md @@ -174,6 +174,18 @@ Resolution falls back to newest-mtime-in-cwd when a pane has no recorded session yet. The earlier `channel_shim.py` recorder was removed — the hook's payload is strictly better (current vs spawn-frozen). +The same mapping gives a done banner its words (THI-416). For a `done` Claude +pane, `window_view` publishes `last_reply`: `turns.last_reply_for_session` +reads the session's transcript backward from the end to the newest assistant +text, skipping meta and sidechain events and Claude's bookkeeping rows +(`turn_duration`, `artifact-autoreact-ledger`, `mode`, …). It returns None once +a prompt follows that text (the new turn has said nothing yet), and flattens the +markdown so a link's URL doesn't fill the banner. It's cached per session on the +file's size and mtime, so a done pane costs a `stat` per poll. There is no +newest-mtime-in-cwd fallback here: a wrong transcript would put another pane's +words in the banner, so without the session the banner falls back to +`last_line`, the screen's guess. + ## Permission requests (a banner's Allow / Deny) **`pane_permission_hook.py`** is `pane_session_hook.py`'s sibling: diff --git a/docs/invariants.md b/docs/invariants.md index 51742025..7df2d535 100644 --- a/docs/invariants.md +++ b/docs/invariants.md @@ -17,7 +17,10 @@ These are the non-obvious behaviors worth preserving: `Resume this session with:` row under the composer means exited. The status line alone isn't enough: it's the user's statusLine script's output, and `STATUS_RE` knew one script's, so every other pane read as a shell - (THI-434). See `parse_pane`, `_detect_composer`. + (THI-434). A named session's top rule carries the name at its right end + (`──── thi-416-banner ─`) and still counts; missing it made the footer's + statusline the done banner's text (THI-416). See `parse_pane`, + `_detect_composer`, `_composer_rows`. 3. **WebSocket paint is self-healing, not perfect.** The initial blob still mirrors tmux's size/cursor/alt-screen state (all from diff --git a/docs/superpowers/specs/2026-10-21-native-notifications-design.md b/docs/superpowers/specs/2026-10-21-native-notifications-design.md index 1c410e08..dbc404f1 100644 --- a/docs/superpowers/specs/2026-10-21-native-notifications-design.md +++ b/docs/superpowers/specs/2026-10-21-native-notifications-design.md @@ -174,7 +174,7 @@ Titles stay under ~40 characters (macOS truncates). |---|---|---|---|---|---| | `needs_input` | a pane is in needs-input. With a `pending_permission`: the permission variant. An AskUserQuestion or other dialog: the dialog variant | 2a: `pid@needs_since`. 2b: the permission's `request_id` when present | `{pane} needs you` / `{session} · {branch}` / `Allow {tool}: {summary}?`; dialog variant: the `waitLabel` text | permission: **Allow** · Deny · Open. Dialog: Open | reveal pane | | `question` | an unacked, undismissed `need_human` alert | alert id | `{pane} asks` / `{session} · {branch}` / the message | text reply **Send** | reveal pane | -| `done` | a pane enters READY (`state: "done"`, the rail's READY rule) | `pid@completed_at` | `{pane} is done` / `{session} · PR #{n}` when a PR is linked, else branch / `w.recap` or `w.last_line`, first sentence | **Review diff** · Mark seen | reveal pane | +| `done` | a pane enters READY (`state: "done"`, the rail's READY rule) | `pid@completed_at` | `{pane} is done` / `{session} · PR #{n}` when a PR is linked, else branch / `w.last_reply` (the transcript's newest assistant text, THI-416), else `w.recap` or `w.last_line`, first sentence | **Review diff** · Mark seen | reveal pane | | `ci_failed` | a linked open PR's `w.ci` changes to `✗` from anything else | `repo#pr` + the transition (fires again after a re-run fails again) | `CI failed on #{pr}` / `{session} · {pane}` / `✗ a · ✗ b · ✓ c` from `w.ci_checks` | **Ask agent to fix** · Open PR | open PR | | `pr_merged` | a PR goes from `open` to `merged`, the `nextMergeWatch` rule, keyed per PR, not per pane | `repo#pr` | `PR #{pr} merged` / `{session} · {branch} → {pr_base}` / `The worktree and its session can be cleaned up.` | **Clean up worktree** · Open PR | open PR | | `usage` | the worst meter's mood (`codingMood`) gets worse | `account/meter/resets_at/mood` | `Account {A} is running hot` / `{Meter} usage {pct}% · →{proj}% at reset` / `{n} sessions are working on {A}.` | **Spawn on {B}** · Details | open the usage pill | From 8644c40b863fbd15a82758124cb31ee499ada358 Mon Sep 17 00:00:00 2001 From: Thibault Dody Date: Fri, 30 Oct 2026 18:40:16 -0400 Subject: [PATCH 5/6] panes: last_line is the start of the newest reply on screen, its first paragraph joined, not its last row; empty after a tool call (THI-416) Co-Authored-By: Claude Opus 5.5 (1M context) --- periscope/panes.py | 43 +++++++++++++++++++++++--------- tests/test_panes.py | 60 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 90 insertions(+), 13 deletions(-) diff --git a/periscope/panes.py b/periscope/panes.py index acf3dd4f..dd4adb27 100644 --- a/periscope/panes.py +++ b/periscope/panes.py @@ -942,19 +942,37 @@ def _is_turn_chrome(line: str) -> bool: return len(line) - len(line.lstrip()) >= _RIGHT_ALIGNED_INDENT -def _last_meaningful_line(lines: list[str]) -> str: - """The closest line of the reply from the bottom, skipping TUI chrome and - the turn's own (_is_turn_chrome): what a done banner says when the - transcript can't (THI-416). "" if nothing real. +# A collapsed tool group (`⏺ Read 3 files (ctrl+o to expand)`) or an MCP call +# (`⏺ periscope - notify (MCP)(kind: …)`): tool headers TOOL_CALL_RE misses. +_TOOL_HEADER_MARKS = ("(ctrl+o to expand)", "(MCP)(") + + +def _last_meaningful_line(rows: list[str]) -> str: + """What a done banner says when the transcript can't (THI-416): the newest + reply on screen from its start, its first paragraph's wrapped rows joined. + Its last row was the banner once, half a sentence ("that use it would + fail."). "" when the newest ⏺ is a tool call: the turn ended without words. + With no ⏺ on screen, the closest line from the bottom that isn't chrome or + the turn's own (_is_turn_chrome). `rows` keeps blank rows: they end the + paragraph. """ - for line in reversed(lines): - s = line.strip() - if not s: - continue - if _is_chrome_line(line) or _is_turn_chrome(line): + for i in range(len(rows) - 1, -1, -1): + s = rows[i].strip() + if not s.startswith("⏺"): continue - # A reply's first row opens with Claude's ⏺; the banner wants the words. - return s.removeprefix("⏺").lstrip()[:200] + if TOOL_CALL_RE.match(s) or any(mark in s for mark in _TOOL_HEADER_MARKS): + return "" + # Claude wraps a reply itself, each continuation row indented under ⏺. + para = [s.removeprefix("⏺").strip()] + for row in rows[i + 1 :]: + if not row.startswith(" ") or not row.strip() or _is_chrome_line(row) or _is_turn_chrome(row): + break + para.append(row.strip()) + return " ".join(para)[:300] + for row in reversed(rows): + s = row.strip() + if s and not _is_chrome_line(row) and not _is_turn_chrome(row): + return s[:200] return "" @@ -1086,7 +1104,8 @@ def parse_pane(content: str) -> dict: pending_input = None if needs_input else _detect_pending_input(raw_rows, plain_rows) recap = _extract_recap(lines) - last_line = _last_meaningful_line(body) + # The same cut over every row: blank rows end the reply's first paragraph. + last_line = _last_meaningful_line(plain_rows[: _last_rule_index(plain_rows) + 1] if composer else plain_rows) asked_question = _detect_asked_question(body) if agent == "claude" and not needs_input else False api_error = _detect_api_error(body) if agent == "claude" else False diff --git a/tests/test_panes.py b/tests/test_panes.py index 6ac76368..909c778a 100644 --- a/tests/test_panes.py +++ b/tests/test_panes.py @@ -646,7 +646,65 @@ f"{CUSTOM_STATUS_LINE} {OSC_LINK}\n" " ⏵⏵ auto mode on (shift+tab to cycle) · ← 2 agents\n" " ⧉ dashboard-latest · client-health-preview\n", - "It now reads the reply for THI-416, not the footer.", + "I fixed the banner. It now reads the reply for THI-416, not the footer.", + ), + # The start of the reply, not its last row: a banner read "that use it + # would fail.", the tail of a wrapped sentence. The first paragraph, its + # wrapped rows joined, holds the first sentence. + ( + "reply-start-not-its-last-row", + textwrap.dedent(f"""\ + ⏺ Bash(gh pr create --draft) + ⎿ https://github.com/o/r/pull/12 + + ⏺ I opened draft PR #12 with the keep-open label. CI is running in the + background; this is the first time the tests run anywhere. + + - What it does: the banner now reads the reply. + - Not yet verified: a real macOS banner. + + I'll report the CI result when the checks finish. Otherwise the scopes + that use it would fail. + + ✻ Brewed for 2m 3s · done 12:28 PM + {RULE} + ❯ + {RULE} + {CUSTOM_STATUS_LINE} + """), + "I opened draft PR #12 with the keep-open label. CI is running in the background; " + "this is the first time the tests run anywhere.", + ), + # A turn that ended in a tool call said nothing after it: no words beat a + # tool header or its output (the banner then says "Finished."). + ( + "turn-ended-in-a-tool-call", + textwrap.dedent(f"""\ + ⏺ Pushing now. + + ⏺ Bash(git push origin main) + ⎿ To github.com:o/r.git + a1b2c3d..e4f5a6b main -> main + + ✻ Brewed for 9s · done 12:28 PM + {RULE} + ❯ + {RULE} + """), + "", + ), + ( + "turn-ended-in-an-mcp-call", + textwrap.dedent(f"""\ + ⏺ Done. + + ⏺ periscope - notify (MCP)(kind: "done", message: "Pushed") + ⎿ {{"ok": true}} + {RULE} + ❯ + {RULE} + """), + "", ), # A tool result or tip under the reply, and the turn's waiting line. ( From 215dea2ce21273a30530a8fb6c8304f36d4b1421 Mon Sep 17 00:00:00 2001 From: Thibault Dody Date: Fri, 30 Oct 2026 19:33:17 -0400 Subject: [PATCH 6/6] build: static/dist with the done banner's last_reply (THI-416) Co-Authored-By: Claude Opus 5.5 (1M context) --- static/dist/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/dist/app.js b/static/dist/app.js index 6c9dc55c..353fa37b 100644 --- a/static/dist/app.js +++ b/static/dist/app.js @@ -29,7 +29,7 @@ ${n}`:w?"updating — periscope will restart itself":$?`${g}, but ${v}`:`${g} `).map((n,i)=>r(V,{children:[i>0?r("br",{}):null,n]}))}function N2({req:e}){const t=ne(null),n=ne(null),i=e.type==="confirm"&&e.danger,l=e.type==="confirm"?!1:e.type==="prompt"?null:void 0;He(()=>Ln(e.id,l)),j(()=>{var f,d;const c=t.current;if(c&&!c.open&&c.showModal(),e.type==="prompt"){const p=n.current;p&&(p.focus(),p.select())}else i?(f=c==null?void 0:c.querySelector(".confirm-dialog-cancel"))==null||f.focus():(d=c==null?void 0:c.querySelector(".confirm-dialog-ok"))==null||d.focus()},[]);const a=()=>{var c;e.type==="prompt"?Ln(e.id,((c=n.current)==null?void 0:c.value)??""):e.type==="alert"?Ln(e.id,void 0):Ln(e.id,!0)},s=()=>Ln(e.id,l),o=c=>{c.target===t.current&&s()},u=e.type==="prompt"?"confirm-dialog prompt-dialog":"confirm-dialog";return r("dialog",{ref:t,class:u,onClick:o,children:[r("p",{class:"confirm-dialog-msg",children:F2(e.message)}),e.type==="prompt"&&r("input",{ref:n,type:"text",class:"prompt-dialog-input",autocomplete:"off",spellcheck:!1,value:e.defaultValue,placeholder:e.placeholder||void 0,onKeyDown:c=>{c.key==="Enter"&&(c.preventDefault(),a())}}),r("div",{class:"confirm-dialog-actions",children:[e.type!=="alert"&&r("button",{type:"button",class:"confirm-dialog-cancel",onClick:s,children:e.cancelLabel}),r("button",{type:"button",class:`confirm-dialog-ok${i?" is-danger":""}`,onClick:a,children:e.okLabel})]})]})}function B2(){const e=hn.value;return r(V,{children:e.map(t=>r(N2,{req:t},t.id))})}async function pr(e,t,n={}){let i;try{i=await fetch(t,n)}catch(a){return{error:`${e} failed: ${a.message}`,reason:a.message}}let l={};try{l=await i.json()}catch{}if(!i.ok){const a=l.detail||`HTTP ${i.status}`;return{error:l.detail||`${e} failed: ${a}`,reason:a,status:i.status}}return{data:l}}const rn=H(!1),Nt=H(null);function z2(e){return Array.isArray(e)?e.filter(Boolean):null}function j2(e,t){return e===t?!0:!e||!t?!1:e.length===t.length&&e.every((n,i)=>n===t[i])}function _d(e){const{paths:t}=e||{},n=z2(t);j2(n,Nt.value)||(Nt.value=n),rn.value=!0,ye("overlay.open",{which:"cleanup",scoped:!!Nt.value})}function H2(e){return e!=null&&e.length?`/api/cleanup/candidates?${e.map(t=>`paths=${encodeURIComponent(t)}`).join("&")}`:"/api/cleanup/candidates"}function In(){rn.value=!1,Nt.value=null}function q2(){He(In,rn.value);const[e,t]=B(""),[n,i]=B(!1),[l,a]=B([]),[s,o]=B({}),[u,c]=B(!1),[f,d]=B(!1),p=ne(0);j(()=>{const x=document.getElementById("cleanup-btn"),w=()=>_d();return x&&x.addEventListener("click",w),()=>{x&&x.removeEventListener("click",w)}},[]);async function h(){const x=++p.current;t(""),i(!0),a([]),o({});const w=Nt.peek();if(w&&w.length===0){i(!1);return}const{data:C,error:y}=await pr("load candidates",H2(w));if(x!==p.current)return;if(i(!1),y){t(y),a([]);return}const g=C.candidates||[];a(g);const $={};g.forEach((v,E)=>{$[E]=!v.dirty}),o($)}j(()=>{if(!rn.value){p.current++;return}c(!1),h()},[rn.value,Nt.value]);const m=Object.values(s).filter(Boolean).length;async function b(){t("");const x=[];if(l.forEach((y,g)=>{s[g]&&x.push({pinned_dir:y.pinned_dir,delete_branch:u})}),x.length===0)return;d(!0);const{data:w,error:C}=await pr("archive","/api/cleanup/archive",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({candidates:x})});if(d(!1),C){t(C);return}if(w.failed&&w.failed.length>0){const y=`Archived ${w.archived.length}, ${w.failed.length} failed: `+w.failed.map(g=>`${g.pinned_dir.split("/").pop()}: ${g.error}`).join("; ");await h(),t(g=>g||y);return}In()}return rn.value?r("div",{id:"cleanup-modal",class:"cleanup-modal-overlay",onClick:x=>{x.target.id==="cleanup-modal"&&In()},children:r("div",{class:"cleanup-modal-card",children:[r("header",{class:"cleanup-modal-head",children:[r("h2",{children:"🧹 cleanup"}),r("button",{id:"cleanup-modal-close",title:"close",onClick:In,children:"×"})]}),r("p",{class:"cleanup-modal-sub",children:"Worktrees with merged/closed PRs, deleted remote branches, or idle activity. Dirty worktrees are NOT auto-selected."}),r("div",{id:"cleanup-modal-controls",children:r("label",{class:"cleanup-delete-branches",children:[r("input",{type:"checkbox",id:"cleanup-delete-branches",checked:u,onChange:x=>c(x.currentTarget.checked)})," also delete local branches"]})}),r("div",{id:"cleanup-modal-list",children:n?r("div",{class:"cleanup-loading",children:"Walking worktrees…"}):l.length===0&&!e?r("div",{class:"cleanup-empty",children:Nt.value?"Nothing to clean up here. Cleanup only offers a worktree (never a main checkout) whose PR is merged or closed, whose remote branch is gone, or that sits idle.":"No cleanup candidates. 🎉"}):l.map((x,w)=>{const C=x.project_name||`(untracked: ${x.pinned_dir.split("/").pop()})`;return r("label",{class:x.dirty?"cleanup-row cleanup-row-dirty":"cleanup-row","data-i":w,children:[r("input",{type:"checkbox",class:"cleanup-row-check",checked:!!s[w],onChange:y=>o(g=>({...g,[w]:y.currentTarget.checked}))}),r("div",{class:"cleanup-row-body",children:[r("div",{class:"cleanup-row-title",children:C}),r("div",{class:"cleanup-row-meta",children:[x.branch," ",x.dirty?r("span",{class:"cleanup-dirty",children:"⚠ dirty"}):null]}),r("div",{class:"cleanup-row-signals",children:(x.signals||[]).map((y,g)=>r("span",{class:`cleanup-badge cleanup-badge-${y.kind}`,children:y.label},g))})]})]},x.pinned_dir)})}),r("div",{id:"cleanup-modal-error",class:"cleanup-modal-error",hidden:!e,children:e}),r("div",{class:"cleanup-modal-actions",children:[r("button",{type:"button",id:"cleanup-cancel",onClick:In,children:"cancel"}),r("button",{type:"button",id:"cleanup-submit",disabled:n||f||m===0,onClick:b,children:["archive selected (",m,")"]})]})]})}):null}const _i=H(!1);function xo(){_i.value=!0,ye("overlay.open",{which:"commands"})}function pl(){_i.value=!1}function U2({c:e,i:t,onDragState:n}){const i=ne(null);async function l(s){const o=i.current;if(!o)return;const u=o.querySelector(".commands-label").value.trim(),c=o.querySelector(".commands-exec").value;u&&await Pg(e.label,{label:u,exec:c})}async function a(){await Ag(e.label)}return r("div",{ref:i,class:"commands-row",draggable:!0,"data-label":e.label,"data-i":t,onDragStart:s=>{var o;n(e.label),(o=i.current)==null||o.classList.add("dragging"),s.dataTransfer.setData("text/plain",e.label),s.dataTransfer.effectAllowed="move"},onDragEnd:()=>n(null),onDragOver:s=>{s.preventDefault(),s.dataTransfer.dropEffect="move";const o=i.current.getBoundingClientRect(),u=s.clientY{var s;(s=i.current)==null||s.classList.remove("drag-over-top","drag-over-bottom")},onDrop:s=>{s.preventDefault();const o=i.current.getBoundingClientRect(),u=s.clientY{const l=document.getElementById("open-commands");return l&&l.addEventListener("click",xo),()=>{l&&l.removeEventListener("click",xo)}},[]);const e=ne(null),t=l=>{e.current=l};t.drop=(l,a)=>{const s=e.current;if(!s||l===s)return;const o=Hr().map(d=>d.label),u=o.indexOf(s);if(u<0)return;o.splice(u,1);const c=o.indexOf(l),f=a?c:c+1;o.splice(f,0,s),Mg(o)};async function n(){const l="command";let a=l,s=1;const o=new Set(Hr().map(u=>u.label));for(;o.has(a);)a=`${l}-${++s}`;await Eg({label:a,exec:""})}if(!_i.value)return null;const i=Hr();return r("div",{id:"commands-modal",class:"commands-modal-overlay",onClick:l=>{l.target.id==="commands-modal"&&pl()},children:r("div",{class:"commands-modal-card",children:[r("header",{class:"commands-modal-head",children:[r("h2",{children:"New-window commands"}),r("button",{id:"commands-modal-close",title:"close",onClick:pl,children:"×"})]}),r("p",{class:"commands-modal-sub",children:["First row is the primary button. Drag rows to reorder. Empty ",r("code",{children:"exec"})," = bare shell."]}),r("div",{id:"commands-modal-list",children:i.map((l,a)=>r(U2,{c:l,i:a,onDragState:t},l.label))}),r("button",{id:"commands-modal-add",class:"commands-modal-add",onClick:n,children:"+ add command"})]})})}const xd=[{id:"default",label:"default"},{id:"full",label:"full"}];function ml(e){return(e==null?void 0:e.mode)==="agent"&&(e.agent||"claude")==="claude"}function K2(e){return!e||e==="default"?null:e}function So(e){var t;return((t=xd.find(n=>n.id===e))==null?void 0:t.label)||e}const $o=["shell","idle","done","working","needs-input"];function Co(e){let t=-1;for(const n of e){const i=$o.indexOf(n);i>t&&(t=i)}return t>=0?$o[t]:"shell"}const Sd="";function Ga(e,t,n,i={},l=[]){const a=i.trackOrder||[],s=i.tabsByTrack||{},o=i.branchOrderByTrack||{},u=[],c={},f={},d={},p={};for(const v of e||[]){const E=v.track_id;if(!E)continue;const A=v.branch||Sd;c[E]||(u.push(E),c[E]=[],f[E]=[],d[E]={}),c[E].includes(v.pid)||c[E].push(v.pid),f[E].includes(A)||f[E].push(A),d[E][A]||(d[E][A]=[]),d[E][A].includes(v.pid)||d[E][A].push(v.pid),p[v.pid]=A}const h=(l||[]).filter(v=>v.id&&v.id!==v.repo).map(v=>v.id),m=new Set(h),b=new Set(u),x=a.filter(v=>b.has(v)||m.has(v)),w=new Set(x),C=[...x,...u.filter(v=>!w.has(v)),...h.filter(v=>!w.has(v)&&!b.has(v))],y={},g={},$={};for(const v of C){const E=c[v]||[],A=new Set(E),T=(s[v]||[]).filter(O=>A.has(O)),S=new Set(T);y[v]=[...T,...E.filter(O=>!S.has(O))];const R=f[v]||[],F=new Set(R),q=(o[v]||[]).filter(O=>F.has(O)),L=new Set(q),I=[...q,...R.filter(O=>!L.has(O))];if(I.length>=2){g[v]=I,$[v]={};for(const O of I)$[v][O]=[];for(const O of y[v]){const Q=p[O];$[v][Q]&&$[v][Q].push(O)}}else g[v]=[]}return{trackOrder:C,tabsByTrack:y,branchesByTrack:g,tabsByBranch:$}}function xi(e,t,n=[]){for(const l of t||[])if(l.track_id===e&&l.track_name)return l.track_name;for(const l of n||[])if(l.id===e&&l.name)return l.name;if(e==="loose")return"loose";const i=String(e||"").split("/").filter(Boolean);return i[i.length-1]||e}function V2(e,t,n=[]){if(e==="loose")return"loose";for(const i of t||[])if(i.track_id===e&&i.track_kind)return i.track_kind;for(const i of n||[])if(i.id===e)return i.repo===i.id?"repo":"goal";return"repo"}function G2(e){return String(e||"").replace(/^\/(?:Users|home)\/[^/]+/,"~")}function J2(e){const t=e.worktree_affiliation||{};if(t.kind==="sibling")return t.label||null;if(t.kind==="no-repo"||t.kind==="off-repo"){if(e.repo_key&&e.repo_label)return e.branch?`${e.repo_label}/${e.branch}`:e.repo_label;if(e.cwd)return G2(e.cwd)}return null}function Q2(e,t,n,i){const l={};for(const a of i||[])if(a.walkthrough){for(const s of e)if((t[s]||[]).map(u=>n[u]).find(u=>u&&u.repo_label===a.repo&&(u.branch||Sd)===a.branch)){l[s]||(l[s]={}),l[s][a.branch]||(l[s][a.branch]=[]),l[s][a.branch].push(a);break}}return l}const Ut=H(null),Ue=H(null),Xe=H(null),Tt=H(""),Qr=H("default"),Yr=H("default"),Zr=H("default"),Xr=H("default");function Y2(e){return(e==null?void 0:e.mode)==="agent"&&(e.agent||"claude")==="claude"}const Si=H(null);function $d(e,t){const n=new Map;for(const i of t||[]){if(i.track_id!==e)continue;const l=i.branch;l&&(n.has(l)||n.set(l,i.cwd||""))}return[...n.entries()].map(([i,l])=>({branch:i,cwd:l}))}function Z2(e,t,n,i){const l=[],a=new Set;for(const o of $d(e,t))a.add(o.branch),l.push({...o,live:!0});if(!n||!i)return l;for(const o of i.worktrees||[])o.repo!==n||!o.branch||a.has(o.branch)||(a.add(o.branch),l.push({branch:o.branch,cwd:o.path,live:!1}));const s=(i.repos||[]).find(o=>o.repo===n);for(const o of(s==null?void 0:s.branches)||[])!o||a.has(o)||(a.add(o),l.push({branch:o,cwd:"",live:!1}));return l}function X2(e,t,n=5){const i=[],l=new Set,a=s=>{!s||l.has(s.branch)||(l.add(s.branch),i.push(s))};a(e.find(s=>s.branch===t));for(const s of e)s.live&&a(s);for(const s of e){if(i.length>=n)break;a(s)}return i}function eb(e,t){const n=String(t||"").toLowerCase().split(/[\s/]+/).filter(Boolean);return n.length?e.filter(i=>{const l=i.branch.toLowerCase();return n.every(a=>l.includes(a))}):[]}function tb(e){const t=[{id:"claude",label:"Claude",mode:"agent",agent:"claude"},{id:"codex",label:"Codex",mode:"agent",agent:"codex"}],n=new Set(["claude","codex"]),i=(e||[]).filter(l=>!n.has((l.label||"").trim().toLowerCase())).map(l=>({id:`cmd:${l.label}`,label:l.label,mode:"shell",exec:l.exec||""}));return[...t,...i]}const nb=Vt(()=>Z2(Ut.value,de.value,Ja(Ut.value),Si.value));function Ja(e){var t;return((t=(Bt.value||[]).find(n=>n.id===e))==null?void 0:t.repo)||null}function rb(e){var n,i;const t=Ja(e);return t&&((i=(((n=Si.value)==null?void 0:n.repos)||[]).find(l=>l.repo===t))==null?void 0:i.default_branch)||null}function To(e){var n,i,l;Ut.value=e;const t=$d(e,de.value);Ue.value=t.length?t[0].branch:null,Xe.value=null,Tt.value="",Qr.value=((n=sn.value)==null?void 0:n.account)||"default",Zr.value=((i=sn.value)==null?void 0:i.model)||"default",Xr.value=((l=sn.value)==null?void 0:l.effort)||"default",Yr.value=Ng(),ye("overlay.open",{which:"launcher"}),Si.value=null,ie("open catalog","/api/open/catalog").then(a=>{Ut.value===e&&(Si.value=a)})}function Mr(){Ut.value=null,Ue.value=null,Xe.value=null,Tt.value=""}function Eo(e){Ue.value=e,Xe.value=null}function ib(){Ue.value=null,Xe.value=""}function lb(){He(Mr,Ut.value!=null),j(()=>(window.__periscopeOpenLauncher=To,()=>{window.__periscopeOpenLauncher===To&&delete window.__periscopeOpenLauncher}),[]);const e=Ut.value;if(e==null)return null;const t=Hr(),n=nb.value,i=Ja(e),l=n.length>0||Xe.value!=null||!!i;async function a(p){const h=new URLSearchParams({session:e,agent:p.agent||"claude",mode:p.mode});p.mode==="shell"&&p.exec&&h.set("exec",p.exec),Y2(p)&&h.set("account",Qr.value);const m=ml(p)?K2(Yr.value):null;m&&h.set("profile",m),ml(p)&&h.set("model",Zr.value),ml(p)&&h.set("effort",Xr.value);const b=Xe.value;if(b!=null&&b.trim())h.set("branch",b.trim());else if(Ue.value!=null){const x=n.find(w=>w.branch===Ue.value);x!=null&&x.cwd?h.set("cwd",x.cwd):x&&h.set("branch",x.branch)}await ie("new window",`/api/window/new?${h.toString()}`,{method:"POST"}),Mr()}const s=tb(t),o=s[0],u=Tt.value.trim(),c=u?eb(n,u).slice(0,24):X2(n,rb(e)),f=Ue.value!=null&&!c.some(p=>p.branch===Ue.value)?n.find(p=>p.branch===Ue.value):null,d=f?[f,...c]:c;return r("div",{id:"launcher-modal",class:"launcher-modal-overlay",onClick:p=>{p.target.id==="launcher-modal"&&Mr()},children:r("div",{class:"launcher-modal-card",children:[r("header",{class:"launcher-modal-head",children:[r("h2",{children:"+ New tab"}),r("button",{id:"launcher-close",title:"close",onClick:Mr,children:"×"})]}),r("p",{class:"launcher-modal-sub",id:"launcher-session-name",children:["Add to track: ",xi(e,de.value,Bt.value)]}),l&&r("div",{class:"launcher-section",children:[r("div",{class:"launcher-section-head",children:[r("div",{class:"launcher-section-label",children:"Branch"}),r("input",{class:"launcher-branch-search",type:"text",placeholder:n.length>1?`search ${n.length} branches…`:"search branches…",value:Tt.value,onInput:p=>{Tt.value=p.target.value},onKeyDown:p=>{if(p.key==="Escape"&&Tt.value)p.stopPropagation(),Tt.value="";else if(p.key==="Enter"){const h=d[0];h&&Eo(h.branch)}}})]}),r("div",{class:"launcher-branches",children:[d.map(p=>r("button",{class:`launcher-branch${Ue.value===p.branch?" is-active":""}${p.live?"":" is-dormant"}`,title:p.live?"running now":p.cwd||"no worktree yet",onClick:()=>Eo(p.branch),children:["⎇ ",p.branch]},p.branch)),Xe.value==null?r("button",{class:"launcher-branch launcher-branch-new",onClick:ib,children:"+ new branch…"}):r("input",{class:"launcher-branch-input is-active",type:"text",placeholder:"new-branch-name",ref:p=>p==null?void 0:p.focus(),value:Xe.value,onInput:p=>{Xe.value=p.target.value},onKeyDown:p=>{p.key==="Enter"&&o&&a(o)}})]}),u&&d.length===0&&r("div",{class:"launcher-empty",children:["No branch matches “",u,"”."]})]}),Sn()&&r("div",{class:"launcher-section",children:[r("div",{class:"launcher-section-label",children:"Account"}),r("div",{class:"launcher-branches",children:at.value.map(p=>r("button",{class:`launcher-branch${Qr.value===p.id?" is-active":""}`,onClick:()=>{Qr.value=p.id},children:p.label},p.id))})]}),r("div",{class:"launcher-section",children:[r("div",{class:"launcher-section-label",children:"Profile"}),r("div",{class:"launcher-branches",children:xd.map(p=>r("button",{class:`launcher-branch${Yr.value===p.id?" is-active":""}`,title:p.id==="full"?"the pipeline: superpowers, tom-workflow, codebase-health-check, plus the pipeline direction prompt":"tom-core alone, on the plugin set settings.json enables",onClick:()=>{Yr.value=p.id,Bg(p.id)},children:p.label},p.id))})]}),r("div",{class:"launcher-section",children:[r("div",{class:"launcher-section-label",children:"Model"}),r("div",{class:"launcher-branches",children:sr.map(p=>r("button",{class:`launcher-branch${Zr.value===p.id?" is-active":""}`,title:p.id==="default"?"no override — whatever the account's settings.json picks (this launch only; the header pin is the standing default)":`spawn this launch on ${p.label} (ANTHROPIC_MODEL on the pane — a hand re-run claude there keeps it)`,onClick:()=>{Zr.value=p.id},children:p.label},p.id))})]}),r("div",{class:"launcher-section",children:[r("div",{class:"launcher-section-label",children:"Effort"}),r("div",{class:"launcher-branches",children:Oa.map(p=>r("button",{class:`launcher-branch${Xr.value===p?" is-active":""}`,title:p==="default"?"no override — Claude's own choice for the model; /effort works in the pane (this launch only; the header pin is the standing default)":`spawn this launch at ${p} (--effort on the pane's claude; /effort can still change it later)`,onClick:()=>{Xr.value=p},children:p},p))})]}),r("div",{class:"launcher-section",children:[r("div",{class:"launcher-section-label",children:"Run"}),r("div",{id:"launcher-list",children:s.map((p,h)=>r("button",{class:`launcher-row${h===0?" is-default":""}`,"data-label":p.label,onClick:()=>a(p),children:[r("span",{class:"launcher-row-label",children:p.label}),h===0&&r("span",{class:"launcher-row-hint",children:"⏎"})]},p.id))})]})]})})}const ab=/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/,sb=/^#?(\d+)$/;function ob(e){const t=e.match(ab);if(t)return{repo:t[1],pr:Number(t[2])};const n=e.trim().match(sb);return n?{repo:null,pr:Number(n[1])}:null}const cb=/^(~|\.{0,2}\/)/;function ub(e){const t=(e||"").trim();return!t||!cb.test(t)?null:t.replace(/\/+$/,"")||"/"}function Yt(e,t){return e.toLowerCase().includes(t.toLowerCase())}function db(e,t){var s;const n=(e||"").trim(),i=[];if(!n)return i;"reviews".startsWith(n.toLowerCase())&&n.length>=2&&i.push({kind:"reviews",label:"Reviews · open branches and walkthroughs",sub:"the review queue",descriptor:null});const l=ub(n);l&&!(t.worktrees||[]).some(o=>o.path===l)&&i.push({kind:"open",label:`open path: ${l}`,sub:"register + open",descriptor:{path:l}});for(const o of t.repos||[])(Yt(o.label,n)||Yt(o.repo,n))&&i.push({kind:"worktree",label:`${o.label} · new worktree…`,sub:`off origin/${o.default_branch||"HEAD"}`,repo:o.repo,branches:o.branches,descriptor:null});for(const o of t.repos||[])(Yt(o.label,n)||Yt(o.repo,n))&&i.push({kind:"track",label:`${o.label} · new track…`,sub:"goal-scoped group",repo:o.repo,descriptor:null});for(const o of t.worktrees||[])if(Yt(o.path,n)||Yt(o.branch||"",n)){const u=((s=t.repos.find(c=>c.repo===o.repo))==null?void 0:s.label)||o.repo;i.push({kind:"open",label:`${u} · ${o.branch||"detached"}`,sub:o.path,descriptor:{path:o.path}})}const a=ob(n);return a&&i.push({kind:"pr",label:a.repo?`review PR #${a.pr} in ${a.repo}`:`review PR #${a.pr}`,sub:a.repo||"choose a repo",needsRepo:!a.repo,pr:a}),i.push({kind:"command",label:`⚡ run: ${n}`,text:n}),i}const dt=H(!1);function Po(){dt.value=!0,ye("overlay.open",{which:"open"})}function St(){dt.value=!1}const Ao={open:{group:"Open",icon:"▸"},worktree:{group:"New worktree",icon:"✦"},pr:{group:"Review PR",icon:"◆"},branch:{group:"Branches",icon:"⎇"},newbranch:{group:"Branches",icon:"+"},repo:{group:"Repositories",icon:"◳"},track:{group:"New track",icon:"▧"},command:{group:"Command",icon:"⚡"},reviews:{group:"Reviews",icon:"◉"}},hb=3e3;function fb(e){const[t,n]=B([]);return j(()=>{if(!e)return;let i=!0,l=null;const a=async()=>{const s=await ie("jobs","/api/command/jobs");i&&(Array.isArray(s)&&n(s),l=setTimeout(a,hb))};return a(),()=>{i=!1,l&&clearTimeout(l)}},[e]),t}function pb(e){const[t,n]=B(null);return j(()=>{if(!e){n(null);return}let i=!0;return n(null),(async()=>{const l=await ie("job-turns",`/api/command/jobs/${encodeURIComponent(e)}/turns`);i&&n((l==null?void 0:l.messages)||[])})(),()=>{i=!1}},[e]),{messages:t,pending:t===null}}function mb(e){if(e.role==="system")return"— context compacted —";const t=e.role==="user"?"❯":"⏺",n=e.tool_uses||[];if(n.length){const l=n[0],a=l.input||{},s=a.command||a.file_path||a.path||a.pattern||a.description||a.query||a.url||"",o=n.length>1?` +${n.length-1}`:"";return`${t} ${l.name||"tool"}${s?`(${s})`:""}${o}`}const i=String(e.text||"").split(` `).find(l=>l.trim())||"";return`${t} ${i}`}function gb(){He(St,dt.value);const[e,t]=B({repos:[],worktrees:[]}),[n,i]=B(""),[l,a]=B(null),[s,o]=B(!1),[u,c]=B(null),[f,d]=B(!1),[p,h]=B(""),[m,b]=B("claude");if(j(()=>{window.__periscopeOpenOmnibox=Po;function $(v){(v.metaKey||v.ctrlKey)&&(v.key==="k"||v.key==="p")&&(v.preventDefault(),dt.value=!dt.value)}return document.addEventListener("keydown",$),()=>{document.removeEventListener("keydown",$),window.__periscopeOpenOmnibox===Po&&delete window.__periscopeOpenOmnibox}},[]),j(()=>{dt.value&&(i(""),a(null),c(null),h(""),b("claude"),(async()=>{const $=await ie("open catalog","/api/open/catalog");$&&t($)})())},[dt.value]),!dt.value)return null;async function x($){if(f)return;d(!0),h("");const v=await ie("open","/api/open",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({...$,agent:m})});if(d(!1),!v){h("open failed");return}Lg(v.ui),v.agent_pid&&(ke.value=`pane:${v.agent_pid}`,gt({kind:"pane",pid:v.agent_pid})),St()}async function w($,v){if(!v)return;d(!0),h("");const E=await ie("create track","/api/tracks",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:$||null,name:v})});if(d(!1),!E){h("create track failed");return}St()}async function C($){d(!0),h("");const v=await ie("command","/api/command",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text:$})});if(d(!1),!v){h("command failed");return}c(null),o(!0)}function y($){if($.kind==="reviews"){Ia(),St();return}if($.kind==="open")return x($.descriptor);if($.kind==="command")return C($.text);if($.kind==="pr"&&!$.needsRepo)return x({repo:$.pr.repo,pr:$.pr.pr});a({card:$})}const g=db(n,e).map($=>({...$,group:Ao[$.kind].group,icon:Ao[$.kind].icon,key:$.label}));return r("div",{id:"open-omnibox",class:"open-omnibox-overlay",onClick:$=>{$.target.id==="open-omnibox"&&St()},children:r("div",{class:"open-omnibox-card",children:[!s&&r("div",{class:"launcher-branches",children:["claude","codex"].map($=>r("button",{class:`launcher-branch${m===$?" is-active":""}`,onClick:()=>b($),children:$==="claude"?"Claude":"Codex"},$))}),!l&&!s&&r(Qi,{value:n,onInput:i,placeholder:"repo, path, #PR…",items:g,onPick:y,onClose:St,empty:n?"no matches":e.roots&&!e.repos.length?`no repos found under ${e.roots.join(", ")} — add roots in ⚙ settings, or type a path`:"type a repo, path, or #PR…"}),s&&r(yb,{selectedJob:u,onSelect:c,onBack:u?()=>c(null):()=>{o(!1),i("")},onClose:St}),l&&l.card.kind==="worktree"&&r(kb,{card:l.card,onPick:$=>x({repo:l.card.repo,branch:$}),onBack:()=>a(null)}),l&&l.card.kind==="track"&&r(wb,{card:l.card,onPick:$=>w(l.card.repo,$),onBack:()=>a(null)}),l&&l.card.kind==="pr"&&r(vb,{repos:e.repos,onPick:$=>x({repo:$,pr:l.card.pr.pr}),onBack:()=>a(null)}),p&&r("div",{class:"open-omnibox-error",children:p}),f&&r("div",{class:"open-omnibox-busy",children:"opening…"})]})})}function yb({selectedJob:e,onSelect:t,onBack:n}){const i=fb(!e);return He(n,!0),e?r(bb,{jobId:e,onBack:n}):r("div",{class:"open-omnibox-jobs",children:[r("div",{class:"open-omnibox-jobs-head",children:"Commands"}),r("div",{class:"open-omnibox-list",children:[i.length===0&&r("div",{class:"open-omnibox-empty",children:"no commands yet"}),i.map(l=>r("button",{class:"open-omnibox-row",onClick:()=>t(l.id),children:[r("span",{class:`open-omnibox-jobdot${l.status==="running"?" is-running":""}`,children:l.status==="running"?"●":"✓"}),r("span",{class:"open-omnibox-label",children:l.text}),r("span",{class:"open-omnibox-sub",children:Pe(l.started_at)})]},l.id))]}),r("div",{class:"open-omnibox-footer",children:r("span",{children:[r("kbd",{children:"esc"})," back"]})})]})}function bb({jobId:e,onBack:t}){const{messages:n,pending:i}=pb(e);return r("div",{class:"open-omnibox-console",children:[r("div",{class:"open-omnibox-console-cmd",children:r("button",{class:"open-omnibox-jobback",onClick:t,children:"← back"})}),r("div",{class:"open-omnibox-console-log",children:[i&&r("div",{class:"open-omnibox-console-spin",children:"no transcript yet — the commander is still starting…"}),!i&&n.length===0&&r("div",{class:"open-omnibox-console-spin",children:"no transcript yet"}),!i&&n.map((l,a)=>r("div",{class:"open-omnibox-console-line",children:mb(l)},l.uuid||a))]})]})}function Qi({value:e,onInput:t,placeholder:n,items:i,onPick:l,onBack:a,empty:s}){const[o,u]=B(0),c=ne(null),f=ne(null),d=i.length,p=Math.min(o,Math.max(0,d-1));j(()=>{const b=requestAnimationFrame(()=>{var x;return(x=f.current)==null?void 0:x.focus()});return()=>cancelAnimationFrame(b)},[]),j(()=>{u(0)},[e]),j(()=>{var b,x;(x=(b=c.current)==null?void 0:b.querySelector(".is-active"))==null||x.scrollIntoView({block:"nearest"})},[p,d]);function h(b){b.key==="ArrowDown"?(b.preventDefault(),u(Math.min(p+1,d-1))):b.key==="ArrowUp"?(b.preventDefault(),u(Math.max(p-1,0))):b.key==="Enter"?(b.preventDefault(),i[p]&&l(i[p])):b.key==="Escape"&&a&&(b.preventDefault(),b.stopPropagation(),a())}let m=null;return r(V,{children:[r("input",{class:"open-omnibox-input",ref:f,placeholder:n,value:e,onInput:b=>t(b.target.value),onKeyDown:h}),r("div",{class:"open-omnibox-list",ref:c,children:[d===0&&r("div",{class:"open-omnibox-empty",children:s}),i.map((b,x)=>{const w=b.group&&b.group!==m?r("div",{class:"open-omnibox-group",children:b.group},`g:${b.group}`):null;return m=b.group,r(V,{children:[w,r("button",{class:`open-omnibox-row${x===p?" is-active":""}`,onMouseMove:()=>u(x),onClick:()=>l(b),children:[b.icon&&r("span",{class:"open-omnibox-icon",children:b.icon}),r("span",{class:"open-omnibox-label",children:b.label}),b.sub&&r("span",{class:"open-omnibox-sub",children:b.sub})]},b.key||x)]})})]}),r("div",{class:"open-omnibox-footer",children:[r("span",{children:[r("kbd",{children:"↑"}),r("kbd",{children:"↓"})," navigate"]}),r("span",{children:[r("kbd",{children:"↵"})," ",a?"select":"open"]}),r("span",{children:[r("kbd",{children:"esc"})," ",a?"back":"close"]})]})]})}function kb({card:e,onPick:t,onBack:n}){const[i,l]=B(""),a=(e.branches||[]).filter(o=>o.toLowerCase().includes(i.toLowerCase())),s=a.map(o=>({key:o,group:"Branches",icon:"⎇",label:o,value:o}));return i&&!a.includes(i)&&s.push({key:`__new:${i}`,group:"Branches",icon:"+",label:`new branch "${i}"`,value:i}),r(Qi,{value:i,onInput:l,placeholder:`branch in ${e.repo}…`,items:s,onPick:o=>t(o.value),onBack:n,empty:"type a new branch name…"})}function wb({card:e,onPick:t,onBack:n}){const[i,l]=B(""),a=i.trim(),s=a?[{key:`__tk:${a}`,group:"New track",icon:"▧",label:`create track "${a}"`,sub:e.repo,value:a}]:[];return r(Qi,{value:i,onInput:l,placeholder:`name this track (repo: ${e.repo})…`,items:s,onPick:o=>t(o.value),onBack:n,empty:"type a track name…"})}function vb({repos:e,onPick:t,onBack:n}){const[i,l]=B(""),s=(e||[]).filter(o=>o.label.toLowerCase().includes(i.toLowerCase())||o.repo.toLowerCase().includes(i.toLowerCase())).map(o=>({key:o.repo,group:"Repositories",icon:"◳",label:o.label,sub:o.repo,value:o.repo}));return r(Qi,{value:i,onInput:l,placeholder:"repo for this PR…",items:s,onPick:o=>t(o.value),onBack:n,empty:"no matching repo"})}function Mo({which:e,label:t,fonts:n}){return r("label",{children:[t,r("select",{name:`font-${e}`,value:ar(e).id,onChange:i=>jg(e,i.currentTarget.value),children:n.map(i=>r("option",{value:i.id,children:i.name},i.id))})]})}function _b(){return r("fieldset",{class:"font-picker",children:[r("legend",{children:"Fonts"}),r(Mo,{which:"ui",label:"Interface",fonts:hi}),r(Mo,{which:"code",label:"Code & terminal",fonts:hi.filter(e=>e.mono)})]})}const xb=[["needs_input","A session needs input"],["done","A session finishes"],["ci_failed","CI fails on a session's PR"],["pr_merged","A linked PR merges"],["usage","Usage projected past a limit"],["quiet_when_focused","Stay quiet while the dashboard is focused"],["group","Group when more than 2 arrive at once"],["badge","Dock badge counts sessions waiting on you"]];function Sb(){const e=pi();return r("fieldset",{class:"notify-settings",children:[r("legend",{children:"Notifications"}),!Ve()&&r("p",{class:"settings-overrides-hint",children:"Notifications appear in the Periscope macOS app only."}),xb.map(([t,n])=>r("label",{class:"notify-option",children:[r("input",{type:"checkbox",name:`notify-${t}`,checked:e[t],onChange:i=>qg(t,i.currentTarget.checked)}),n]},t))]})}function $b({critter:e}){return r("svg",{class:"critter-preview",viewBox:e.sprite?"0 0 20 20":"-1.5 1 20 20",width:"40",height:"40","shape-rendering":"crispEdges","aria-hidden":"true",children:e.sprite?r(Wi,{sprite:e.sprite}):r(qy,{})})}function Cb(){const e=hu();return r("fieldset",{class:"tama-picker",children:[r("legend",{children:"Tamagotchi"}),r("div",{class:"tama-picker-options",children:di.map(t=>r("label",{class:"tama-picker-option",children:[r("input",{type:"radio",name:"tama-critter",value:t.id,checked:t.id===e,onChange:()=>zg(t.id)}),r($b,{critter:t}),r("span",{children:t.name})]},t.id))})]})}const ln=H(!1);function Lo(){ln.value=!0,ye("overlay.open",{which:"settings"})}function On(){ln.value=!1}function Tb(){He(On,ln.value);const[e,t]=B(""),[n,i]=B(!1),[l,a]=B(14),[s,o]=B("sibling"),[u,c]=B([]),[f,d]=B(""),[p,h]=B([]),[m,b]=B("");j(()=>{const w=document.getElementById("settings-btn");return w&&w.addEventListener("click",Lo),()=>{w&&w.removeEventListener("click",Lo)}},[]),j(()=>{ln.value&&(t(""),(async()=>{const{data:w,error:C}=await pr("load settings","/api/settings");if(C){t(C);return}const y=w.settings||{};a(y.cleanup_idle_days??14),o(y.worktree_layout_default??"sibling"),c(Object.entries(y.worktree_layout_overrides||{}).map(([g,$])=>({repo:g,layout:$}))),h(w.editors_available||[]),d(y.editor??""),b((y.repo_roots??["~/dev"]).join(` `))})())},[ln.value]);async function x(w){w.preventDefault(),t("");const C=parseInt(l,10);if(!C||C<1){t("cleanup idle days must be a positive integer");return}const y={};for(const{repo:v,layout:E}of u)v&&E&&(y[v]=E);const g={cleanup_idle_days:C,worktree_layout_default:s,worktree_layout_overrides:y,editor:f||null,repo_roots:m.split(` -`).map(v=>v.trim()).filter(Boolean)};i(!0);const{error:$}=await pr("save settings","/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(g)});if(i(!1),$){t($);return}On()}return ln.value?r("div",{id:"settings-modal",class:"settings-modal-overlay",onClick:w=>{w.target.id==="settings-modal"&&On()},children:r("div",{class:"settings-modal-card",children:[r("header",{class:"settings-modal-head",children:[r("h2",{children:"🛠 settings"}),r("button",{id:"settings-modal-close",title:"close",onClick:On,children:"×"})]}),r("form",{id:"settings-form",onSubmit:x,children:[r(_b,{}),r(Cb,{}),r(Sb,{}),r("label",{children:["Cleanup idle threshold (days)",r("input",{id:"settings-cleanup-idle-days",type:"number",min:"1",value:l,onInput:w=>a(w.currentTarget.value)})]}),r("label",{children:["Preferred editor",r("select",{id:"settings-editor",value:f,onChange:w=>d(w.currentTarget.value),children:[r("option",{value:"",children:"(none — hides the rail's ↗ action)"}),p.map(w=>r("option",{value:w,children:w},w))]})]}),r("label",{children:["Repo roots",r("textarea",{id:"settings-repo-roots",rows:3,spellcheck:!1,value:m,onInput:w=>b(w.currentTarget.value)})]}),r("p",{class:"settings-overrides-hint",children:"One directory per line. Repos one level under each are offered in ⌘K; a repo elsewhere can be opened once by typing its path there."}),r("label",{children:["Default worktree layout",r("select",{id:"settings-worktree-default",value:s,onChange:w=>o(w.currentTarget.value),children:[r("option",{value:"sibling",children:"sibling — ~/dev/worktrees//"}),r("option",{value:"inline",children:"inline — /.worktrees/"})]})]}),r("fieldset",{children:[r("legend",{children:"Per-repo overrides"}),r("div",{id:"settings-overrides-list",children:u.length===0?r("div",{class:"settings-overrides-empty",children:"(none yet)"}):u.map((w,C)=>r("div",{class:"settings-overrides-row","data-repo":w.repo,children:[r("span",{class:"settings-overrides-repo",children:w.repo}),r("select",{class:"settings-overrides-layout",value:w.layout,onChange:y=>{const g=y.currentTarget.value;c($=>$.map((v,E)=>E===C?{...v,layout:g}:v))},children:[r("option",{value:"sibling",children:"sibling"}),r("option",{value:"inline",children:"inline"})]}),r("button",{type:"button",class:"settings-overrides-remove",title:"remove override",onClick:()=>c(y=>y.filter((g,$)=>$!==C)),children:"×"})]},w.repo))}),r("p",{class:"settings-overrides-hint",children:["Auto-detected on first ",r("code",{children:"+ project"})," per repo. Edit or remove rows here."]})]}),r("div",{id:"settings-modal-error",class:"settings-modal-error",hidden:!e,children:e}),r("div",{class:"settings-modal-actions",children:[r("button",{type:"button",id:"settings-cancel",onClick:On,children:"cancel"}),r("button",{type:"submit",id:"settings-submit",disabled:n,children:"save"})]})]})]})}):null}function Eb(){return j(()=>{Xg(),"serviceWorker"in navigator&&navigator.serviceWorker.register("/sw.js",{scope:"/"}).catch(()=>{})},[]),r(V,{children:[r(W2,{}),r(q2,{}),r(Tb,{}),r(lb,{}),r(gb,{})]})}const Io=120,Pb=3600,Yl=e=>`${e.repo_slug||e.repo_key||""}#${e.pr}`;function Oo(e){let t=0;for(const n of e.message||"")t=t*31+n.codePointAt(0)|0;return`question:${e.pane_id||e.target}@${e.ts}:${(t>>>0).toString(36)}`}const Lr={calm:0,grumpy:1,furious:2};function Ab(e,t,n){const{windows:i=[],alerts:l=[],dismissedAlertIds:a=new Set,dismissedNeedsPids:s=new Set,dismissedReadyPids:o=new Set,plan:u={}}=t,c=!e,f=(e==null?void 0:e.pass)??0,d=f+1,p=(e==null?void 0:e.seen)||{},h=new Set;for(const[T,S]of Object.entries(p))typeof S=="number"&&S<=f&&f-S<=Io&&h.add(T);const m={},b=[],x=(T,S)=>{m[T]=d,!c&&!h.has(T)&&b.push({key:T,...S})},w=T=>{m[T]=d},C=Pa(i,l,a,s),y=new Set;for(const T of C)T.kind==="live"&&(y.add(T.w.pid),x(`needs:${T.w.pid}@${T.w.needs_since||0}`,{kind:"needs_input",w:T.w}));for(const T of i)T.state==="needs-input"&&s.has(T.pid)&&w(`needs:${T.pid}@${T.needs_since||0}`);for(const T of C){if(T.kind!=="event")continue;const S=Oo(T);T.w&&y.has(T.w.pid)?(c||h.has(S))&&w(S):x(S,{kind:"question",w:T.w,alert:T})}for(const T of l)T.kind==="need_human"&&a.has(T.id)&&w(Oo(T));for(const T of i){if(T.state!=="done")continue;const S=`done:${T.pid}@${T.completed_at||0}`;o.has(T.pid)?w(S):x(S,{kind:"done",w:T})}const g={};for(const[T,S]of Object.entries((e==null?void 0:e.prAt)||{}))d-S<=Pb&&(g[T]=S);const $={},v={};for(const T of Object.keys(g))T in((e==null?void 0:e.ci)||{})&&($[T]=e.ci[T]),T in((e==null?void 0:e.prs)||{})&&(v[T]=e.prs[T]);const E=new Map;for(const T of i){if(!T.pr||T.pr_linked&&!T.pr_state)continue;const S=Yl(T);(!E.has(S)||T.pr_state&&!E.get(S).pr_state)&&E.set(S,T)}for(const[T,S]of E){g[T]=d;const F=(!S.pr_state||S.pr_state==="open")&&S.ci||null;!c&&T in $&&$[T]!=="✗"&&F==="✗"&&b.push({key:`ci:${T}`,kind:"ci_failed",w:S}),$[T]=F,S.pr_state&&(!c&&v[T]==="open"&&S.pr_state==="merged"&&b.push({key:`merged:${T}`,kind:"pr_merged",w:S}),v[T]=S.pr_state)}const A={};for(const[T,S]of Object.entries((e==null?void 0:e.meters)||{}))Number(T.slice(T.lastIndexOf("/")+1))*60>n&&(A[T]=S);for(const[T,S]of Object.entries(u||{})){if(!(S!=null&&S.available)||!S.meters||!c&&Ba(S,n))continue;let R=null;for(const[F,q]of Object.entries(S.meters)){if(!(q!=null&&q.resets_at))continue;const L=ju(q),I=`${T}/${F}/${Math.round(q.resets_at/60)}`,O=A[I]??0;A[I]=Math.max(O,Lr[L]),!c&&Lr[L]>O&&(!R||Lr[L]>Lr[R.mood])&&(R={key:`usage:${I}:${L}`,kind:"usage",account:T,meter:F,m:q,mood:L})}R&&b.push(R)}for(const T of h)!(T in m)&&d-p[T]<=Io&&(m[T]=p[T]);return{state:{pass:d,seen:m,ci:$,prs:v,prAt:g,meters:A},events:b}}const Qn=(e,t)=>{const n=String(e||"").trim(),i=Array.from(n);return i.length>t?`${i.slice(0,t-1).join("")}…`:n},gl=e=>Qn(Se(e),26),Dn=e=>[e==null?void 0:e.session,e==null?void 0:e.branch].filter(Boolean).join(" · "),yl=e=>!!(e!=null&&e.pid&&e.agent==="claude"),Do=e=>{const t=/^mcp__(.+?)__(.+)$/.exec(e||"");return t?`${t[2]} (${t[1]})`:e||"this"};function Mb(e){const t=String(e||"").replace(/\s+/g," ").trim(),n=t.match(/^.*?[.!?](?=\s[A-Z0-9]|$)/);return Qn(n?n[0]:t,140)}const Ro={permission:"Waiting for your approval.",question:"Waiting for your answer.",dialog:"Waiting for your input."},Lb={failure:"✗",pending:"⟳",success:"✓"},Cd=e=>`usage_to_${e}`;function Td(e,t={}){var l,a;const n=e.w,i={kind:e.kind,thread:e.kind,target:(n==null?void 0:n.target)||"",data:{}};switch(e.kind){case"needs_input":{const s=n.pending_permission;return s!=null&&s.request_id&&n.pid?{...i,title:`${gl(n)} needs you`,subtitle:Dn(n),body:s.summary?`Allow ${Do(s.tool)}: ${s.summary}?`:`Allow ${Do(s.tool)}?`,category:"needs_permission",data:{open:"pane",pid:n.pid,request_id:s.request_id}}:{...i,title:`${gl(n)} needs you`,subtitle:Dn(n),body:Ro[su(n.waiting_for)]||Ro.dialog,category:"needs_dialog",data:{open:"pane",pid:n.pid}}}case"question":{const s=e.alert;return{...i,title:`${Qn(n?Se(n):s.name||s.session||"A session",26)} asks`,subtitle:n?Dn(n):s.session||"",body:Qn(s.message,180),target:s.target||i.target,...yl(n)?{category:"question"}:{},data:{open:"pane",pid:(n==null?void 0:n.pid)??null}}}case"done":return{...i,title:`${gl(n)} is done`,subtitle:n.pr&&n.pr_linked?`${n.session} · PR #${n.pr}`:Dn(n),body:Mb(n.recap||n.last_line)||"Finished.",category:"done_seen",data:{open:"pane",pid:n.pid,session:n.session}};case"ci_failed":{const s=(n.ci_checks||[]).map(f=>`${Lb[f.state]||"·"} ${f.name}`).join(" · "),o=rr(n.repo_slug,n.pr),u=yl(n)?n:(t.windows||[]).find(f=>yl(f)&&Yl(f)===Yl(n));let c;return u!=null&&u.pid&&o?c="ci_failed":u!=null&&u.pid?c="ci_failed_fix":o&&(c="ci_failed_pr"),{...i,title:`CI failed on #${n.pr}`,subtitle:[n.session,Se(n)].filter(Boolean).join(" · "),body:s||"Checks are failing.",...c?{category:c}:{},data:{open:"url",url:o,pid:(u==null?void 0:u.pid)||n.pid,pr:n.pr,checks:(n.ci_checks||[]).filter(f=>f.state==="failure").map(f=>f.name)}}}case"pr_merged":{const s=rr(n.repo_slug,n.pr);let o;return n.cwd&&s?o="pr_merged":n.cwd?o="pr_merged_cleanup":s&&(o="pr_merged_pr"),{...i,title:`PR #${n.pr} merged`,subtitle:n.pr_base?`${n.session} · ${n.branch} → ${n.pr_base}`:Dn(n),body:"The worktree and its session can be cleaned up.",...o?{category:o}:{},data:{open:"url",url:s,pid:n.pid,cwd:n.cwd}}}case"usage":{const s=t.accounts||[],o=Qn(((l=s.find(h=>h.id===e.account))==null?void 0:l.label)||e.account,16),u=s.length>1,c=Math.max(...[e.m.projected_percent,e.m.projected_recent].filter(h=>h!=null),-1),f=(t.windows||[]).filter(h=>h.state==="working"&&h.account===e.account).length,d=(a=t.moveTargets)==null?void 0:a[e.account],p=d&&d!==e.account;return{...i,target:"",title:u?`Account ${o} is running hot`:"Usage is running hot",subtitle:`${ja(e.meter)} usage ${Math.round(e.m.percent??0)}%`+(c>=0?` · →${Math.round(c)}% at reset`:""),body:u?`${kn(f,"session")} working on ${o}.`:`${kn(f,"session")} working.`,category:p?Cd(d):"usage",data:p?{open:"usage",spawn_to:d}:{open:"usage"}}}default:return null}}const it=(e,t,n={})=>Object.freeze({id:e,title:t,...n}),Fo=it("open_pr","Open PR"),Ib=it("mark_seen","Mark seen"),No=it("cleanup","Clean up worktree",{foreground:!0}),Ob=it("details","Details",{foreground:!0}),Db=it("open","Open",{foreground:!0}),Bo=it("fix","Ask agent to fix",{auth:!0});function Rb({accounts:e=[],moveTargets:t={}}={}){const n=[{id:"done_seen",actions:[Ib]},{id:"ci_failed_pr",actions:[Fo]},{id:"pr_merged",actions:[No]},{id:"pr_merged_cleanup",actions:[No]},{id:"pr_merged_pr",actions:[Fo]},{id:"usage",actions:[Ob]},{id:"needs_permission",actions:[it("allow","Allow",{auth:!0})]},{id:"needs_dialog",actions:[Db]},{id:"question",actions:[it("send","Reply",{auth:!0,reply:{button:"Send",placeholder:"Reply…"}})]},{id:"ci_failed",actions:[Bo]},{id:"ci_failed_fix",actions:[Bo]}];for(const i of[...new Set(Object.values(t||{}))].sort())n.push({id:Cd(i),actions:[it("spawn_on",`Spawn on ${gn(i,e||[])}`)]});return n}const Fb=Object.freeze({openExternal:qi,openUsageDetails:vd,pickSpawnAccount:Cu,openCleanupModal:_d,markReadySeen:_u,selectPane:yt,revealPane:jt,post:Tg,toast:bn,notify:mu}),Nb={allow:"Allow",deny:"Deny",send:"Reply",fix:"Ask agent to fix"},Bb={allow:"Couldn't allow",deny:"Couldn't deny",send:"Couldn't send your reply",fix:"Couldn't ask the agent to fix"},zb=new Set(["allow","deny"]),jb={"/api/pane/permission":"The dialog changed — open the pane to answer it.","/api/pane/reply":"The pane has a dialog open, or isn't running an agent — open it to see why."};function Hb(e,t){return(t==null?void 0:t.status)===404?"The pane is gone.":(t==null?void 0:t.status)===0?"Periscope isn't answering.":(t==null?void 0:t.status)===409?typeof t.detail=="string"&&t.detail!==`HTTP ${t.status}`?t.detail:jb[e]:(t==null?void 0:t.detail)||"No answer"}const qb=e=>{const t=e.codePointAt(0);return t!==9&&t!==10&&(t<32||t>=127&&t<=159)},Ub=e=>[...String(e)].filter(t=>!qb(t)).join("").trim();function Wb(e,t){const n=(t||[]).map(Ub).filter(Boolean),i=n.length?`${n.join(" · ")}.`:"Checks are failing.";return`CI failed on #${e}: ${i} Please investigate and fix.`}async function bl({target:e,show:t},n,i,l,a){let s=!1;const o=u=>{s||(s=!0,Promise.resolve().then(()=>a.notify({kind:"notice",thread:"notice",title:Bb[n],body:u,target:e,data:{open:"pane",pid:l.pid},quiet_when_focused:!0})).catch(c=>console.error("[notify] follow-up failed",c)))};try{if(!l.pid||zb.has(n)&&!l.request_id){o("Open the pane to answer it."),t();return}const u=await a.post(i,l);if(u!=null&&u.ok)return;const c=Hb(i,u);o(c),t(),(u==null?void 0:u.status)!==409&&a.toast(`${Nb[n]} failed: ${(u==null?void 0:u.detail)||"no answer"}`,"bad",6e3)}catch(u){console.error("[notify] action failed",n,u),o((u==null?void 0:u.message)||"Periscope isn't answering.");try{t()}catch(c){console.error("[notify] reveal failed",c)}}}function Kb({target:e,action:t,text:n,data:i}={},l=Fb){i||(i={});let a;const s=()=>a?l.selectPane(a):e&&l.revealPane(e);try{switch(a=i.pid?(de.peek()||[]).find(o=>String(o.pid)===String(i.pid)):null,t){case"open_pr":i.url?l.openExternal(i.url):s();return;case"details":l.openUsageDetails();return;case"spawn_on":i.spawn_to&&l.pickSpawnAccount(i.spawn_to);return;case"mark_seen":a&&l.markReadySeen(a);return;case"cleanup":{const o=i.cwd||(a==null?void 0:a.cwd);o?l.openCleanupModal({paths:[o]}):s();return}case"allow":case"deny":return bl({target:e,show:s},t,"/api/pane/permission",{pid:i.pid,request_id:i.request_id,choice:t},l);case"send":{const o=String(n||"").trim();return o?bl({target:e,show:s},t,"/api/pane/reply",{pid:i.pid,text:o},l):void 0}case"fix":return bl({target:e,show:s},t,"/api/pane/reply",{pid:i.pid,text:Wb(i.pr,i.checks)},l);case"open":s();return;default:console.warn("[notify] unknown action",t)}}catch(o){console.error("[notify] action failed",t,o);try{s()}catch(u){console.error("[notify] reveal failed",u)}}}const Zl=3e3,Vb=2,Gb=new Set(["needs_input","question"]);function Jb(e,t,n,i=!0){const l=t.filter(o=>n-o{var t;return e.kind==="question"?"question":su((t=e.w)==null?void 0:t.waiting_for)==="permission"?"permission":"input"};function Yb(e){var n,i;const t=new Set(e.map((l,a)=>{var s,o;return((s=l.w)==null?void 0:s.pid)??((o=l.alert)==null?void 0:o.pane_id)??a})).size;return{kind:"summary",thread:"needs_you",target:"",data:{open:"pane",pid:((i=(n=e[0])==null?void 0:n.w)==null?void 0:i.pid)??null},title:t===1?"1 more session needs you":`${t} more sessions need you`,subtitle:"",body:e.map(l=>{var a;return`● ${l.w?Se(l.w):((a=l.alert)==null?void 0:a.name)||"a session"} · ${Qb(l)}`}).join(` +`).map(v=>v.trim()).filter(Boolean)};i(!0);const{error:$}=await pr("save settings","/api/settings",{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify(g)});if(i(!1),$){t($);return}On()}return ln.value?r("div",{id:"settings-modal",class:"settings-modal-overlay",onClick:w=>{w.target.id==="settings-modal"&&On()},children:r("div",{class:"settings-modal-card",children:[r("header",{class:"settings-modal-head",children:[r("h2",{children:"🛠 settings"}),r("button",{id:"settings-modal-close",title:"close",onClick:On,children:"×"})]}),r("form",{id:"settings-form",onSubmit:x,children:[r(_b,{}),r(Cb,{}),r(Sb,{}),r("label",{children:["Cleanup idle threshold (days)",r("input",{id:"settings-cleanup-idle-days",type:"number",min:"1",value:l,onInput:w=>a(w.currentTarget.value)})]}),r("label",{children:["Preferred editor",r("select",{id:"settings-editor",value:f,onChange:w=>d(w.currentTarget.value),children:[r("option",{value:"",children:"(none — hides the rail's ↗ action)"}),p.map(w=>r("option",{value:w,children:w},w))]})]}),r("label",{children:["Repo roots",r("textarea",{id:"settings-repo-roots",rows:3,spellcheck:!1,value:m,onInput:w=>b(w.currentTarget.value)})]}),r("p",{class:"settings-overrides-hint",children:"One directory per line. Repos one level under each are offered in ⌘K; a repo elsewhere can be opened once by typing its path there."}),r("label",{children:["Default worktree layout",r("select",{id:"settings-worktree-default",value:s,onChange:w=>o(w.currentTarget.value),children:[r("option",{value:"sibling",children:"sibling — ~/dev/worktrees//"}),r("option",{value:"inline",children:"inline — /.worktrees/"})]})]}),r("fieldset",{children:[r("legend",{children:"Per-repo overrides"}),r("div",{id:"settings-overrides-list",children:u.length===0?r("div",{class:"settings-overrides-empty",children:"(none yet)"}):u.map((w,C)=>r("div",{class:"settings-overrides-row","data-repo":w.repo,children:[r("span",{class:"settings-overrides-repo",children:w.repo}),r("select",{class:"settings-overrides-layout",value:w.layout,onChange:y=>{const g=y.currentTarget.value;c($=>$.map((v,E)=>E===C?{...v,layout:g}:v))},children:[r("option",{value:"sibling",children:"sibling"}),r("option",{value:"inline",children:"inline"})]}),r("button",{type:"button",class:"settings-overrides-remove",title:"remove override",onClick:()=>c(y=>y.filter((g,$)=>$!==C)),children:"×"})]},w.repo))}),r("p",{class:"settings-overrides-hint",children:["Auto-detected on first ",r("code",{children:"+ project"})," per repo. Edit or remove rows here."]})]}),r("div",{id:"settings-modal-error",class:"settings-modal-error",hidden:!e,children:e}),r("div",{class:"settings-modal-actions",children:[r("button",{type:"button",id:"settings-cancel",onClick:On,children:"cancel"}),r("button",{type:"submit",id:"settings-submit",disabled:n,children:"save"})]})]})]})}):null}function Eb(){return j(()=>{Xg(),"serviceWorker"in navigator&&navigator.serviceWorker.register("/sw.js",{scope:"/"}).catch(()=>{})},[]),r(V,{children:[r(W2,{}),r(q2,{}),r(Tb,{}),r(lb,{}),r(gb,{})]})}const Io=120,Pb=3600,Yl=e=>`${e.repo_slug||e.repo_key||""}#${e.pr}`;function Oo(e){let t=0;for(const n of e.message||"")t=t*31+n.codePointAt(0)|0;return`question:${e.pane_id||e.target}@${e.ts}:${(t>>>0).toString(36)}`}const Lr={calm:0,grumpy:1,furious:2};function Ab(e,t,n){const{windows:i=[],alerts:l=[],dismissedAlertIds:a=new Set,dismissedNeedsPids:s=new Set,dismissedReadyPids:o=new Set,plan:u={}}=t,c=!e,f=(e==null?void 0:e.pass)??0,d=f+1,p=(e==null?void 0:e.seen)||{},h=new Set;for(const[T,S]of Object.entries(p))typeof S=="number"&&S<=f&&f-S<=Io&&h.add(T);const m={},b=[],x=(T,S)=>{m[T]=d,!c&&!h.has(T)&&b.push({key:T,...S})},w=T=>{m[T]=d},C=Pa(i,l,a,s),y=new Set;for(const T of C)T.kind==="live"&&(y.add(T.w.pid),x(`needs:${T.w.pid}@${T.w.needs_since||0}`,{kind:"needs_input",w:T.w}));for(const T of i)T.state==="needs-input"&&s.has(T.pid)&&w(`needs:${T.pid}@${T.needs_since||0}`);for(const T of C){if(T.kind!=="event")continue;const S=Oo(T);T.w&&y.has(T.w.pid)?(c||h.has(S))&&w(S):x(S,{kind:"question",w:T.w,alert:T})}for(const T of l)T.kind==="need_human"&&a.has(T.id)&&w(Oo(T));for(const T of i){if(T.state!=="done")continue;const S=`done:${T.pid}@${T.completed_at||0}`;o.has(T.pid)?w(S):x(S,{kind:"done",w:T})}const g={};for(const[T,S]of Object.entries((e==null?void 0:e.prAt)||{}))d-S<=Pb&&(g[T]=S);const $={},v={};for(const T of Object.keys(g))T in((e==null?void 0:e.ci)||{})&&($[T]=e.ci[T]),T in((e==null?void 0:e.prs)||{})&&(v[T]=e.prs[T]);const E=new Map;for(const T of i){if(!T.pr||T.pr_linked&&!T.pr_state)continue;const S=Yl(T);(!E.has(S)||T.pr_state&&!E.get(S).pr_state)&&E.set(S,T)}for(const[T,S]of E){g[T]=d;const F=(!S.pr_state||S.pr_state==="open")&&S.ci||null;!c&&T in $&&$[T]!=="✗"&&F==="✗"&&b.push({key:`ci:${T}`,kind:"ci_failed",w:S}),$[T]=F,S.pr_state&&(!c&&v[T]==="open"&&S.pr_state==="merged"&&b.push({key:`merged:${T}`,kind:"pr_merged",w:S}),v[T]=S.pr_state)}const A={};for(const[T,S]of Object.entries((e==null?void 0:e.meters)||{}))Number(T.slice(T.lastIndexOf("/")+1))*60>n&&(A[T]=S);for(const[T,S]of Object.entries(u||{})){if(!(S!=null&&S.available)||!S.meters||!c&&Ba(S,n))continue;let R=null;for(const[F,q]of Object.entries(S.meters)){if(!(q!=null&&q.resets_at))continue;const L=ju(q),I=`${T}/${F}/${Math.round(q.resets_at/60)}`,O=A[I]??0;A[I]=Math.max(O,Lr[L]),!c&&Lr[L]>O&&(!R||Lr[L]>Lr[R.mood])&&(R={key:`usage:${I}:${L}`,kind:"usage",account:T,meter:F,m:q,mood:L})}R&&b.push(R)}for(const T of h)!(T in m)&&d-p[T]<=Io&&(m[T]=p[T]);return{state:{pass:d,seen:m,ci:$,prs:v,prAt:g,meters:A},events:b}}const Qn=(e,t)=>{const n=String(e||"").trim(),i=Array.from(n);return i.length>t?`${i.slice(0,t-1).join("")}…`:n},gl=e=>Qn(Se(e),26),Dn=e=>[e==null?void 0:e.session,e==null?void 0:e.branch].filter(Boolean).join(" · "),yl=e=>!!(e!=null&&e.pid&&e.agent==="claude"),Do=e=>{const t=/^mcp__(.+?)__(.+)$/.exec(e||"");return t?`${t[2]} (${t[1]})`:e||"this"};function Mb(e){const t=String(e||"").replace(/\s+/g," ").trim(),n=t.match(/^.*?[.!?](?=\s[A-Z0-9]|$)/);return Qn(n?n[0]:t,140)}const Ro={permission:"Waiting for your approval.",question:"Waiting for your answer.",dialog:"Waiting for your input."},Lb={failure:"✗",pending:"⟳",success:"✓"},Cd=e=>`usage_to_${e}`;function Td(e,t={}){var l,a;const n=e.w,i={kind:e.kind,thread:e.kind,target:(n==null?void 0:n.target)||"",data:{}};switch(e.kind){case"needs_input":{const s=n.pending_permission;return s!=null&&s.request_id&&n.pid?{...i,title:`${gl(n)} needs you`,subtitle:Dn(n),body:s.summary?`Allow ${Do(s.tool)}: ${s.summary}?`:`Allow ${Do(s.tool)}?`,category:"needs_permission",data:{open:"pane",pid:n.pid,request_id:s.request_id}}:{...i,title:`${gl(n)} needs you`,subtitle:Dn(n),body:Ro[su(n.waiting_for)]||Ro.dialog,category:"needs_dialog",data:{open:"pane",pid:n.pid}}}case"question":{const s=e.alert;return{...i,title:`${Qn(n?Se(n):s.name||s.session||"A session",26)} asks`,subtitle:n?Dn(n):s.session||"",body:Qn(s.message,180),target:s.target||i.target,...yl(n)?{category:"question"}:{},data:{open:"pane",pid:(n==null?void 0:n.pid)??null}}}case"done":return{...i,title:`${gl(n)} is done`,subtitle:n.pr&&n.pr_linked?`${n.session} · PR #${n.pr}`:Dn(n),body:Mb(n.last_reply||n.recap||n.last_line)||"Finished.",category:"done_seen",data:{open:"pane",pid:n.pid,session:n.session}};case"ci_failed":{const s=(n.ci_checks||[]).map(f=>`${Lb[f.state]||"·"} ${f.name}`).join(" · "),o=rr(n.repo_slug,n.pr),u=yl(n)?n:(t.windows||[]).find(f=>yl(f)&&Yl(f)===Yl(n));let c;return u!=null&&u.pid&&o?c="ci_failed":u!=null&&u.pid?c="ci_failed_fix":o&&(c="ci_failed_pr"),{...i,title:`CI failed on #${n.pr}`,subtitle:[n.session,Se(n)].filter(Boolean).join(" · "),body:s||"Checks are failing.",...c?{category:c}:{},data:{open:"url",url:o,pid:(u==null?void 0:u.pid)||n.pid,pr:n.pr,checks:(n.ci_checks||[]).filter(f=>f.state==="failure").map(f=>f.name)}}}case"pr_merged":{const s=rr(n.repo_slug,n.pr);let o;return n.cwd&&s?o="pr_merged":n.cwd?o="pr_merged_cleanup":s&&(o="pr_merged_pr"),{...i,title:`PR #${n.pr} merged`,subtitle:n.pr_base?`${n.session} · ${n.branch} → ${n.pr_base}`:Dn(n),body:"The worktree and its session can be cleaned up.",...o?{category:o}:{},data:{open:"url",url:s,pid:n.pid,cwd:n.cwd}}}case"usage":{const s=t.accounts||[],o=Qn(((l=s.find(h=>h.id===e.account))==null?void 0:l.label)||e.account,16),u=s.length>1,c=Math.max(...[e.m.projected_percent,e.m.projected_recent].filter(h=>h!=null),-1),f=(t.windows||[]).filter(h=>h.state==="working"&&h.account===e.account).length,d=(a=t.moveTargets)==null?void 0:a[e.account],p=d&&d!==e.account;return{...i,target:"",title:u?`Account ${o} is running hot`:"Usage is running hot",subtitle:`${ja(e.meter)} usage ${Math.round(e.m.percent??0)}%`+(c>=0?` · →${Math.round(c)}% at reset`:""),body:u?`${kn(f,"session")} working on ${o}.`:`${kn(f,"session")} working.`,category:p?Cd(d):"usage",data:p?{open:"usage",spawn_to:d}:{open:"usage"}}}default:return null}}const it=(e,t,n={})=>Object.freeze({id:e,title:t,...n}),Fo=it("open_pr","Open PR"),Ib=it("mark_seen","Mark seen"),No=it("cleanup","Clean up worktree",{foreground:!0}),Ob=it("details","Details",{foreground:!0}),Db=it("open","Open",{foreground:!0}),Bo=it("fix","Ask agent to fix",{auth:!0});function Rb({accounts:e=[],moveTargets:t={}}={}){const n=[{id:"done_seen",actions:[Ib]},{id:"ci_failed_pr",actions:[Fo]},{id:"pr_merged",actions:[No]},{id:"pr_merged_cleanup",actions:[No]},{id:"pr_merged_pr",actions:[Fo]},{id:"usage",actions:[Ob]},{id:"needs_permission",actions:[it("allow","Allow",{auth:!0})]},{id:"needs_dialog",actions:[Db]},{id:"question",actions:[it("send","Reply",{auth:!0,reply:{button:"Send",placeholder:"Reply…"}})]},{id:"ci_failed",actions:[Bo]},{id:"ci_failed_fix",actions:[Bo]}];for(const i of[...new Set(Object.values(t||{}))].sort())n.push({id:Cd(i),actions:[it("spawn_on",`Spawn on ${gn(i,e||[])}`)]});return n}const Fb=Object.freeze({openExternal:qi,openUsageDetails:vd,pickSpawnAccount:Cu,openCleanupModal:_d,markReadySeen:_u,selectPane:yt,revealPane:jt,post:Tg,toast:bn,notify:mu}),Nb={allow:"Allow",deny:"Deny",send:"Reply",fix:"Ask agent to fix"},Bb={allow:"Couldn't allow",deny:"Couldn't deny",send:"Couldn't send your reply",fix:"Couldn't ask the agent to fix"},zb=new Set(["allow","deny"]),jb={"/api/pane/permission":"The dialog changed — open the pane to answer it.","/api/pane/reply":"The pane has a dialog open, or isn't running an agent — open it to see why."};function Hb(e,t){return(t==null?void 0:t.status)===404?"The pane is gone.":(t==null?void 0:t.status)===0?"Periscope isn't answering.":(t==null?void 0:t.status)===409?typeof t.detail=="string"&&t.detail!==`HTTP ${t.status}`?t.detail:jb[e]:(t==null?void 0:t.detail)||"No answer"}const qb=e=>{const t=e.codePointAt(0);return t!==9&&t!==10&&(t<32||t>=127&&t<=159)},Ub=e=>[...String(e)].filter(t=>!qb(t)).join("").trim();function Wb(e,t){const n=(t||[]).map(Ub).filter(Boolean),i=n.length?`${n.join(" · ")}.`:"Checks are failing.";return`CI failed on #${e}: ${i} Please investigate and fix.`}async function bl({target:e,show:t},n,i,l,a){let s=!1;const o=u=>{s||(s=!0,Promise.resolve().then(()=>a.notify({kind:"notice",thread:"notice",title:Bb[n],body:u,target:e,data:{open:"pane",pid:l.pid},quiet_when_focused:!0})).catch(c=>console.error("[notify] follow-up failed",c)))};try{if(!l.pid||zb.has(n)&&!l.request_id){o("Open the pane to answer it."),t();return}const u=await a.post(i,l);if(u!=null&&u.ok)return;const c=Hb(i,u);o(c),t(),(u==null?void 0:u.status)!==409&&a.toast(`${Nb[n]} failed: ${(u==null?void 0:u.detail)||"no answer"}`,"bad",6e3)}catch(u){console.error("[notify] action failed",n,u),o((u==null?void 0:u.message)||"Periscope isn't answering.");try{t()}catch(c){console.error("[notify] reveal failed",c)}}}function Kb({target:e,action:t,text:n,data:i}={},l=Fb){i||(i={});let a;const s=()=>a?l.selectPane(a):e&&l.revealPane(e);try{switch(a=i.pid?(de.peek()||[]).find(o=>String(o.pid)===String(i.pid)):null,t){case"open_pr":i.url?l.openExternal(i.url):s();return;case"details":l.openUsageDetails();return;case"spawn_on":i.spawn_to&&l.pickSpawnAccount(i.spawn_to);return;case"mark_seen":a&&l.markReadySeen(a);return;case"cleanup":{const o=i.cwd||(a==null?void 0:a.cwd);o?l.openCleanupModal({paths:[o]}):s();return}case"allow":case"deny":return bl({target:e,show:s},t,"/api/pane/permission",{pid:i.pid,request_id:i.request_id,choice:t},l);case"send":{const o=String(n||"").trim();return o?bl({target:e,show:s},t,"/api/pane/reply",{pid:i.pid,text:o},l):void 0}case"fix":return bl({target:e,show:s},t,"/api/pane/reply",{pid:i.pid,text:Wb(i.pr,i.checks)},l);case"open":s();return;default:console.warn("[notify] unknown action",t)}}catch(o){console.error("[notify] action failed",t,o);try{s()}catch(u){console.error("[notify] reveal failed",u)}}}const Zl=3e3,Vb=2,Gb=new Set(["needs_input","question"]);function Jb(e,t,n,i=!0){const l=t.filter(o=>n-o{var t;return e.kind==="question"?"question":su((t=e.w)==null?void 0:t.waiting_for)==="permission"?"permission":"input"};function Yb(e){var n,i;const t=new Set(e.map((l,a)=>{var s,o;return((s=l.w)==null?void 0:s.pid)??((o=l.alert)==null?void 0:o.pane_id)??a})).size;return{kind:"summary",thread:"needs_you",target:"",data:{open:"pane",pid:((i=(n=e[0])==null?void 0:n.w)==null?void 0:i.pid)??null},title:t===1?"1 more session needs you":`${t} more sessions need you`,subtitle:"",body:e.map(l=>{var a;return`● ${l.w?Se(l.w):((a=l.alert)==null?void 0:a.name)||"a session"} · ${Qb(l)}`}).join(` `)}}function Ed(e,t,n){if(e.length===1)try{const i=t(e[0],n);if(i)return i}catch(i){console.error("[notify] banner failed",e[0].key,i)}return Yb(e)}const Pd="periscope.notify.v1",Zb=15*60*1e3;function Xb(e,t){try{const n=JSON.parse((e==null?void 0:e.getItem(Pd))||"null");if(!(n!=null&&n.state)||!(n.at<=t)||t-n.at>Zb)return null;const i=Array.isArray(n.held)?n.held:[];return{state:n.state,held:i,heldSince:i.length?n.heldSince??n.at:null}}catch{return null}}function ek(e,t,n,i=[],l=null){try{e==null||e.setItem(Pd,JSON.stringify({at:n,state:t,held:i,heldSince:l}))}catch{}}const tk={question:"needs_input"},nk=(e,t)=>e[tk[t]||t]!==!1;function rk({target:e,data:t}={}){if(t||(t={}),t.open==="url"&&t.url){qi(t.url);return}if(t.open==="usage"){vd();return}const n=t.pid!=null&&(de.value||[]).find(i=>String(i.pid)===String(t.pid));n?yt(n):jt(e)}const ik={sends:[],flush:[],badge:null};function lk(e,t,n,i,l=Td){let{state:a,recent:s,held:o,heldSince:u,lastBadge:c,lastCategories:f}=e;const d={quiet_when_focused:n.quiet_when_focused},p=[],h={accounts:t.accounts,windows:t.windows,moveTargets:t.moveTargets};o.length&&i-u>=Zl&&(p.push({...Ed(o,l,h),...d}),o=[],u=null);let m=null;const b=g=>({state:a,recent:s,held:o,heldSince:u,lastBadge:c,lastCategories:f,...ik,flush:p,categories:m,timerAt:o.length?u+Zl:null,...g});if(t.alerts===null)return b();try{const g=Rb({accounts:t.accounts,moveTargets:t.moveTargets}),$=JSON.stringify(g);$!==f&&(m=g,f=$)}catch(g){console.error("[notify] category set failed",g)}const x=ku(t.windows,t.alerts,t.dismissedAlertIds,t.dismissedNeedsPids,i/1e3),w=n.badge?x.size:0,C=w===c?null:w;if(c=w,!a&&(!t.windows.length||e.editing))return b({badge:C});const y=[];if(!e.editing){const g=Ab(a,t,i/1e3);a=g.state;const $=Jb(g.events.filter(v=>nk(n,v.kind)),s,i,n.group);s=$.recent;for(const v of $.now){let E=null;try{E=l(v,h)}catch(A){console.error("[notify] banner failed",v.key,A)}E&&y.push({...E,...d})}$.held.length&&(o=[...o,...$.held],u??(u=i))}return b({sends:y,badge:C})}const ak=2e3;function sk(e={setNotifyCategories:Zg,notify:mu}){let t=Promise.resolve();return(n,i)=>{n&&(t=t.then(()=>Promise.race([e.setNotifyCategories(n),new Promise(l=>setTimeout(l,ak))])).catch(l=>console.error("[notify] registration failed",l))),i.length&&(t=t.then(()=>{for(const l of i)try{e.notify(l)}catch(a){console.error("[notify] post failed",a)}}))}}let zo=!1;function ok(){if(zo||!Ve())return;zo=!0;const e=typeof localStorage>"u"?null:localStorage,t=Xb(e,Date.now());let n={state:null,held:[],heldSince:null,...t,recent:[],lastBadge:void 0,lastCategories:void 0};const i=u=>ek(e,n.state,u,n.held,n.heldSince),l=sk();let a=null,s=null;const o=()=>{a=null,s=null;const u=n.held;if(n={...n,held:[],heldSince:null},!u.length)return;n.state&&i(Date.now());const c={accounts:at.peek(),windows:de.peek()||[],moveTargets:si.peek()};l(null,[{...Ed(u,Td,c),quiet_when_focused:pi().quiet_when_focused}])};Qg(rk),Yg(u=>Kb(u)),lt(()=>{$c.value,Ai(()=>{var u;try{const c={windows:de.peek()||[],alerts:er.peek(),dismissedAlertIds:nr.peek(),dismissedNeedsPids:ht.peek(),dismissedReadyPids:ft.peek(),plan:((u=Oi.peek())==null?void 0:u.plan)||{},accounts:at.peek(),moveTargets:si.peek()},f=!!(pa.peek()||Et.peek()),d=Date.now(),p=n,{sends:h,flush:m,badge:b,categories:x,timerAt:w,...C}=lk({...n,editing:f},c,pi(),d);n=C,n.state&&(n.state!==p.state||n.held!==p.held)&&i(d),l(x,[...m,...h]),w==null?(a&&clearTimeout(a),a=null,s=null):w!==s&&(a&&clearTimeout(a),s=w,a=setTimeout(o,Math.max(0,w-d))),b!==null&&Gg(b)}catch(c){console.error("[notify] pass failed",c)}})})}const ck=3e3,uk=2e3;let $i=0;function Ad(){return document.getElementById("connection-banner")}function Md(e){try{if(er.value=e.alerts||[],pa.value||Et.value)return;de.value=e.windows||[],Li.value=e.projects||[],Ii.value=e.workspaces||[],Bt.value=e.tracks||[],wh(de.value),Oi.value={plan:e.usage_plan,fallback:e.usage,poke:e.poke||{}},ha.value=e.update||null,at.value=e.accounts||[],si.value=e.move_targets||{},Un.value=e.spawn_account||null,Wn.value=e.spawn_model||null,Kn.value=e.spawn_effort||null,sn.value=e.launch_default||null,xc.value=e.walls||null,fa.value=e.wall_spans||[],Sc.value=e.editor||""}finally{$c.value+=1}}function Ld(){if($i>0){$i=0;const e=Ad();e&&(e.hidden=!0),document.body.classList.remove("disconnected")}}function Id(e){if($i+=1,$i>=2){const t=Ad();t&&(t.hidden=!1),document.body.classList.add("disconnected")}e&&console.warn("[periscope]",e)}async function dk(){if(!pa.value&&!Et.value)try{const e=await fetch("/api/state");if(!e.ok)throw new Error(`HTTP ${e.status}`);Md(await e.json()),Ld()}catch(e){Id(`poll failed: ${e.message}`)}}let kl=!1,Xl=!1,Yn=null,Ot=null,fn=null;function hk(){if(Ot)return;const e=async()=>{await dk(),Ot!==null&&(Ot=setTimeout(e,ck))};Ot=setTimeout(e,0)}function Od(){Ot&&(clearTimeout(Ot),Ot=null)}const fk=1e4;let Dt=null,jo=0;async function Yi(){const e=++jo;try{const t=await fetch("/api/reviews");if(!t.ok)return;const n=await t.json();e===jo&&(mt.value=n.rows||[])}catch{}}function pk(){if(Dt)return;const e=async()=>{await Yi(),Dt!==null&&(Dt=setTimeout(e,fk))};Dt=setTimeout(e,0)}function mk(){Dt&&(clearTimeout(Dt),Dt=null)}function Dd(){const e=location.protocol==="https:"?"wss:":"ws:",t=new WebSocket(`${e}//${location.host}/ws/state`);Yn=t,t.onmessage=n=>{Od();try{Md(JSON.parse(n.data)),Ld()}catch(i){Id(`bad frame: ${i.message}`)}},t.onclose=()=>{Xl||(Yn=null,hk(),fn||(fn=setTimeout(()=>{fn=null,Dd()},uk)))},t.onerror=()=>{try{t.close()}catch{}}}function gk(){return kl?()=>{}:(kl=!0,Xl=!1,Dd(),pk(),()=>{if(Xl=!0,mk(),kl=!1,Yn){try{Yn.close()}catch{}Yn=null}Od(),fn&&(clearTimeout(fn),fn=null)})}const yk=/[A-Za-z0-9_]+|\s+|[^A-Za-z0-9_\s]/g;function Ho(e){return String(e??"").match(yk)||[]}function bk(e,t){let n=0;for(;n{const b=[],x=(w,C)=>{if(!w)return;const y=b[b.length-1];y&&y.changed===C?y.text+=w:b.push({text:w,changed:C})};return x(p.slice(0,h).join(""),!1),x(p.slice(h,m).join(""),!0),x(p.slice(m).join(""),!1),b},f=c(i,a,i.length-s),d=c(l,a,l.length-s);return!f.some(p=>p.changed)&&!d.some(p=>p.changed)?null:{del:f,add:d}}function vk(e,t=.5){const n=e.map(l=>({...l}));let i=0;for(;i0&&s===o)for(let u=0;ui?a:i+1}return n}const _k=["dist/","build/","vendor/",".next/","target/debug/","target/release/"],xk=new Set(["package-lock.json","yarn.lock","pnpm-lock.yaml","uv.lock","poetry.lock","Cargo.lock","Gemfile.lock","composer.lock","go.sum"]),Sk=[".min.js",".min.css",".map",".snap"];function Rd(e){const t=String(e||"");if(!t)return!1;const n=t.split("/").pop()||"";if(xk.has(n)||Sk.some(l=>n.endsWith(l)))return!0;const i=`${t}/`;return _k.some(l=>i.includes(`/${l}`)||i.startsWith(l))}function $k(e){const t=[],n=[];for(const i of e||[])(Rd(i.path)?n:t).push(i);return t.concat(n)}const ea=H({}),Ck="\0";function Fd(e,t){return`${e}${Ck}${t}`}function Nd(e,t,n,i,l=!1){const a=e[Fd(t,n)];if(!a)return{collapsed:l,viewed:!1};const s=!!a.viewedSig&&a.viewedSig===i;return{collapsed:a.collapsed===void 0?l:a.collapsed,viewed:s}}function Bd(e,t,n){const i=Fd(e,t),l={...ea.value},a={...l[i]||{},...n};a.collapsed===void 0&&!a.viewedSig?delete l[i]:l[i]=a,ea.value=l}function Tk(e,t,n){Bd(e,t,{collapsed:n})}function Ek(e,t,n,i){Bd(e,t,{viewedSig:i?n:void 0,collapsed:!!i})}function Pk(e,t,n){return n.reduce((i,l)=>i+(Nd(e,t,l.path,l.sig).viewed?1:0),0)}const Ak=[["session","This session","Changes since the current Claude session started"],["branch","This branch","Changes vs where this branch forked"]],Mk={js:"js",mjs:"js",cjs:"js",jsx:"jsx",ts:"ts",tsx:"tsx",py:"py",rs:"rs",json:"json",css:"css",html:"html",htm:"html",md:"md",markdown:"md"};function zd(e){const t=(e||"").split("/").pop()||"",n=t.lastIndexOf(".");return n>0&&Mk[t.slice(n+1).toLowerCase()]||""}function qo({text:e,lang:t,hl:n}){if(!n||!t)return e||" ";try{return n(e,t)}catch{return e||" "}}function Lk({line:e,lang:t,hl:n}){return r("div",{class:`dl dl-${e.kind}`,children:r("span",{class:"dl-text",children:e.segs?e.segs.map((i,l)=>r("span",{class:i.changed?"dl-intra":"",children:r(qo,{text:i.text,lang:t,hl:n})},l)):r(qo,{text:e.text,lang:t,hl:n})})})}function jd({hunk:e,lang:t,hl:n,onOpen:i,title:l}){return r("div",{class:"dhunk",children:[r("button",{type:"button",class:"dhunk-head",disabled:!i,title:i?l:void 0,onClick:()=>i==null?void 0:i(e.new_start),children:[r("span",{class:"dhunk-range",children:["@@ ",e.new_start]}),e.header?r("span",{class:"dhunk-sym",children:e.header}):null]}),vk(e.lines).map((a,s)=>r(Lk,{line:a,lang:t,hl:n},s))]})}function Ik({file:e,repo:t,review:n,hl:i}){const l=Rd(e.path),{collapsed:a,viewed:s}=Nd(n,t,e.path,e.sig,l),o=!a,u=zd(e.path),c=t&&e.status!=="deleted"&&e.status!=="binary",f=(e.additions||0)+(e.deletions||0);return r("div",{class:`dfile dfile-${e.status}${s?" is-viewed":""}${l?" is-generated":""}`,children:[r("div",{class:"dfile-head",children:[r("button",{type:"button",class:"dfile-toggle",onClick:()=>Tk(t,e.path,o),children:[r("span",{class:"dfile-caret",children:o?"▾":"▸"}),r("span",{class:"dfile-path",children:e.path}),l?r("span",{class:"dfile-gen",title:"Generated artifact — folded by default and sorted last",children:"generated"}):null,r("span",{class:"dfile-status",children:e.status}),r("span",{class:"dfile-stat",children:[e.additions?r("span",{class:"dstat-add",children:["+",e.additions]}):null,e.deletions?r("span",{class:"dstat-del",children:["−",e.deletions]}):null,!f&&e.status==="binary"?r("span",{class:"dstat-bin",children:"binary"}):null]})]}),r("button",{type:"button",class:`dfile-viewed${s?" is-on":""}`,title:s?"Marked viewed — re-surfaces automatically if this file changes again":"Mark viewed (folds it away until it changes again)",onClick:()=>Ek(t,e.path,e.sig,!s),children:s?"✓ viewed":"viewed"})]}),o&&e.hunks.map((d,p)=>r(jd,{hunk:d,lang:u,hl:i,onOpen:c?h=>gr({path:`${t}/${e.path}`,line:h}):null,title:`Open ${e.path}:${d.new_start}`},p)),o&&e.truncated?r("div",{class:"dfile-trunc",children:"diff truncated — open the file to see the rest"}):null]})}const Ok=[3,10,25];function Dk({target:e,active:t,gitSig:n}){var x,w,C;const[i,l]=B("session"),[a,s]=B(3),[o,u]=B({loading:!0,error:null,data:null}),[c,f]=B(null);j(()=>{if(!t||c)return;let y=!0;return Ji(()=>import("./chunks/preview.js").then(g=>g.h),[]).then(g=>{y&&f(()=>g.highlightCode)}).catch(()=>{}),()=>{y=!1}},[t,c]),j(()=>{if(!t||!e)return;let y=!0;return(async()=>{try{const g=await fetch(`/api/fs/diff?${nt(e)}&scope=${i}&context=${a}`);if(!y)return;if(!g.ok){const $=await g.json().catch(()=>({}));u({loading:!1,error:$.detail||`HTTP ${g.status}`,data:null});return}u({loading:!1,error:null,data:await g.json()})}catch(g){y&&u({loading:!1,error:String(g),data:null})}})(),()=>{y=!1}},[e,i,t,n,a]);const d=$k(((x=o.data)==null?void 0:x.files)||[]),p=((w=o.data)==null?void 0:w.repo)||"",h=ea.value,m=Pk(h,p,d),b=d.reduce((y,g)=>({a:y.a+(g.additions||0),d:y.d+(g.deletions||0)}),{a:0,d:0});return r("div",{class:"changes-tab",children:[r("header",{class:"changes-head",children:[r("span",{class:"changes-scope",children:Ak.map(([y,g,$])=>r("button",{type:"button",class:i===y?"is-active":"",title:$,onClick:()=>l(y),children:g},y))}),r("span",{class:"changes-ctx",title:"Lines of context around each change",children:Ok.map(y=>r("button",{type:"button",class:a===y?"is-active":"",onClick:()=>s(y),children:y},y))}),!o.loading&&!o.error&&r("span",{class:"changes-summary",children:d.length?r(V,{children:[m?`${m}/${d.length} viewed · `:`${d.length} file${d.length===1?"":"s"} · `,r("span",{class:"dstat-add",children:["+",b.a]})," ",r("span",{class:"dstat-del",children:["−",b.d]})]}):"no changes"})]}),r("div",{class:"changes-body",children:[o.loading?r("div",{class:"changes-msg",children:"loading…"}):null,o.error?r("div",{class:"changes-msg changes-err",children:o.error}):null,!o.loading&&!o.error&&!d.length?r("div",{class:"changes-msg",children:i==="session"?"Nothing changed on disk since this session started.":"This branch matches its fork point."}):null,d.map(y=>r(Ik,{file:y,repo:p,review:h,hl:c},y.path)),(C=o.data)!=null&&C.truncated?r("div",{class:"changes-msg",children:[o.data.truncated," more file",o.data.truncated===1?"":"s"," not shown"]}):null]})]})}const Rk=new Set(["Read","Edit","Write","MultiEdit","NotebookEdit"]),Fk=new Set(["html","md"]);function Nk(e,t){const n=(t||"").trim();if(!n)return!0;const i=(e||"").toLowerCase();if(!/[*?]/.test(n))return i.includes(n.toLowerCase());const l=n.split("**").map(a=>a.replace(/[.+^${}()|[\]\\]/g,"\\$&").replace(/\*/g,"[^/]*").replace(/\?/g,"[^/]")).join(".*");return new RegExp(`(?:^|/)${l}$`,"i").test(i)}function Bk(e){const t=(e||"").split("/").pop()||"",n=t.lastIndexOf(".");return n>0?t.slice(n+1).toLowerCase():""}function zk(e,t=Fk){const n=[],i=[];for(const l of e)(t.has(Bk(l.path))?n:i).push(l);return{priority:n,others:i}}function jk(e){if(!Array.isArray(e)||e.length===0)return[];const t=new Map;for(let n=e.length-1;n>=0;n--){const i=e[n],l=i==null?void 0:i.tool_uses;if(l!=null&&l.length)for(let a=l.length-1;a>=0;a--){const s=l[a];if(!s||!Rk.has(s.name))continue;const o=s.input&&(s.input.file_path||s.input.notebook_path);o&&(t.has(o)||t.set(o,s.name))}}return[...t.entries()].map(([n,i])=>({path:n,op:i}))}function Hk(e){return e==="need_human"?"var(--s-danger)":e==="done"?"var(--s-success)":"var(--fg-3)"}function qk(e,t){return e==="commit"?"var(--s-shell)":e==="ci"?t==="failed"?"var(--s-danger)":t==="running"?"var(--s-working)":"var(--s-success)":e==="open"?"var(--fg-3)":e==="reset"?"var(--s-working)":e==="rename"?"var(--accent)":"var(--fg-3)"}function Uk(e,t){return e==="commit"?"commit":e==="ci"?t?`ci ${t}`:"ci":e==="open"?"opened":e==="reset"?t==="compacted"?"compacted":"cleared":e==="rename"?"renamed":e}function Wk(e){return e?(e.replace(/[^A-Za-z0-9]/g,"").slice(0,2)||e.slice(0,1)).toUpperCase():"?"}const Kk={pr:"Please link the PR you're working on for this pane using the `link_pr` MCP tool. If there isn't one, ignore this.",linear:"Please link the relevant Linear ticket for this pane using the `link_linear` MCP tool. If there isn't one, ignore this."};function Vk({data:e,onLinkAsk:t}){if(!e.pr){if(e.agent!=="claude")return null;const o=!!e.channel_attached;return r("button",{class:"modal-side-link-btn",type:"button",disabled:!o,title:o?"Ask Claude to link the PR for this pane (via link_pr MCP tool)":"Channel not attached. Respawn Claude via + claude.",onClick:()=>t("pr"),children:"+ link pull request"})}const n=e.ci==="✓"?"passing":e.ci==="✗"?"failing":e.ci==="⟳"?"running":"—",i=e.ci==="✓"?"ci-passing":e.ci==="✗"?"ci-failing":e.ci==="⟳"?"ci-running":"",l=rr(e.repo_slug,e.pr),a=e.pr_additions||0,s=e.pr_deletions||0;return r("div",{class:"modal-card-inset",children:[r("div",{class:"pr-head",children:[l?r("a",{class:"pr-num",href:l,target:"_blank",rel:"noopener",onClick:o=>o.stopPropagation(),children:["#",e.pr]}):r("span",{class:"pr-num",children:["#",e.pr]}),r("span",{class:"pr-title",title:e.pr_title||"",children:e.pr_title||""})]}),r("div",{class:"pr-meta",children:[e.pr_draft?r("span",{class:"pr-mini pr-mini-draft",children:"draft"}):r("span",{class:"pr-mini pr-mini-open",children:"open"}),r("span",{class:"pr-diff",children:[r("span",{class:"diff-plus",children:["+",a]})," ",r("span",{class:"diff-minus",children:["−",s]})]}),r("span",{class:`pr-ci ${i}`,children:[r("span",{class:"ci-dot"}),"ci ",n]}),(e.pr_reviewers||[]).length>0&&r("span",{class:"pr-reviewers",children:(e.pr_reviewers||[]).map(o=>r("span",{class:"modal-avatar",title:o,children:Wk(o)}))})]})]})}function Gk({data:e,onLinkAsk:t}){if(e.linked_linear){const s=e.linked_linear,o=`https://linear.app/issue/${s}`,u=e.linked_linear_title||"linear ticket";return r("div",{class:"modal-card-inset",children:[r("div",{class:"pr-head",children:[r("a",{class:"pr-num",href:o,target:"_blank",rel:"noopener",onClick:c=>c.stopPropagation(),title:`Linear ticket ${s} (linked by claude)`,children:s}),r("span",{class:"pr-title",title:u,children:u})]}),e.linked_linear_status&&r("div",{class:"pr-meta",children:r("span",{class:"pr-mini",children:e.linked_linear_status})})]})}const n=br(e),i=n&&r("div",{class:"modal-card-inset",children:r("div",{class:"pr-head",children:[r("a",{class:"pr-num",href:n.href,target:"_blank",rel:"noopener",onClick:s=>s.stopPropagation(),title:n.tip,children:n.id}),r("span",{class:"pr-title",children:n.note})]})});if(e.agent!=="claude")return i||null;const l=!!e.channel_attached;return r(V,{children:[i,r("button",{class:"modal-side-link-btn",type:"button",disabled:!l,title:l?"Ask Claude to link a Linear ticket for this pane (via link_linear MCP tool)":"Channel not attached. Respawn Claude via + claude.",onClick:()=>t("linear"),children:"+ link Linear ticket"})]})}function Jk({e}){return e.src==="alert"?r("li",{class:"timeline-row timeline-row-alert","data-kind":e.kind,children:[r("span",{class:"timeline-dot",style:`background:${Hk(e.kind)}`}),r("div",{class:"timeline-body",children:[r("div",{class:"timeline-text",children:e.text}),r("div",{class:"timeline-when",children:["claude · ",e.kind," · ",Pe(e.at)," ago"]})]})]}):e.src==="channel"?r("li",{class:"timeline-row timeline-row-channel","data-kind":e.kind,children:[r("span",{class:"timeline-dot",style:"background:var(--accent)"}),r("div",{class:"timeline-body",children:[r("div",{class:"timeline-text",children:e.text}),r("div",{class:"timeline-when",children:["periscope · ",e.kind," · ",Pe(e.at)," ago"]})]})]}):r("li",{class:"timeline-row","data-kind":e.kind,children:[r("span",{class:"timeline-dot",style:`background:${qk(e.kind,e.state)}`}),r("div",{class:"timeline-body",children:[e.url?r("a",{class:"timeline-text timeline-link",href:e.url,target:"_blank",rel:"noopener",children:e.text}):r("div",{class:"timeline-text",children:e.text}),r("div",{class:"timeline-when",children:[Uk(e.kind,e.state)," · ",Pe(e.at)," ago"]})]})]})}function Qk({data:e,streamRef:t}){const n=e.activity||[],i=n.filter(s=>s.src==="alert").reduce((s,o)=>s&&s.at>=o.at?s:o,null),l=i&&i.kind==="need_human"?i:null,a=n.filter(s=>s!==l);return r(V,{children:[l&&r("div",{class:"activity-pinned",children:[r("div",{class:"activity-pinned-label",children:["needs you · ",Pe(l.at)," ago"]}),r("div",{class:"activity-pinned-text",children:l.text})]}),a.length?r("ol",{class:"timeline activity-stream",ref:t,children:a.map((s,o)=>r(Jk,{e:s},o))}):l?null:r("div",{class:"timeline-empty",children:"no recent activity"})]})}function Yk({pid:e,onRefresh:t,idPrefix:n}){const i=ne(null),l=ne(null),a=ne(null),s=e?Cr(e):null,o=(s==null?void 0:s.tags)||[];function u(){if(!e)return;const d=Cr(e)||{tags:[]};qr(e,{notes:i.current.value,tags:d.tags})}function c(){const d=(l.current.value||"").trim();if(!d||!e)return;const p=Cr(e)||{notes:i.current.value,tags:[]},h=d.split(/[\s,]+/).filter(Boolean);qr(e,{notes:i.current.value,tags:[...p.tags,...h]}),l.current.value="",t==null||t()}function f(d){const p=Cr(e)||{notes:i.current.value,tags:[]};qr(e,{notes:i.current.value,tags:p.tags.filter((h,m)=>m!==d)}),t==null||t()}return r(V,{children:[r("textarea",{ref:i,id:`${n}-notes`,class:"modal-notes",placeholder:e?"Notes — saves on blur":"Notes unavailable (no pid)",disabled:!e,defaultValue:(s==null?void 0:s.notes)||"",onInput:()=>{clearTimeout(a.current),a.current=setTimeout(u,600)},onBlur:()=>{clearTimeout(a.current),u()},onKeyDown:d=>d.stopPropagation()}),r("div",{class:"tag-row",children:[r("div",{class:"tag-chips",id:`${n}-tags`,children:o.map((d,p)=>r("span",{class:"tag-chip","data-tag-i":p,children:[d,r("button",{class:"tag-chip-x",title:"remove",onClick:()=>f(p),children:"×"})]}))}),r("input",{ref:l,id:`${n}-tag-input`,class:"modal-tag-input",type:"text",placeholder:"add tag, Enter or comma",disabled:!e,onKeyDown:d=>{d.stopPropagation(),(d.key==="Enter"||d.key===",")&&(d.preventDefault(),c())}})]})]})}function wl({it:e,label:t,priority:n,pinned:i,onTogglePin:l}){return r("li",{class:`files-row${n?" files-row-priority":""}${i?" files-row-pinned":""}`,onClick:()=>gr({path:e.path,line:null}),title:`Open ${e.path} as a preview tab`,children:[r("span",{class:"files-op",children:Xk(e.op)}),r("span",{class:"files-path",children:t}),r("button",{type:"button",class:`files-pin${i?" files-pin-on":""}`,title:i?"Unpin file":"Pin file","aria-label":i?"Unpin file":"Pin file","aria-pressed":i?"true":"false",onClick:a=>{a.stopPropagation(),l()},children:i?"★":"☆"})]})}function Zk({pid:e}){me.value;const[t,n]=B("");if(!e)return null;const l=Fr.value[e]?Ft.value[e]:null,a=l!=null&&l.messages?jk(l.messages):[],s=uu(e);if(!a.length&&!s.length)return null;const o=new Map(a.map(g=>[g.path,g])),u=s.map(g=>o.get(g)||{path:g,op:null}),c=new Set(s),f=a.filter(g=>!c.has(g.path)),d=[...s,...f.map(g=>g.path)],p=g=>ou(g,d),h=t.trim(),m=g=>Nk(g.path,h),b=u.filter(m),{priority:x,others:w}=zk(f.filter(m)),C=g=>()=>Ig(e,g),y=[{items:b,render:g=>r(wl,{it:g,label:p(g.path),pinned:!0,onTogglePin:C(g.path)},`pin:${g.path}`)},{items:x,render:g=>r(wl,{it:g,label:p(g.path),priority:!0,pinned:!1,onTogglePin:C(g.path)},`pri:${g.path}`)},{items:w,render:g=>r(wl,{it:g,label:p(g.path),pinned:!1,onTogglePin:C(g.path)},`oth:${g.path}`)}].filter(g=>g.items.length>0);return r("section",{class:"modal-side-section modal-side-files",children:[r("h4",{children:"Files"}),r("input",{class:"files-filter",type:"text",placeholder:"filter files — text or glob",value:t,onInput:g=>n(g.currentTarget.value),onKeyDown:g=>{g.stopPropagation(),g.key==="Escape"&&(g.preventDefault(),n(""))}}),r("ul",{class:"files-list",children:[y.map((g,$)=>r(V,{children:[$>0&&r("li",{class:"files-divider","aria-hidden":"true"}),g.items.map(g.render)]})),h&&!y.length&&r("li",{class:"files-nomatch",children:"no files match"})]})]})}function Xk(e){switch(e){case"Read":return"👁";case"Write":return"+";case"Edit":case"MultiEdit":case"NotebookEdit":return"✎";default:return"·"}}function ew({data:e,onRefresh:t,containerId:n,containerClass:i,idPrefix:l}){const a=ne(null),s=ne(0),o=(e==null?void 0:e.pid)||"";j(()=>{a.current&&s.current&&(a.current.scrollTop=s.current)});function u(c){const f=Kk[c];!f||!(e!=null&&e.pane_id)||fetch(`/api/channel/push?pane_id=${encodeURIComponent(e.pane_id)}`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({content:f})}).catch(()=>{})}return a.current&&(s.current=a.current.scrollTop),r("aside",{id:n,class:i,children:[r("section",{class:"modal-side-section",children:[r("h4",{children:"Linked"}),r(Vk,{data:e,onLinkAsk:u}),r(Gk,{data:e,onLinkAsk:u})]}),r("section",{class:"modal-side-section modal-side-notes",children:[r("h4",{children:"Notes"}),r(Yk,{pid:o,idPrefix:l,onRefresh:t},o)]}),r(Zk,{pid:o}),r("section",{class:"modal-side-section modal-side-activity",children:[r("h4",{children:"Activity"}),r(Qk,{data:e,streamRef:a})]})]})}let pn=null,Ir=null;function tw(){return pn?Promise.resolve(pn):(Ir||(Ir=Ji(()=>import("./chunks/preview.js").then(e=>e.P),[]).then(e=>(pn=e,e)).catch(e=>{throw Ir=null,e})),Ir)}function nw({entry:e,active:t=!0}){const[n,i]=B(()=>(pn==null?void 0:pn.PreviewTabInner)||null),[l,a]=B(null);return j(()=>{if(n)return;let s=!0;return a(null),tw().then(o=>{s&&i(()=>o.PreviewTabInner)}).catch(o=>{s&&a((o==null?void 0:o.message)||String(o))}),()=>{s=!1}},[n]),l?r("div",{class:"preview-tab-content",role:"region","aria-label":"File preview",children:[r("header",{class:"preview-header",children:r("span",{class:"preview-path",children:e.path})}),r("div",{class:"preview-body",children:r("div",{class:"preview-error",children:[r("div",{children:"Failed to load preview module:"}),r("div",{class:"preview-error-code",children:l})]})})]}):n?r(n,{entry:e,active:t}):r("div",{class:"preview-tab-content",role:"region","aria-label":"File preview",children:[r("header",{class:"preview-header",children:r("span",{class:"preview-path",children:[e.path,e.line?`:${e.line}`:""]})}),r("div",{class:"preview-body",children:r("div",{class:"preview-loading",children:"loading…"})})]})}async function vl(e,t,n,i){i(!0);try{const l=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({repo:t.repo,branch:t.branch})}),a=await l.json().catch(()=>({}));n(l.ok?a.mode==="spawned"?a.checked_out?"checked out the PR and spawned a pane to prepare":"spawned a pane to prepare":"sent":a.detail||`HTTP ${l.status}`),Yi()}catch(l){n(String(l))}finally{i(!1)}}function rw({row:e}){const t=br(e),n=t&&r("a",{class:"rq-linear",href:t.href,target:"_blank",rel:"noopener",title:t.tip,children:t.id});if(!e.pr)return n||"—";const i=ji(e.pr.state),l=`rq-pr-link ${i.cls}`;return r(V,{children:[e.pr.url?r("a",{class:l,href:e.pr.url,target:"_blank",rel:"noopener",children:["#",e.pr.number]}):r("span",{class:l,children:["#",e.pr.number]}),n?r(V,{children:[" · ",n]}):null,e.pr.draft?" draft":""," ",e.pr.state||""," ",e.pr.ci&&!i.cls?e.pr.ci:"",r("div",{class:"rq-repo",children:e.pr.title})]})}const Uo="filter branches, PRs, people, tickets";function iw({col:e}){const{label:t,what:n}=Bi[e],i=Ll.value,l=(i==null?void 0:i.key)===e,a=lg(i,e);return r("th",{"aria-sort":l?i.dir==="asc"?"ascending":"descending":void 0,children:r("button",{type:"button",class:`rq-sort${l?" is-active":""}`,title:a?l?"reverse the order":`sort by ${n}`:"back to the default order",onClick:()=>{Ll.value=a},children:[t,l?r("span",{class:"rq-sort-dir","aria-hidden":"true",children:i.dir==="asc"?"▲":"▼"}):null]})})}function lw({row:e}){var f,d,p;const[t,n]=B(""),[i,l]=B(!1),a=((f=e.pr)==null?void 0:f.state)==="merged",s=!!e.owner||!!e.worktree||((d=e.pr)==null?void 0:d.state)==="open",o=e.walkthrough,u=e.owner?"ask the owning pane to write the walkthrough":e.worktree?"spawn a pane in the worktree to write the walkthrough":s?`check out PR #${e.pr.number} and spawn a pane to write the walkthrough`:"open the branch first (⌘K)",c=e.unresolved>0?`${e.unresolved} unresolved comment${e.unresolved===1?"":"s"}`:e.owner?"ask the owning pane to merge":"no pane on this branch";return r("tr",{class:`rq-row rq-${e.role}${a?" is-merged":""}`,children:[r("td",{class:"rq-branch",children:[o?r("button",{type:"button",class:"rq-open",onClick:()=>Su(e.repo,e.branch),children:e.branch}):e.branch,r("div",{class:"rq-repo",children:[e.repo," · ",e.role==="review-requested"?"review requested":e.role]})]}),r("td",{class:"rq-pr",children:r(rw,{row:e})}),r("td",{class:"rq-wt",children:o?`${o.stops} stop${o.stops===1?"":"s"} · ${ng(e)}`:"none"}),r("td",{class:"rq-comments",children:e.unresolved?r("b",{children:[e.unresolved," open"]}):"—"}),r("td",{class:"rq-owner",children:[e.owner?e.owner.name:(p=e.pr)!=null&&p.author?`opened by ${e.pr.author}`:"—",e.owner?null:r("div",{class:"rq-repo",children:e.worktree?"no pane":"not checked out"})]}),r("td",{class:"rq-actions",children:[r("button",{type:"button",disabled:i||!s,title:u,onClick:()=>vl("/api/reviews/prepare",e,n,l),children:o?"re-prepare":"prepare"}),r("button",{type:"button",disabled:i||!e.owner||a||e.unresolved>0,title:c,onClick:async()=>{await Jn(`Ask the owning pane to merge ${e.branch}?`,{okLabel:"Merge"})&&vl("/api/reviews/merge",e,n,l)},children:"merge"}),(a||!e.worktree)&&(o||e.unresolved)?r("button",{type:"button",disabled:i,title:"remove this review's files",onClick:async()=>{await Jn(`Remove the walkthrough and comments for ${e.branch}? This can't be undone.`,{okLabel:"Clear",danger:!0})&&vl("/api/reviews/clear",e,n,l)},children:"clear"}):null,t?r("span",{class:"rq-note",children:t}):null]})]})}function aw(){j(()=>{Yi()},[]);const e=mt.value;if(e===null)return r("div",{class:"wt-empty",children:"loading…"});const t=ig(e,Ll.value),n=ds.value,i=ag(t,n),l=`branch${t.length===1?"":"es"}`;return r("div",{class:"reviews-queue",children:[r("header",{class:"wt-head",children:[r("h2",{children:"Reviews"}),r("input",{type:"search",class:"rq-search",placeholder:Uo,"aria-label":Uo,spellcheck:!1,autocomplete:"off",value:n,onInput:a=>{ds.value=a.currentTarget.value}}),r("span",{class:"wt-sub","aria-live":"polite",children:n.trim()?`${i.length} of ${t.length} ${l}`:`${t.length} ${l}`})]}),t.length===0?r("div",{class:"wt-empty",children:"nothing open"}):i.length===0?r("div",{class:"wt-empty",children:['no branches match "',n.trim(),'"']}):r("table",{class:"rq-table",children:[r("thead",{children:r("tr",{children:[Object.keys(Bi).map(a=>r(iw,{col:a},a)),r("th",{})]})}),r("tbody",{children:i.map(a=>r(lw,{row:a},`${a.repo}/${a.branch}`))})]})]})}async function sw(e,t){const n=await fetch(e,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t)}),i=await n.json().catch(()=>({}));return{ok:n.ok,status:n.status,data:i}}function Hd({list:e}){return e.length?r("ul",{class:"wt-comments",children:e.map((t,n)=>r("li",{class:t.resolved?"is-resolved":"",children:[r("span",{class:"wt-comment-ts",children:t.ts}),r("span",{class:"wt-comment-state",children:t.resolved?"resolved":"open"}),r("div",{class:"wt-comment-body",children:fr(t.body,{demote:!1,softBreaks:"space"})})]},n))}):null}function ow({repo:e,branch:t,stop:n,onPosted:i}){const[l,a]=B(""),[s,o]=B(""),[u,c]=B(!1);async function f(){if(!(u||!l.trim())){c(!0);try{const d=await sw("/api/reviews/comment",{repo:e,branch:t,stop:n,body:l});d.ok?(a(""),o("sent to the owning pane")):d.status===409?(a(""),o(`saved, ${d.data.detail||"not delivered"}`)):o(d.data.detail||`HTTP ${d.status}`),i()}catch(d){o(String(d))}finally{c(!1)}}}return r("div",{class:"wt-comment-box",children:[r("textarea",{rows:2,value:l,placeholder:"comment on this stop…",onInput:d=>a(d.currentTarget.value),onKeyDown:d=>{d.key==="Enter"&&(d.metaKey||d.ctrlKey)&&f()}}),r("div",{class:"wt-comment-actions",children:[r("button",{type:"button",disabled:u||!l.trim(),onClick:f,children:"comment"}),s?r("span",{class:"wt-comment-note",children:s}):null]})]})}function cw({stop:e,diffFiles:t,comments:n,repo:i,branch:l,hl:a,onPosted:s}){const[o,u]=B(!1),c=tg(e.refs,t||[]),f=c.reduce((p,h)=>p+h.hunks.length,0),d=p=>{const h=c.find(m=>m.ref.raw===p.raw&&m.hunks.length);return r("button",{type:"button",class:`md-cite${h?"":" is-inert"}`,title:h?"show hunks":"no hunk in this branch's diff",onClick:()=>h&&u(!0),children:p.raw})};return r("section",{class:"wt-stop",id:`stop-${e.id}`,children:[r("h3",{class:"wt-stop-title",children:[e.kind?r("span",{class:`wt-kind wt-kind-${e.knownKind?e.kind:"other"}`,children:e.kind}):null,e.title]}),e.refs.length?r("div",{class:"md-meta",children:r("span",{class:"md-meta-kv","data-key":"refs",children:[r("span",{class:"md-meta-k",children:"refs"}),r("span",{class:"md-meta-v",children:e.refs.map((p,h)=>r("span",{children:[h?", ":"",d(p)]},h))})]})}):null,r("div",{class:"md-doc wt-stop-body",children:fr(e.bodyMarkdown,{demote:!1,softBreaks:"space",mermaid:!0,meta:!0,cite:d})}),t&&e.refs.length?r("div",{class:"wt-evidence",children:[r("button",{type:"button",class:"wt-evidence-toggle",onClick:()=>u(!o),children:[o?"▾":"▸"," ",f," hunk",f===1?"":"s"]}),o&&c.map((p,h)=>r("div",{class:"wt-evidence-file",children:[r("div",{class:"wt-evidence-path",children:[p.ref.raw,p.file?p.generated?" — generated, not shown":"":" — not in this diff"]}),p.hunks.map((m,b)=>r(jd,{hunk:m,lang:zd(p.ref.path),hl:a,onOpen:null},b))]},h))]}):null,r(Hd,{list:n}),r(ow,{repo:i,branch:l,stop:e.title,onPosted:s})]})}function uw({repo:e,branch:t,pr:n,sig:i}){var y;const[l,a]=B({loading:!0,error:null,data:null}),[s,o]=B(null),[u,c]=B(0);j(()=>{let g=!0;return Ji(()=>import("./chunks/preview.js").then($=>$.h),[]).then($=>{g&&o(()=>$.highlightCode)}).catch(()=>{}),()=>{g=!1}},[]),j(()=>{let g=!0;return(async()=>{try{const $=await fetch(`/api/reviews/walkthrough?repo=${encodeURIComponent(e)}&branch=${encodeURIComponent(t)}`),v=await $.json().catch(()=>({}));if(!g)return;if(!$.ok){a(E=>({loading:!1,error:v.detail||`HTTP ${$.status}`,data:E.data}));return}a({loading:!1,error:null,data:v})}catch($){g&&a(v=>({loading:!1,error:String($),data:v.data}))}})(),()=>{g=!1}},[e,t,u,i]);const f=()=>{c(g=>g+1),Yi()};if(l.loading)return r("div",{class:"wt-empty",children:"loading…"});if(!l.data)return r("div",{class:"wt-empty",children:l.error});const{text:d,comments:p,diff:h,stale_by:m}=l.data,b=Xm(d),x=new Map;for(const g of p)(x.get(g.stop)||x.set(g.stop,[]).get(g.stop)).push(g);const w=new Set(b.stops.map(g=>g.title)),C=p.filter(g=>!w.has(g.stop));return r("div",{class:"walkthrough",children:[l.error?r("div",{class:"wt-empty wt-error",children:["refresh failed: ",l.error]}):null,r("header",{class:"wt-head",children:[r("h2",{children:b.title||t}),r("span",{class:"wt-sub",children:[e," · ",t,(y=b.header)!=null&&y.pr?r(V,{children:[" · ",n!=null&&n.url&&n.number===b.header.pr?r("a",{class:`rq-pr-link ${ji(n.state).cls}`,href:n.url,target:"_blank",rel:"noopener",children:["PR #",b.header.pr]}):`PR #${b.header.pr}`]}):"",m==null&&b.header?h?"":" · no worktree — hunks unavailable":m>0?` · stale: ${m} commit${m===1?"":"s"} since`:""]})]}),b.problem?r("p",{class:"wt-problem",children:b.problem}):null,b.stops.map(g=>r(cw,{stop:g,diffFiles:(h==null?void 0:h.files)||null,comments:x.get(g.title)||[],repo:e,branch:t,hl:s,onPosted:f},g.id)),C.length?r("section",{class:"wt-stop",children:[r("h3",{class:"wt-stop-title",children:"unattached comments"}),r(Hd,{list:C})]}):null]})}function dw({target:e,agent:t,onPaste:n,class:i="modal-xterm",id:l}){const a=ne(null);return j(()=>(z1(a.current,e,{agent:t,onPaste:n}),Fu),[]),r("div",{ref:a,id:l,class:i})}function hw(){const e=ne(null),[t,n]=B(!1),[i,l]=B("");j(()=>{function o(u){(u.metaKey||u.ctrlKey)&&u.key==="f"&&(u.preventDefault(),n(!0),requestAnimationFrame(()=>{var c;return(c=e.current)==null?void 0:c.focus()}))}return window.addEventListener("keydown",o),()=>window.removeEventListener("keydown",o)},[]);const a=Pi(()=>{n(!1),l(""),B1()},[]);He(a,t);function s(o){o.preventDefault(),i&&(o.shiftKey?Ks(i):Ws(i))}return t?r("form",{class:"term-search",onSubmit:s,children:[r("input",{ref:e,class:"term-search-input",value:i,placeholder:"find in terminal",onInput:o=>{l(o.currentTarget.value)}}),r("button",{type:"button",class:"term-search-btn",title:"Previous (Shift+Enter)",onClick:()=>i&&Ks(i),children:"‹"}),r("button",{type:"button",class:"term-search-btn",title:"Next (Enter)",onClick:()=>i&&Ws(i),children:"›"}),r("button",{type:"button",class:"term-search-btn",title:"Close (Esc)",onClick:a,children:"✕"})]}):null}const qd={rail:{pref:"rail_width",cssVar:"--rail-w",columnId:"rail",def:320,min:180,dir:1},side:{pref:"side_width",cssVar:"--side-w",columnId:"detail-side",def:280,min:200,dir:-1}},fw=.5;function pw(e,t,n){return Math.round(Math.min(Math.max(e,t),Math.max(t,n)))}function mw(e,t,n,i){return pw(e+n.dir*t,n.min,i*fw)}function gw(e){return Number.isFinite(e)&&e>0?e:null}function Ud({which:e}){const t=qd[e];function n(i){if(i.button!==0)return;const l=i.currentTarget,a=document.getElementById("split-view"),s=document.getElementById(t.columnId),o=l.parentElement;if(!a||!s||!o)return;i.preventDefault();const u=i.clientX,c=Math.round(s.getBoundingClientRect().width),f=o.getBoundingClientRect().width;let d=c;l.setPointerCapture(i.pointerId),l.classList.add("dragging"),document.body.classList.add("col-resizing"),F1();const p=m=>{d=mw(c,m.clientX-u,t,f),a.style.setProperty(t.cssVar,`${d}px`)},h=()=>{l.removeEventListener("pointermove",p),l.removeEventListener("pointerup",h),l.removeEventListener("pointercancel",h),l.classList.remove("dragging"),document.body.classList.remove("col-resizing"),N1(),d!==c&&Ds(t.pref,d)};l.addEventListener("pointermove",p),l.addEventListener("pointerup",h),l.addEventListener("pointercancel",h)}return r("div",{class:`col-resize col-resize-${e}`,title:"Drag to resize · double-click to reset",onPointerDown:n,onDblClick:()=>Ds(t.pref,t.def)})}function yw(e){return(e==null?void 0:e.agent)!=="claude"?"not a Claude pane":e.state!=="idle"&&e.state!=="done"?`pane is ${e.state||"busy"}, try again when it's idle`:e.pending_input?"the pane has unsent text in its input box":null}function bw(e){return(e==null?void 0:e.session_effort)||(e==null?void 0:e.launch_effort)||"default"}function kw(e){var t;return e!=null&&e.model?e.model.replace(/\s*\(.*\)/,""):e!=null&&e.session_model?((t=sr.find(n=>n.id===e.session_model))==null?void 0:t.label)||e.session_model:"model"}function ww({w:e}){var c;const[t,n]=B({}),[i,l]=B(!1);j(()=>{n({})},[e.pid,e.session_model,e.session_effort]);const a=i?"waiting for the pane to confirm the last change":yw(e);async function s(f,d){const p=d.currentTarget.value;if(d.currentTarget.value="",!p||i)return;const h=`pid=${encodeURIComponent(e.pid)}&setting=${f}&value=${encodeURIComponent(p)}`;l(!0);let m;try{m=await ie(`set ${f}`,`/api/pane/session-setting?${h}`,{method:"POST"})}finally{l(!1)}m&&(m.result==="applied"?n(b=>({...b,[f]:p})):m.result==="confirm"?bn("Claude is asking to confirm the model switch in the pane — answer it there","info",7e3):bn(`Sent ${m.sent}, but Claude didn't confirm it took`,"info",5e3))}const o=t.model?((c=sr.find(f=>f.id===t.model))==null?void 0:c.label)||t.model:kw(e),u=t.effort||bw(e);return r("span",{class:"header-session-picks",children:[r("select",{class:"header-session-pick",value:"",disabled:!!a,title:a||"change this session's model (types /model into the pane)",onChange:f=>s("model",f),children:[r("option",{value:"",hidden:!0,children:o}),sr.map(f=>r("option",{value:f.id,children:f.label},f.id))]}),r("select",{class:"header-session-pick",value:"",disabled:!!a,title:a||"change this session's effort (types /effort into the pane)",onChange:f=>s("effort",f),children:[r("option",{value:"",hidden:!0,children:`effort ${u}`}),Oa.map(f=>r("option",{value:f,children:f},f))]})]})}const vw=2e3;function _w(e,t,n){j(()=>{if(!n||!e)return;let i=!0,l=null;async function a(){try{const o=await(await fetch(`/api/pane/turns?${nt(e)}`)).json();if(!i)return;const u=o&&o.turns===null?[]:o.messages||[],c=(o==null?void 0:o.session_id)||null;Ft.value={...Ft.value,[t]:{messages:u,sessionId:c}},u.length&&!Fr.value[t]&&(Fr.value={...Fr.value,[t]:!0})}catch{}i&&(l=setTimeout(a,vw))}return a(),()=>{i=!1,l&&clearTimeout(l)}},[e,t,n])}function xw(e){const t=e.input||{};switch(e.name){case"Bash":return t.command||"";case"Read":case"Edit":case"Write":case"MultiEdit":case"NotebookEdit":return t.file_path||t.notebook_path||"";case"Grep":return t.pattern||"";case"Glob":return t.pattern||"";case"Task":case"Agent":return t.description||"";case"TaskCreate":return t.subject||"";case"TaskUpdate":return t.taskId?`#${t.taskId}`:"";case"WebFetch":return t.url||"";case"WebSearch":return t.query||"";case"Skill":return t.skill||t.command||"";case"AskUserQuestion":{const n=t.questions||[];return n.length===1?n[0].question||n[0].header||"":n.length?`${n.length} questions`:""}default:{const n=JSON.stringify(t);return n==="{}"||n==="null"?"":n.length>120?`${n.slice(0,120)}…`:n}}}function Sw({input:e}){const t=String(e.old_string||"").split(` `),n=String(e.new_string||"").split(` `);return r("div",{class:"tc-diff",children:[t.map((i,l)=>r("div",{class:"tc-diff-del",children:i||" "},`d${l}`)),n.map((i,l)=>r("div",{class:"tc-diff-add",children:i||" "},`a${l}`))]})}function $w(e){const t=e.input||{};switch(e.name){case"Bash":return t.command||"";case"Write":return t.content||"";default:return""}}function Cw({input:e}){const t=e.questions||[];return r("div",{class:"tc-ask",children:t.map((n,i)=>r("div",{class:"tc-ask-q",children:[r("div",{class:"tc-ask-qhead",children:[n.header&&r("span",{class:"tc-ask-chip",children:n.header}),n.multiSelect&&r("span",{class:"tc-ask-multi",children:"multi-select"})]}),r("div",{class:"tc-ask-question",children:n.question}),r("div",{class:"tc-ask-options",children:(n.options||[]).map((l,a)=>r("div",{class:"tc-ask-opt",children:[r("span",{class:"tc-ask-opt-label",children:l.label}),l.description&&r("span",{class:"tc-ask-opt-desc",children:l.description})]},a))})]},i))})}function Tw({t:e}){var p;const[t,n]=B(!1),i=e.result==null,l=e.name==="Edit",a=e.name==="AskUserQuestion",s=e.name==="Agent"||e.name==="Task",o=$w(e),u=!i&&e.result!=null&&e.result!=="",c=l||a||!!o||u,f=xw(e),d=s&&((p=e.input)==null?void 0:p.subagent_type)||"";return r("div",{class:`tc${s?" tc-agent":""}`,children:[r("button",{class:`tc-head${c?" expandable":""}`,onClick:c?()=>n(!t):void 0,children:[r("span",{class:"tc-dot",children:s?"⚇":"⏺"}),r("span",{class:"tc-name",children:e.name}),d&&r("span",{class:"tc-agent-type",children:d}),f&&(e.name==="Read"||e.name==="Edit"||e.name==="Write"||e.name==="MultiEdit"||e.name==="NotebookEdit"?r("span",{class:"tc-arg tc-arg-clickable",title:"Cmd+click to preview",onClick:m=>{!m.metaKey&&!m.ctrlKey||(m.stopPropagation(),gr({path:f,line:null}))},children:f}):r("span",{class:"tc-arg",children:f})),i&&r("span",{class:"tc-running",children:"running…"}),c&&r("span",{class:"tc-caret",children:t?"▾":"▸"})]}),t&&r("div",{class:"tc-detail",children:[l?r(Sw,{input:e.input||{}}):a?r(Cw,{input:e.input||{}}):o&&r("pre",{class:"tc-cmd",children:o}),u&&r("pre",{class:"tc-out",children:e.result})]})]})}function Ew({m:e,current:t}){if(e.role==="user")return r("div",{class:`turn turn-user${t?" turn-current":""}`,children:[r("span",{class:"turn-user-mark",children:"›"}),r("div",{class:"turn-user-body",children:fr(e.text)})]});const n=e.tool_uses||[];return r("div",{class:`turn turn-assistant${t?" turn-current":""}`,children:[e.text&&r("div",{class:"turn-prose",children:fr(e.text)}),n.length>0&&r("div",{class:"turn-tools",children:n.map((i,l)=>r(Tw,{t:i},i.id||l))})]})}function Pw({target:e,composerRef:t}){const[n,i]=B(""),[l,a]=B(!1),s=ne(null);async function o(){const d=n;if(!d.trim()||l)return;a(!0),await ie("send",`/api/send?${nt(e)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({paste:d})})&&(await new Promise(h=>setTimeout(h,250)),await ie("send",`/api/send?${nt(e)}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({keys:["Enter"]})}),i(""),s.current&&(s.current.style.height="")),a(!1)}function u(d){d.key==="Enter"&&!d.shiftKey&&(d.preventDefault(),o())}function c(d){const p=d.currentTarget;i(p.value),p.style.height="0px",p.style.height=`${Math.min(p.scrollHeight,140)}px`}async function f(d){var h;const p=((h=d.clipboardData)==null?void 0:h.items)||[];for(const m of p){if(m.kind!=="file"||!m.type.startsWith("image/"))continue;const b=m.getAsFile();if(!b)continue;d.preventDefault();const w=await(await fetch(`/api/paste-image?${nt(e)}&deliver=false`,{method:"POST",headers:{"Content-Type":b.type||"image/png"},body:b})).json().catch(()=>({}));w.ok&&i(C=>`${C}${C&&!C.endsWith(" ")?" ":""}@${w.path} `);return}}return r("div",{class:"transcript-composer",ref:t,children:r("textarea",{ref:s,class:"transcript-composer-input",placeholder:"Message Claude… (Enter to send · Shift+Enter for newline · paste screenshots)",value:n,rows:1,onInput:c,onKeyDown:u,onPaste:f})})}function Aw({target:e,pid:t,selected:n,state:i,waitingFor:l,spinner:a}){var m;_w(e,t,n);const s=((m=Ft.value[t])==null?void 0:m.messages)||[],o=ne(null),u=ne(null),c=ne(!0),f=ne(""),d=()=>{const b=o.current;b&&c.current&&(b.scrollTop=b.scrollHeight)};j(()=>{const b=s[s.length-1],x=`${s.length}:${b?b.uuid:""}:${b?(b.tool_uses||[]).length:0}`;x!==f.current&&(f.current=x,d())},[s]),j(()=>{const b=u.current,x=o.current;if(!b||!x)return;const w=()=>{x.style.paddingBottom=`${b.offsetHeight+14}px`,requestAnimationFrame(d)};w();const C=new ResizeObserver(w);return C.observe(b),()=>C.disconnect()},[]),j(()=>{(i==="needs-input"||i==="working")&&d()},[i]);function p(b){const x=b.currentTarget;c.current=x.scrollHeight-x.scrollTop-x.clientHeight<80}let h="";for(let b=s.length-1;b>=0;b--)if(s[b].role!=="system"){h=s[b].uuid;break}return r(V,{children:[r("div",{class:"transcript",ref:o,onScroll:p,children:[s.length===0?r("div",{class:"transcript-empty",children:"No transcript yet."}):s.map(b=>b.role==="system"&&b.kind==="compact"?r("div",{class:"transcript-compact",children:r("span",{children:"context compacted"})},b.uuid):r(Ew,{m:b,current:b.uuid===h},b.uuid)),i==="needs-input"&&r("div",{class:"transcript-status transcript-status-waiting",children:[r("span",{class:"transcript-status-dot"}),r("span",{class:"transcript-status-text",children:["Waiting for your input",l?` — ${l}`:""]})]}),i==="working"&&r("div",{class:"transcript-working",children:[r("span",{class:"transcript-working-dot"}),r("span",{class:"transcript-working-verb",children:a?`${a.toLowerCase()}…`:"working…"})]})]}),r(Pw,{target:e,composerRef:u})]})}const Mw=1500;function Lw(e){const t=e.replace(/\/+$/,""),n=t.lastIndexOf("/");return n>=0?t.slice(n+1):t}function Iw({mode:e,onMode:t}){return r("span",{class:"detail-mode-toggle",children:[r("button",{class:e==="transcript"?"is-active":"",onClick:()=>t("transcript"),children:"Transcript"}),r("button",{class:e==="terminal"?"is-active":"",onClick:()=>t("terminal"),children:"Terminal"})]})}function Ow({pid:e,paneLabel:t}){const n=Ne.value[e]||[],i=Re.value[e]||"pane";return r("div",{class:"detail-tab-strip",role:"tablist",children:[r("button",{type:"button",role:"tab",class:`detail-tab${i==="pane"?" is-active":""}`,"aria-selected":i==="pane",onClick:()=>Nr(e,"pane"),title:t,children:r("span",{class:"detail-tab-label",children:t})}),r("button",{type:"button",role:"tab",class:`detail-tab${i==="changes"?" is-active":""}`,"aria-selected":i==="changes",onClick:()=>Nr(e,"changes"),title:"Changes in this worktree",children:r("span",{class:"detail-tab-label",children:"Changes"})}),n.map(l=>{const a=`file:${l.path}`,s=i===a;return r("button",{type:"button",role:"tab",class:`detail-tab${s?" is-active":""}`,"aria-selected":s,onClick:()=>Nr(e,a),title:l.path,children:[r("span",{class:"detail-tab-label",children:Lw(l.path)}),r("span",{class:"detail-tab-close",title:"Close tab",onClick:o=>{o.stopPropagation(),Tc(e,l.path)},children:"×"})]},a)})]})}function Wo(e){return(de.value||[]).find(t=>t.pid===e)||null}function Wd(e){return(e==null?void 0:e.agent)!=="claude"?"terminal":fu(e.pid)||"terminal"}async function Dw(e){var n;if(!tr.value)return;const t=((n=e.clipboardData)==null?void 0:n.items)||[];for(const i of t){if(i.kind!=="file"||!i.type.startsWith("image/"))continue;const l=i.getAsFile();if(l){e.preventDefault(),e.stopPropagation();try{const s=await(await fetch(`/api/paste-image?${nt(tr.value)}`,{method:"POST",headers:{"Content-Type":l.type||"image/png"},body:l})).json();s.ok||Us(`\r