Repository navigation
chore: rolling promotion dev -> main #650
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
be1d3b7
4e67fb3
931ea45
4d01f39
f63dd23
3891d74
925dc5b
d20f367
98abeac
7cb8712
46d30d4
a08fb48
3f6fa34
c97ab00
eee97a3
d4c0c51
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| #!/usr/bin/env bash | ||
| # Genie TUI — project session tabs for tmux top status bar | ||
| # Reads workers.json for agent counts per session, merges with tmux sessions. | ||
| # | ||
| # Output format per session: | ||
| # Active: #[bg=#7b2ff7,fg=#e0e0e0,bold] name (N) ● #[bg=#1a1a2e,fg=#7b2ff7] | ||
| # Inactive: #[fg=#b8a9c9,bg=#1a1a2e] name (N) | ||
| # | ||
| # Env: GENIE_WORKERS — override path to workers.json (for testing) | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| workers_file="${GENIE_WORKERS:-${HOME}/.genie/workers.json}" | ||
|
|
||
| # Get agent counts per session from workers.json (single jq pass) | ||
| declare -A agent_counts=() | ||
| if [[ -f "$workers_file" ]]; then | ||
| worker_data=$(jq -r ' | ||
| .workers // {} | to_entries | ||
| | group_by(.value.session) | ||
| | map({session: .[0].value.session, count: length}) | ||
| | map(.session + "\t" + (.count | tostring)) | ||
| | .[] | ||
| ' "$workers_file" 2>/dev/null) || worker_data="" | ||
|
|
||
| if [[ -n "$worker_data" ]]; then | ||
| while IFS=$'\t' read -r sess count; do | ||
| [[ -z "$sess" ]] && continue | ||
| agent_counts["$sess"]="$count" | ||
| done <<< "$worker_data" | ||
| fi | ||
| fi | ||
|
|
||
| # Get the currently active session name | ||
| has_tmux=true | ||
| active_session="" | ||
| if command -v tmux &>/dev/null; then | ||
| active_session=$(tmux display-message -p '#{client_session}' 2>/dev/null) || active_session="" | ||
| else | ||
| has_tmux=false | ||
| fi | ||
|
|
||
| max_visible=8 | ||
| output="" | ||
| declare -A seen_sessions=() | ||
| declare -a all_sessions=() | ||
| active_output="" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Remove the dead
Also applies to: 60-60 🤖 Prompt for AI Agents |
||
|
|
||
| if [[ "$has_tmux" == "true" ]]; then | ||
| # Collect all sessions with their rendered output | ||
| while IFS=$'\t' read -r session_name window_count; do | ||
| [[ -z "$session_name" ]] && continue | ||
| seen_sessions["$session_name"]=1 | ||
| all_sessions+=("$session_name") | ||
|
|
||
| # Use agent count from workers.json; fall back to window count | ||
| task_count="${agent_counts[$session_name]:-$window_count}" | ||
|
|
||
| if [[ "$session_name" == "$active_session" ]]; then | ||
| active_output="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${session_name} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] " | ||
| fi | ||
| done < <(tmux list-sessions -F "#{session_name} #{session_windows}" 2>/dev/null) | ||
| fi | ||
|
|
||
| # Also include sessions from workers.json not yet seen (no tmux session or testing mode) | ||
| for sess in "${!agent_counts[@]}"; do | ||
| [[ -n "${seen_sessions[$sess]:-}" ]] && continue | ||
| all_sessions+=("$sess") | ||
| done | ||
|
|
||
| total=${#all_sessions[@]} | ||
|
|
||
| if [[ "$total" -le "$max_visible" ]]; then | ||
| # No overflow — render all sessions | ||
| for session_name in "${all_sessions[@]}"; do | ||
| task_count="${agent_counts[$session_name]:-0}" | ||
| # Prefer window count from tmux if available | ||
| if [[ "$has_tmux" == "true" ]]; then | ||
| wcount=$(tmux list-windows -t "$session_name" -F "x" 2>/dev/null | wc -l) || wcount=0 | ||
| task_count="${agent_counts[$session_name]:-$wcount}" | ||
| fi | ||
|
|
||
| if [[ "$session_name" == "$active_session" ]]; then | ||
| output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${session_name} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] " | ||
| else | ||
| output+="#[fg=#b8a9c9,bg=#1a1a2e] ${session_name} (${task_count}) " | ||
| fi | ||
| done | ||
| else | ||
| # Overflow — show up to max_visible with active session always included | ||
| shown=0 | ||
| active_shown=false | ||
|
|
||
| for session_name in "${all_sessions[@]}"; do | ||
| if [[ "$shown" -ge "$((max_visible - 1))" && "$active_shown" == "false" && "$session_name" != "$active_session" ]]; then | ||
| # Reserve last slot for active session | ||
| continue | ||
| fi | ||
| if [[ "$shown" -ge "$max_visible" ]]; then | ||
| break | ||
|
Comment on lines
+95
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't reserve an active-session slot when In the no-tmux/testing path, Suggested fixAlso applies to: 128-130 🤖 Prompt for AI Agents |
||
| fi | ||
|
|
||
| task_count="${agent_counts[$session_name]:-0}" | ||
| if [[ "$has_tmux" == "true" ]]; then | ||
| wcount=$(tmux list-windows -t "$session_name" -F "x" 2>/dev/null | wc -l) || wcount=0 | ||
| task_count="${agent_counts[$session_name]:-$wcount}" | ||
| fi | ||
|
|
||
| if [[ "$session_name" == "$active_session" ]]; then | ||
| output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${session_name} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] " | ||
| active_shown=true | ||
| else | ||
| output+="#[fg=#b8a9c9,bg=#1a1a2e] ${session_name} (${task_count}) " | ||
| fi | ||
| ((shown++)) || true | ||
| done | ||
|
|
||
| # If active session wasn't shown yet, append it | ||
| if [[ "$active_shown" == "false" && -n "$active_session" ]]; then | ||
| task_count="${agent_counts[$active_session]:-0}" | ||
| if [[ "$has_tmux" == "true" ]]; then | ||
| wcount=$(tmux list-windows -t "$active_session" -F "x" 2>/dev/null | wc -l) || wcount=0 | ||
| task_count="${agent_counts[$active_session]:-$wcount}" | ||
| fi | ||
| output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] ${active_session} (${task_count}) ● #[bg=#1a1a2e,fg=#7b2ff7] " | ||
| fi | ||
|
|
||
| # Append overflow indicator | ||
| remaining=$((total - max_visible)) | ||
| output+="#[fg=#6c6c8a,bg=#1a1a2e] +${remaining} more " | ||
| fi | ||
|
|
||
| echo -n "$output" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| #!/usr/bin/env bash | ||
| # List all tmux sessions formatted for status bar display | ||
| # Active session gets purple highlight, others get dim text | ||
|
|
||
| current_session="$1" | ||
| if [ -z "$current_session" ]; then | ||
| current_session=$(tmux display-message -p '#{session_name}' 2>/dev/null) | ||
| fi | ||
|
|
||
| output="" | ||
| for sess in $(tmux list-sessions -F '#{session_name}' 2>/dev/null | sort); do | ||
| if [ "$sess" = "$current_session" ]; then | ||
| output+="#[bg=#7b2ff7,fg=#e0e0e0,bold] $sess #[bg=#16213e,fg=#7b2ff7,nobold]" | ||
| else | ||
| output+="#[fg=#b8a9c9,bg=#16213e] $sess " | ||
| fi | ||
| done | ||
|
Comment on lines
+11
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Repository: automagik-dev/genie Length of output: 93 🏁 Script executed: Repository: automagik-dev/genie Length of output: 839 🌐 Web query:
💡 Result: Tmux session names must be non-empty and cannot contain the characters ':' or '.'. Spaces and other special characters are allowed as long as they are not ':' or '.'. There is no defined maximum length. When creating or renaming a session, tmux validates the name using session_check_name, which returns an error if the name is empty or contains ':' or '.'. In the source code, this check is: return (*name != '\0' && name[strcspn(name, ":.")] == '\0');. If invalid, commands like new-session or rename-session report "bad session name". Colons and periods are prohibited because they are special characters used in tmux targets to separate session:window and window.pane (e.g., mysession:mywindow.1). Tabs are allowed in older versions but sanitized to spaces in tmux 3.2+. Control characters may cause issues like hanging (fixed in 3.0+), but are not explicitly forbidden beyond validation. Examples: - Valid: "my session", "test-123", "user@host" - Invalid: "", "my:session", "my.session", "session:name" Official tmux man pages do not explicitly document these constraints, but they are enforced in the code and confirmed by tmux maintainer in GitHub issues. Citations:
Avoid word-splitting session names during iteration. Line 11 uses command substitution in a Suggested fix🤖 Prompt for AI Agents |
||
|
|
||
| echo "$output" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| #!/usr/bin/env bash | ||
| # Genie TUI — per-window agent enrichment label | ||
| # Called from window-status-format via #() for each window tab. | ||
| # Must be fast (< 50ms) — single jq query, early exit on missing data. | ||
| # | ||
| # Usage: genie-window-label.sh <session_name> <window_name> | ||
| # Output: " ×count emoji" (e.g., " ×3 🔨") or empty string | ||
| # | ||
| # Env: GENIE_WORKERS — override path to workers.json (for testing) | ||
|
|
||
| session="${1:-}" | ||
| window="${2:-}" | ||
|
|
||
| # Early exit if no args | ||
| if [ -z "$session" ] || [ -z "$window" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| workers_file="${GENIE_WORKERS:-${HOME}/.genie/workers.json}" | ||
|
|
||
| # Early exit if no workers file | ||
| if [ ! -f "$workers_file" ]; then | ||
| exit 0 | ||
| fi | ||
|
|
||
| # Single jq pass: filter by session + window, count + worst state emoji | ||
| result=$(jq -r --arg sess "$session" --arg win "$window" ' | ||
| def state_priority: | ||
| {"error": 7, "permission": 6, "working": 5, "spawning": 4, "idle": 3, "done": 2, "suspended": 1}; | ||
| def state_emoji: | ||
| {"spawning": "⏳", "working": "🔨", "idle": "⏸", "done": "✓", "error": "✗", "permission": "❓", "suspended": "💤"}; | ||
|
|
||
| .workers // {} | to_entries | ||
| | map(select(.value.session == $sess and (.value.windowName // .value.team // "unknown") == $win)) | ||
| | if length == 0 then "" | ||
| else | ||
| length as $count | ||
| | map(.value.state // "idle") | ||
| | map(. as $s | state_priority[$s] // 0) | ||
| | max as $max_pri | ||
| | (state_priority | to_entries | map(select(.value == $max_pri)) | .[0].key // "idle") as $worst | ||
| | " ×\($count) \(state_emoji[$worst] // "")" | ||
| end | ||
| ' "$workers_file" 2>/dev/null) || exit 0 | ||
|
|
||
| # Only print if non-empty | ||
| if [ -n "$result" ]; then | ||
| printf '%s' "$result" | ||
| fi |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -33,6 +33,7 @@ setw -g mode-keys vi | |||||
| # --- Pane borders --- | ||||||
| set -g pane-border-style "fg=#0f3460" | ||||||
| set -g pane-active-border-style "fg=#7b2ff7" | ||||||
| set -g pane-border-status off | ||||||
|
|
||||||
| # --- Message styling --- | ||||||
| set -g message-style "bg=#16213e,fg=#00d2ff" | ||||||
|
|
@@ -45,54 +46,50 @@ setw -g mode-style "bg=#7b2ff7,fg=#e0e0e0" | |||||
| setw -g clock-mode-colour "#7b2ff7" | ||||||
|
|
||||||
| # ============================================================================ | ||||||
| # STATUS BAR | ||||||
| # STATUS BAR — Single bar at bottom with clickable tabs + info | ||||||
| # ============================================================================ | ||||||
| set -g status on | ||||||
| set -g status-interval 5 | ||||||
| set -g status-position top | ||||||
| set -g status-position bottom | ||||||
| set -g status-justify left | ||||||
| set -g status-style "bg=#1a1a2e,fg=#e0e0e0" | ||||||
|
|
||||||
| # --- Left: Genie branding + version --- | ||||||
| set -g status-left-length 60 | ||||||
| set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] #(genie --version 2>/dev/null | head -1 || echo 'Genie') #[bg=#1a1a2e,fg=#7b2ff7]" | ||||||
| # Left: branding | ||||||
| set -g status-left-length 40 | ||||||
| set -g status-left "#[bg=#7b2ff7,fg=#e0e0e0,bold] 🧞 #(genie --version 2>/dev/null | head -1 || echo Genie) #[bg=#1a1a2e,fg=#7b2ff7]" | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Repository: automagik-dev/genie Length of output: 302 Remove On line 59, the Proposed fix📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
|
|
||||||
| # --- Right: git | CPU | RAM | clock --- | ||||||
| set -g status-right-length 120 | ||||||
| # Right: system info | ||||||
| set -g status-right-length 100 | ||||||
| set -g status-right "#[fg=#6c6c8a]#($HOME/.genie/scripts/genie-git.sh) #[fg=#0f3460]| #[fg=#00d2ff]CPU #($HOME/.genie/scripts/cpu-info.sh) #[fg=#0f3460]| #[fg=#00d2ff]RAM #($HOME/.genie/scripts/ram-info.sh) #[fg=#0f3460]| #[fg=#e0e0e0]%H:%M " | ||||||
|
|
||||||
| # ============================================================================ | ||||||
| # WINDOW (TAB) STYLING — clickable tabs | ||||||
| # ============================================================================ | ||||||
|
|
||||||
| # Tab separator | ||||||
| set -g window-status-separator "" | ||||||
|
|
||||||
| # Inactive tabs: lavender text on dark bg | ||||||
| set -g window-status-format "#[fg=#b8a9c9,bg=#1a1a2e] #I:#W " | ||||||
|
|
||||||
| # Active tab: purple bg, white text, bold | ||||||
| set -g window-status-current-format "#[fg=#1a1a2e,bg=#7b2ff7]#[fg=#e0e0e0,bg=#7b2ff7,bold] #I:#W #[fg=#7b2ff7,bg=#1a1a2e]" | ||||||
|
|
||||||
| # Activity monitoring | ||||||
| # Activity | ||||||
| setw -g monitor-activity on | ||||||
| set -g visual-activity off | ||||||
| set -g window-status-activity-style "fg=#f5a623,bg=#1a1a2e" | ||||||
|
|
||||||
| # ============================================================================ | ||||||
| # SECOND STATUS LINE — bottom tab bar (tmux 3.2+) | ||||||
| # ============================================================================ | ||||||
| # Uncomment the following for a dual status bar (requires tmux 3.2+): | ||||||
| # set -g status 2 | ||||||
| # set -g status-format[1] "#[align=centre,fg=#6c6c8a,bg=#16213e] #($HOME/.genie/scripts/genie-update-check.sh) " | ||||||
|
|
||||||
| # ============================================================================ | ||||||
| # KEYBINDINGS (standard — no changes from defaults beyond vi mode) | ||||||
| # KEYBINDINGS | ||||||
| # ============================================================================ | ||||||
|
|
||||||
| # Reload config | ||||||
| bind r source-file ~/.tmux.conf \; display-message "Config reloaded" | ||||||
|
|
||||||
| # Vi copy mode bindings | ||||||
| bind -n C-t new-window -c "#{?session_path,#{session_path},#{pane_current_path}}" | ||||||
| bind -T copy-mode-vi v send-keys -X begin-selection | ||||||
| bind -T copy-mode-vi y send-keys -X copy-selection-and-cancel | ||||||
| bind -n C-) switch-client -n | ||||||
| bind -n C-( switch-client -p | ||||||
| bind -n M-1 select-window -t 0 | ||||||
| bind -n M-2 select-window -t 1 | ||||||
| bind -n M-3 select-window -t 2 | ||||||
| bind -n M-4 select-window -t 3 | ||||||
| bind -n M-5 select-window -t 4 | ||||||
| bind -n M-6 select-window -t 5 | ||||||
| bind -n M-7 select-window -t 6 | ||||||
| bind -n M-8 select-window -t 7 | ||||||
| bind -n M-9 select-window -t 8 | ||||||
| bind -n DoubleClick1StatusDefault choose-tree -s | ||||||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -113,7 +113,7 @@ async function ensureNativeTeamForLeader(teamName: string, cwd: string): Promise | |||||||
|
|
||||||||
| await registerNativeMember(teamName, { | ||||||||
| agentName: basename(cwd), | ||||||||
| agentType: 'general-purpose', | ||||||||
| agentType: 'team-lead', | ||||||||
| color: 'blue', | ||||||||
| cwd, | ||||||||
| }); | ||||||||
|
|
@@ -319,8 +319,8 @@ function attachToWindow(sessionName: string, windowName: string): void { | |||||||
| } | ||||||||
|
|
||||||||
| export async function sessionCommand(options: SessionOptions = {}): Promise<void> { | ||||||||
| const sessionName = options.name ?? DEFAULT_SESSION_NAME; | ||||||||
| const workspaceDir = options.dir ?? process.cwd(); | ||||||||
| const sessionName = options.name ?? sanitizeWindowName(basename(workspaceDir)); | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Revert default Defaulting to Proposed fix📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
|
|
||||||||
| try { | ||||||||
| const windowName = await deriveWindowName(sessionName, workspaceDir, options.team); | ||||||||
|
|
@@ -335,12 +335,24 @@ export async function sessionCommand(options: SessionOptions = {}): Promise<void | |||||||
|
|
||||||||
| if (!session) { | ||||||||
| await createSession(sessionName, windowName, workspaceDir, systemPromptFile); | ||||||||
| attachToWindow(sessionName, windowName); | ||||||||
| } else if (process.env.TMUX) { | ||||||||
| // Already inside tmux — launch Claude Code in the CURRENT pane | ||||||||
| const suffix = Date.now().toString(36).slice(-4); | ||||||||
| const currentWindowName = `${windowName}-${suffix}`; | ||||||||
| await tmux.executeTmux(`rename-window ${shellQuote(currentWindowName)}`); | ||||||||
| await ensureNativeTeamForLeader(currentWindowName, workspaceDir); | ||||||||
| const agentName = basename(workspaceDir); | ||||||||
| const resumeSessionId = findLastSessionId(sanitizeTeamName(currentWindowName), agentName, workspaceDir); | ||||||||
| const cmd = buildClaudeCommand(currentWindowName, systemPromptFile || undefined, resumeSessionId || undefined); | ||||||||
|
Comment on lines
+339
to
+347
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Inside-tmux flow can target the wrong window and fragments team identity.
Proposed fixBased on learnings: 🤖 Prompt for AI Agents |
||||||||
| const { execSync: execSyncCmd } = require('node:child_process'); | ||||||||
| execSyncCmd(cmd, { stdio: 'inherit', cwd: workspaceDir }); | ||||||||
|
Comment on lines
+348
to
+349
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: Repository: automagik-dev/genie Length of output: 1151 🏁 Script executed: Repository: automagik-dev/genie Length of output: 2020 🏁 Script executed: Repository: automagik-dev/genie Length of output: 637 🏁 Script executed: Repository: automagik-dev/genie Length of output: 657 Use static import for In ESM mode ( Proposed fixThen at line 348–349: 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||
| } else { | ||||||||
| // Outside tmux — attach to existing session | ||||||||
| console.log(`Session "${sessionName}" already exists`); | ||||||||
| await focusTeamWindow(sessionName, windowName, workspaceDir, systemPromptFile); | ||||||||
| attachToWindow(sessionName, windowName); | ||||||||
| } | ||||||||
|
|
||||||||
| attachToWindow(sessionName, windowName); | ||||||||
| } catch (error) { | ||||||||
| const message = error instanceof Error ? error.message : String(error); | ||||||||
| console.error(`Error: ${message}`); | ||||||||
|
|
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The versioning script (
scripts/version.ts) updates this version by targeting the first plugin in thepluginsarray (plugins[0]). This approach is brittle and may lead to incorrect versioning if more plugins are added to this file or if their order changes. To make the update process more robust, the script should be modified to locate the 'genie' plugin by itsnameproperty before updating the version.