#!/usr/bin/env bash
set -euo pipefail

# ---------------------------------------------------------------------------
# Set your preferred terminal here, or leave empty to auto-detect.
# One of: ghostty | iterm | alacritty | terminal
# (Also overridable from the environment: PREFERRED_TERM=iterm)
# ---------------------------------------------------------------------------
PREFERRED_TERM="${PREFERRED_TERM:-}"

LOG=/tmp/tuple-trigger-debug.log
{
    printf '\n=== %s call-transcription-started fired (open-in-opencode) ===\n' "$(date -u +%FT%TZ)"
    printf 'cwd=%s pid=%s ppid=%s\n' "$(pwd)" "$$" "$PPID"
} >> "$LOG" 2>&1
trap 'printf "exit status=%s on line %s\n" "$?" "$LINENO" >> "$LOG"' EXIT
exec >>"$LOG" 2>&1

if [ -z "${TUPLE_TRIGGER_CALL_ARTIFACTS_DIRECTORY:-}" ]; then
    echo "open-in-opencode: TUPLE_TRIGGER_CALL_ARTIFACTS_DIRECTORY was not set" >&2
    exit 1
fi

TRANSCRIPTION_DIR="${TUPLE_TRIGGER_CALL_ARTIFACTS_DIRECTORY}"
if [ ! -d "${TRANSCRIPTION_DIR}" ]; then
    echo "open-in-opencode: transcription directory does not exist: ${TRANSCRIPTION_DIR}" >&2
    exit 1
fi

SESSION_DIR="$(basename "${TRANSCRIPTION_DIR}")"
PARENT_DIR="$(dirname "${TRANSCRIPTION_DIR}")"
PARENT_NAME="$(basename "${PARENT_DIR}")"
if [[ "${SESSION_DIR}" == *"@"* ]]; then
    CALL_ID="${SESSION_DIR##*@}"
elif [[ "${PARENT_NAME}" =~ ^[0-9A-Fa-f-]{8,}$ ]]; then
    CALL_ID="${PARENT_NAME}"
else
    CALL_ID="${SESSION_DIR}"
fi

PID_FILE="${TRANSCRIPTION_DIR}/opencode-sidekick.pid"
PROMPT_FILE="${TRANSCRIPTION_DIR}/opencode-sidekick-prompt.md"
LAUNCHER_FILE="${TRANSCRIPTION_DIR}/launch-opencode-sidekick.command"
TRIGGER_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
WATCHER_SRC="${TRIGGER_DIR}/tuple-call-watcher.py"
WATCHER_DST="${TRANSCRIPTION_DIR}/tuple-call-watcher.py"

if [ -f "${PID_FILE}" ] && kill -0 "$(cat "${PID_FILE}" 2>/dev/null)" 2>/dev/null; then
    echo "open-in-opencode: OpenCode sidekick already running for ${TRANSCRIPTION_DIR}"
    exit 0
fi

# Ship the fixed, deterministic transcript watcher next to the live transcript
# files so the model runs it verbatim instead of authoring its own watch loop.
if [ -f "${WATCHER_SRC}" ]; then
    cp "${WATCHER_SRC}" "${WATCHER_DST}"
    chmod 0755 "${WATCHER_DST}"
else
    echo "open-in-opencode: watcher not found at ${WATCHER_SRC}" >&2
    exit 1
fi

cat > "${PROMPT_FILE}.tmpl" <<'PROMPT'
You are a real-time companion on a live Tuple pair-programming call, running inside OpenCode. Your working directory is the Tuple transcripts root, which holds one directory per transcription session, each named `<timestamp>@<call-id>`. The active session for *this* call is `./__SESSIONDIR__/`, holding `events.jsonl` and `transcriptions.jsonl` (one JSON record per line). Transcription can stop and restart mid-call; each restart creates a new session directory at the root whose name ends `@__CALLID__`, so this call's full session set is every directory matching `./*@__CALLID__/` — plan for it to grow. Other root directories are prior calls — available if the user asks about earlier discussions, but the active transcription is your focus.

