@@ -19,7 +19,11 @@ for d in a b c d; do
1919done
2020
2121SESSION=" livesync"
22- tmux kill-session -t " $SESSION " 2> /dev/null || true
22+
23+ # Kill any MCP servers and the tmux session from a prior demo run. Skipping
24+ # this leaves stale fsnotify watchers attached to /tmp/demo-* that flood the
25+ # sync feed with skip events.
26+ bash " $( dirname " $0 " ) /teardown-livesync.sh"
2327
2428# ── Build the layout ─────────────────────────────────────────────────
2529# Final shape:
@@ -35,64 +39,75 @@ tmux kill-session -t "$SESSION" 2>/dev/null || true
3539tmux new-session -d -s " $SESSION " -x 240 -y 60 -c /tmp/demo-a
3640tmux rename-window -t " $SESSION :0" livesync
3741
38- # Reserve the bottom ~18% for the sync feed.
39- tmux split-window -v -l 18% -t " $SESSION :0" -c /tmp/demo-a
42+ # Capture pane IDs as we create them so we don't depend on numeric
43+ # indices (which shift under `pane-base-index 1` and across tmux
44+ # versions). Pane IDs look like "%17" and are stable.
45+ A=$( tmux display-message -p -t " $SESSION :0" ' #{pane_id}' )
46+
47+ # Reserve the bottom ~35% for the sync feed (it's the narration track
48+ # of the demo — it needs to be tall enough to actually read).
49+ FEED=$( tmux split-window -v -l 35% -t " $A " -c /tmp/demo-a -P -F ' #{pane_id}' )
4050
41- # Top pane (now pane 0) becomes the 2×2. Pane 1 is the feed .
42- tmux split-window -h -l 50% -t " $SESSION :0.0 " -c /tmp/demo-b
43- tmux split-window -v -l 50% -t " $SESSION :0.0 " -c /tmp/demo-c
44- tmux split-window -v -l 50% -t " $SESSION :0.2 " -c /tmp/demo-d
51+ # Turn the top pane into a 2×2.
52+ B= $( tmux split-window -h -l 50% -t " $A " -c /tmp/demo-b -P -F ' #{pane_id} ' )
53+ C= $( tmux split-window -v -l 50% -t " $A " -c /tmp/demo-c -P -F ' #{pane_id} ' )
54+ D= $( tmux split-window -v -l 50% -t " $B " -c /tmp/demo-d -P -F ' #{pane_id} ' )
4555
4656# Wire each agent pane: clear + show which agent + cd into its dir.
47- # Pane indices after the splits above:
48- # 0 = Agent A (top-left)
49- # 2 = Agent B (top-right)
50- # 3 = Agent C (bottom-left)
51- # 4 = Agent D (bottom-right)
52- # 1 = Sync feed (bottom strip)
53- tmux send-keys -t " $SESSION :0.0" ' clear; printf "\033[1;31mAGENT A — backend\033[0m (/tmp/demo-a)\n"' C-m
54- tmux send-keys -t " $SESSION :0.2" ' clear; printf "\033[1;34mAGENT B — tests\033[0m (/tmp/demo-b)\n"' C-m
55- tmux send-keys -t " $SESSION :0.3" ' clear; printf "\033[1;32mAGENT C — frontend\033[0m (/tmp/demo-c)\n"' C-m
56- tmux send-keys -t " $SESSION :0.4" ' clear; printf "\033[1;33mAGENT D — docs\033[0m (/tmp/demo-d)\n"' C-m
57+ tmux send-keys -t " $A " ' clear; printf "\033[1;31mAGENT A — backend\033[0m (/tmp/demo-a)\n"' C-m
58+ tmux send-keys -t " $B " ' clear; printf "\033[1;34mAGENT B — tests\033[0m (/tmp/demo-b)\n"' C-m
59+ tmux send-keys -t " $C " ' clear; printf "\033[1;32mAGENT C — frontend\033[0m (/tmp/demo-c)\n"' C-m
60+ tmux send-keys -t " $D " ' clear; printf "\033[1;33mAGENT D — docs\033[0m (/tmp/demo-d)\n"' C-m
5761
5862# Sync-feed pane: tail every agent's sync log, prefixed with the agent
5963# letter. jq is used when available to pretty-print; otherwise raw JSONL.
60- FEED_CMD=$( cat << 'EOS '
64+ #
65+ # We write the feed program to a file and exec it instead of piping the
66+ # whole multi-line if/then/fi block through `send-keys` — typing that
67+ # into an interactive shell is fragile (PS2 continuation hazards,
68+ # quoting interactions). A script file is parsed as a single unit.
69+ FEED_SCRIPT=" /tmp/demo-livesync-feed.sh"
70+ cat > " $FEED_SCRIPT " << 'EOS '
71+ #!/usr/bin/env bash
6172clear
6273printf "\033[1mSYNC FEED\033[0m (push/recv across all four agents)\n\n"
6374TODAY=$(date +%Y-%m-%d)
6475if command -v jq >/dev/null 2>&1; then
6576 {
6677 for d in a b c d; do
67- tail -F /tmp/demo-$d/.kai/sync-log/$TODAY.jsonl 2>/dev/null | \
68- sed "s/^/ $d /" &
78+ tail -F " /tmp/demo-$d/.kai/sync-log/$TODAY.jsonl" 2>/dev/null \
79+ | awk -v p=" $d " '{print p $0; fflush()}' &
6980 done
7081 wait
71- } | jq -r --unbuffered '
82+ } | jq -R - r --unbuffered '
7283 def color(e):
73- if e == "push" then "\u001b[1;32m"
74- elif e == "recv" then "\u001b[2m"
75- elif e == "merge" then "\u001b[1;33m"
76- elif e == "conflict" then "\u001b[1;31m"
77- else "\u001b[0m" end;
78- def tag(l): if l=="a" then "\u001b[31mA" elif l=="b" then "\u001b[34mB"
79- elif l=="c" then "\u001b[32mC" elif l=="d" then "\u001b[33mD"
80- else l end;
81- (input_line_number|tostring) as $ln |
82- . as $line |
83- ($line | split(" ") | .[0]) as $letter |
84- ($line | .[2:] | fromjson? // {event:"(parse err)", file:$line}) as $ev |
85- "\(tag($letter))\u001b[0m \(color($ev.event))\( ($ev.timestamp // 0) / 1000 | strftime("%H:%M:%S") ) \($ev.event | ascii_upcase) \($ev.file // "")\u001b[0m"
84+ if e == "push" then "[1;32m"
85+ elif e == "receive" then "[2m"
86+ elif e == "merge" then "[1;33m"
87+ elif e == "conflict" then "[1;31m"
88+ else "[0m" end;
89+ def tag(l):
90+ if l == "a" then "[31mA"
91+ elif l == "b" then "[34mB"
92+ elif l == "c" then "[32mC"
93+ elif l == "d" then "[33mD"
94+ else l end;
95+ . as $line
96+ | ($line | split(" ") | .[0]) as $letter
97+ | ($line | .[2:] | fromjson? // {event:"(parse err)", file:$line}) as $ev
98+ | select($ev.event != "skip")
99+ | "\(tag($letter))[0m \(color($ev.event))\(($ev.timestamp // 0) / 1000 | strftime("%H:%M:%S")) \($ev.event | ascii_upcase) \($ev.file // "")[0m"
86100 '
87101else
88102 for d in a b c d; do
89- tail -F /tmp/demo-$d/.kai/sync-log/$TODAY.jsonl 2>/dev/null | sed "s/^/[$d] /" &
103+ tail -F "/tmp/demo-$d/.kai/sync-log/$TODAY.jsonl" 2>/dev/null \
104+ | awk -v p="[$d] " '{print p $0; fflush()}' &
90105 done
91106 wait
92107fi
93108EOS
94- )
95- tmux send-keys -t " $SESSION :0.1 " " $FEED_CMD " C-m
109+ chmod +x " $FEED_SCRIPT "
110+ tmux send-keys -t " $FEED " " exec bash $FEED_SCRIPT " C-m
96111
97112echo " Attaching to tmux session '$SESSION '…"
98113echo " Ctrl-B q = show pane numbers"
0 commit comments