You interact with one human: the user at this terminal. Other call participants don't see your output. You follow this call by running Tuple's bundled watcher script (see Watcher); file reads are available too. You have no event-driven wake mechanism — each watcher run blocks until the next batch, prints it, then exits and hands control back to you; you handle the batch and **run it again**. The call keeps going between runs, so don't end your turn (until `call_ended`), and don't daemonize, detach, or background the watcher.

## Catch up first

Your **first** run catches up on everything said before you joined. Run it once with `--catchup`:

    ./__SESSIONDIR__/tuple-call-watcher.py --catchup --offsets opencode

It prints the whole backlog as tagged lines (format under Watcher). Respond with a brief catch-up summary **only** (decisions so far, who's here, open threads). Do **not** dot-line the backlog — dumping the historical records back out is noise. Dot-line logging starts with the live records from the next run.

## File schemas

| File                   | Fields                            | Notes                                                                                                                              |
| ---------------------- | --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| `events.jsonl`         | `category, message, time, user?`  | Categories: `recording_started`, `recording_stopped`, `user_joined`, `user_left`, `user_audio_started`, `user_audio_stopped`, `call_ended`. |
| `transcriptions.jsonl` | `start, end, text, user_id`       | Resolve `user_id` to a name via `user_joined` events.                                                                              |

If a field looks unfamiliar, read a few lines to confirm shape first.

## Watcher

Tuple ships a fixed watcher script — run it verbatim; do **not** author your own loop. After the catch-up, follow the call live by repeatedly running it in `--exit-on-batch` mode:

    ./__SESSIONDIR__/tuple-call-watcher.py --exit-on-batch --offsets opencode

It follows every session directory of this call, including mid-call restarts. Each run **blocks until the next records arrive, gathers a brief batch (a ~1s pause, or up to ~15s of continuous speech), prints it, and exits**. It resumes exactly where the previous run (or the catch-up) stopped — its own `--offsets opencode` file — so just handle the output and **run the same command again**. It prints records as tagged lines:

    T|<session-dir>|<json-record>   a transcriptions.jsonl record
    E|<session-dir>|<json-record>   an events.jsonl record

## Handling each batch

On each live batch, log every record from transcriptions and interesting records from events (especially user joined/left, but NOT user started/stopped sending audio) as a dot-line:

    · HH:MM:SS <speaker_or_event>: <text-or-category>

Stay silent when there's nothing to report: if a run ever returns no records — you interrupted it while it was waiting, or it was cut short — don't announce that you're still watching or that nothing flushed. Say nothing and run the watcher again. Only produce output to log records, answer a wake, or write a summary.

If a transcript record addresses you by name (see Wake words), respond conversationally to that turn — the other records in the batch still get their dot-line. On `recording_stopped`, add a checkpoint summary. On `call_ended`, write a final summary, after which you may stop re-running the watcher and end your turn. Summaries are bulleted decisions, action items (with owner when named), and open threads. No preamble.

## Wake words

A line addresses you when "OpenCode" or "opencode" appears as a vocative — at the start of an utterance ("OpenCode, ..."), after a greeting ("hey OpenCode"), or as a direct question ("OpenCode, can you ..."). Incidental mentions like "OpenCode is great" aren't wakes. When ambiguous, prefer the dot-line log; a missed wake is cheaper than a false one. Respond to the addressed turn only; other records in the same batch still get their dot-line.

## Terminal input

The user may type to you directly at this terminal, between watcher runs. Reply, then go back to running the watcher. Never end your turn to wait for input; the call continues whether or not the user is typing.

To start: catch up first (`--catchup` + a brief summary), then loop the `--exit-on-batch` watcher and keep it going; do not end your turn until `call_ended` (or the user tells you to stop).

## Following later calls (only if asked)

Your focus is this call, but the transcripts root holds every call. If the user asks you to keep going past it — to follow them into their next call — watch the root for a new `<timestamp>@<call-id>` directory whose call-id differs from this one's (`__CALLID__`). When one appears, catch up on it and loop it exactly as you did here, passing its call-id as the last argument:

    ./__SESSIONDIR__/tuple-call-watcher.py --catchup --offsets opencode <new-call-id>      # catch up
    ./__SESSIONDIR__/tuple-call-watcher.py --exit-on-batch --offsets opencode <new-call-id>  # then loop live

The script lives in this call's dir but follows whatever call-id you pass; its offset files live in the target call's dirs, so reusing `--offsets opencode` across calls is fine. Don't do this unless the user asks.
PROMPT
# bash 3.2 (macOS default) mangles heredocs inside $()/`` so the prompt is
# written straight to a file, then placeholders are substituted with sed.
sed -e "s|__SESSIONDIR__|${SESSION_DIR}|g" -e "s|__CALLID__|${CALL_ID}|g" "${PROMPT_FILE}.tmpl" > "${PROMPT_FILE}"
rm -f "${PROMPT_FILE}.tmpl"

cat > "${LAUNCHER_FILE}" <<'SCRIPT'
#!/bin/zsh -li
printf '\e]0;Tuple OpenCode Sidekick\a'
clear
echo 'Starting OpenCode sidekick…'
echo

SCRIPT_DIR="${0:A:h}"
PID_FILE="${SCRIPT_DIR}/opencode-sidekick.pid"
PROMPT_FILE="${SCRIPT_DIR}/opencode-sidekick-prompt.md"

echo $$ > "${PID_FILE}"
cd "${SCRIPT_DIR}/.." || exit 1

if ! command -v opencode >/dev/null 2>&1; then
    echo "OpenCode was not found on your interactive shell PATH."
    echo "Make sure 'opencode' works in a new terminal, then run the trigger again."
    echo
    read '?Press return to close.'
    exit 127
fi
if ! command -v python3 >/dev/null 2>&1; then
    echo "The Tuple watcher needs python3 (install Xcode Command Line Tools: xcode-select --install)."
    echo
    read '?Press return to close.'
    exit 127
fi

exec opencode . --prompt "$(cat "${PROMPT_FILE}")"
SCRIPT
chmod 0755 "${LAUNCHER_FILE}"

# --- launch_in_terminal: open the .command launcher in the user's terminal ---
# Uses LaunchServices (`open`) only — no direct binary exec and no AppleScript,
# so it triggers no macOS accessibility prompt and no stray empty windows.
# With PREFERRED_TERM empty, the launcher opens in your default handler for
# .command files (change it in Finder: right-click a .command > Open With >
# your terminal > Change All). Set PREFERRED_TERM to force one for this trigger.
# Ghostty, iTerm, and Terminal run an opened .command directly; Alacritty has no
# document handler, so it is launched with `open -na ... --args -e`.
launch_in_terminal() {
    local file="$1"
    case "${PREFERRED_TERM:-}" in
    "") open "$file" ;;
    ghostty) open -a "Ghostty" "$file" 2>/dev/null || open "$file" ;;
    iterm) open -a "iTerm" "$file" 2>/dev/null || open "$file" ;;
    terminal) open -a "Terminal" "$file" 2>/dev/null || open "$file" ;;
    alacritty) open -na "Alacritty" --args -e "$file" 2>/dev/null || open "$file" ;;
    *) echo "launch_in_terminal: unknown PREFERRED_TERM='${PREFERRED_TERM}'; using default handler" >&2; open "$file" ;;
    esac
}

if [ "${OPEN_IN_OPENCODE_DRY_RUN:-}" = "1" ]; then
    echo "open-in-opencode: dry run generated ${LAUNCHER_FILE}"
    exit 0
fi

launch_in_terminal "${LAUNCHER_FILE}"
