claude-plugin-viban 1.0.34 → 1.0.35

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viban",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Terminal Kanban TUI for AI-human collaborative issue tracking",
5
5
  "author": {
6
6
  "name": "happy-nut"
package/bin/viban CHANGED
@@ -119,6 +119,9 @@ esac
119
119
 
120
120
  IN_TUI=false
121
121
  cleanup() {
122
+ # Skip cleanup in subshells — EXIT trap fires in $() command substitutions
123
+ [[ ${ZSH_SUBSHELL:-0} -gt 0 ]] && return
124
+ _stop_coproc
122
125
  printf '\033[?25h\033[0m'
123
126
  stty echo 2>/dev/null
124
127
  $IN_TUI && clear
@@ -126,6 +129,60 @@ cleanup() {
126
129
  }
127
130
  trap cleanup INT TERM EXIT
128
131
 
132
+ # Python coprocess for TUI rendering (eliminates per-frame spawn overhead)
133
+ # Uses explicit file descriptors (fd 7/8) to avoid interfering with read -sk1
134
+ _COPROC_PID=""
135
+ _COPROC_RESULT=""
136
+
137
+ _start_coproc() {
138
+ local _in_fifo _out_fifo
139
+ _in_fifo=$(mktemp -u /tmp/viban_cp_in.XXXXXX)
140
+ _out_fifo=$(mktemp -u /tmp/viban_cp_out.XXXXXX)
141
+ mkfifo "$_in_fifo" "$_out_fifo"
142
+ python3 "$VIBAN_SCRIPT_DIR/scripts/tui_coprocess.py" < "$_in_fifo" > "$_out_fifo" &
143
+ _COPROC_PID=$!
144
+ exec 7>"$_in_fifo" 8<"$_out_fifo"
145
+ rm -f "$_in_fifo" "$_out_fifo"
146
+ }
147
+
148
+ _stop_coproc() {
149
+ if [[ -n "$_COPROC_PID" ]] && kill -0 "$_COPROC_PID" 2>/dev/null; then
150
+ echo "QUIT" >&7 2>/dev/null
151
+ exec 7>&- 2>/dev/null
152
+ wait "$_COPROC_PID" 2>/dev/null
153
+ else
154
+ exec 7>&- 2>/dev/null
155
+ fi
156
+ exec 8<&- 2>/dev/null
157
+ _COPROC_PID=""
158
+ }
159
+
160
+ _coproc_batch_trunc() {
161
+ echo "BATCH_TRUNC" >&7
162
+ echo "$1" >&7
163
+ echo "END" >&7
164
+ _COPROC_RESULT=""
165
+ local line
166
+ while read -r line <&8; do
167
+ [[ "$line" == "END" ]] && break
168
+ [[ -n "$_COPROC_RESULT" ]] && _COPROC_RESULT+=$'\n'
169
+ _COPROC_RESULT+="$line"
170
+ done
171
+ }
172
+
173
+ _coproc_batch_width() {
174
+ echo "BATCH_WIDTH" >&7
175
+ echo "$1" >&7
176
+ echo "END" >&7
177
+ _COPROC_RESULT=""
178
+ local line
179
+ while read -r line <&8; do
180
+ [[ "$line" == "END" ]] && break
181
+ [[ -n "$_COPROC_RESULT" ]] && _COPROC_RESULT+=$'\n'
182
+ _COPROC_RESULT+="$line"
183
+ done
184
+ }
185
+
129
186
  # Prevent gum from querying terminal colors
130
187
  export CLICOLOR_FORCE=1
131
188
  export COLORTERM=truecolor
@@ -449,48 +506,6 @@ get_term_height() {
449
506
  fi
450
507
  }
451
508
 
452
- # Get display width of string using Unicode East Asian Width property
453
- # F(Fullwidth) and W(Wide) = 2 columns, others = 1 column
454
- str_width() {
455
- local str="$1"
456
- local char_count=${#str}
457
- local byte_count
458
- LC_ALL=C byte_count=${#str}
459
-
460
- # If all ASCII, simple calculation (fast path)
461
- [[ $byte_count -eq $char_count ]] && { echo $char_count; return; }
462
-
463
- # Use Python for accurate Unicode width calculation
464
- # Note: <<< adds trailing newline, so we strip it with rstrip()
465
- python3 -c "
466
- import unicodedata, sys
467
- s = sys.stdin.read().rstrip('\n')
468
- print(sum(2 if unicodedata.east_asian_width(c) in 'FW' else 1 for c in s))
469
- " <<< "$str"
470
- }
471
-
472
- # Truncate string to max display width (optimized)
473
- # Uses str_width for width calculation to ensure correct byte counting
474
- truncate_str() {
475
- local str="$1" max=$2
476
- local len=${#str}
477
- local w=$(str_width "$str")
478
- # If already fits, return as-is
479
- (( w <= max )) && { echo "$str"; return; }
480
- # Binary search for truncation point
481
- local lo=0 hi=$len mid sub_str
482
- while (( lo < hi )); do
483
- mid=$(( (lo + hi + 1) / 2 ))
484
- sub_str="${str:0:$mid}"
485
- w=$(str_width "$sub_str")
486
- if (( w <= max )); then
487
- lo=$mid
488
- else
489
- hi=$((mid - 1))
490
- fi
491
- done
492
- echo "${str:0:$lo}"
493
- }
494
509
 
495
510
  # ANSI color codes - Orange Theme
496
511
  A_RESET="\033[0m"
@@ -607,64 +622,120 @@ build_column_lines() {
607
622
  local card_inner=$((col_w - 4))
608
623
  local border=$(gen_border $card_inner)
609
624
 
610
- # Task cards (5 lines: top border, title, desc/priority, empty, bottom border)
611
- local shown=0
612
- while IFS=$'\t' read -r id title desc priority issue_type; do
613
- [[ -z "$id" ]] && continue
614
- (( shown >= CACHED_MAX_TASKS )) && {
615
- local more_text=" +$((count - shown)) more..."
616
- local more_w=${#more_text}
617
- printf "${A_DIM}%s${A_RESET}%$((col_w - more_w))s\n" "$more_text" ""
618
- ((lines_used++))
619
- break
620
- }
625
+ # --- Pass 1: Collect card data into arrays ---
626
+ local -a _ids _titles _descs _priorities _types _title_max_ws _title_pfxs
627
+ local _has_nonascii=0
628
+ local _desc_max_w=$((card_inner - 4))
629
+ local _spinner_w=0
630
+ [[ "$st" == "in_progress" ]] && _spinner_w=2
631
+ local _cc _bc _pfx
632
+
633
+ while IFS=$'\t' read -r _id _title _desc _priority _type; do
634
+ [[ -z "$_id" ]] && continue
635
+ (( ${#_ids} >= CACHED_MAX_TASKS )) && break
636
+ [[ -z "$_priority" || "$_priority" == "null" ]] && _priority="P3"
637
+ [[ -z "$_type" || "$_type" == "null" ]] && _type=""
638
+ [[ "$_desc" == "null" ]] && _desc=""
639
+
640
+ _ids+=("$_id"); _titles+=("$_title"); _descs+=("$_desc")
641
+ _priorities+=("$_priority"); _types+=("$_type")
642
+
643
+ # Per-card title width limit
644
+ _title_max_ws+=($((card_inner - 4 - ${#_id} - _spinner_w - 1)))
645
+ # Prefix for width calc (X as spinner placeholder - same width 1 as braille chars)
646
+ _pfx=" "
647
+ (( _spinner_w )) && _pfx=" X "
648
+ _title_pfxs+=("${_pfx}#${_id} ")
649
+
650
+ # Check for non-ASCII
651
+ if (( ! _has_nonascii )); then
652
+ _cc=${#_title}
653
+ LC_ALL=C _bc=${#_title}; unset LC_ALL
654
+ (( _bc != _cc )) && _has_nonascii=1
655
+ if (( ! _has_nonascii && ${#_desc} > 0 )); then
656
+ _cc=${#_desc}; LC_ALL=C _bc=${#_desc}; unset LC_ALL
657
+ (( _bc != _cc )) && _has_nonascii=1
658
+ fi
659
+ fi
660
+ done <<< "$issues_data"
621
661
 
622
- # Default priority and type if not set
623
- [[ -z "$priority" || "$priority" == "null" ]] && priority="P3"
624
- [[ -z "$issue_type" || "$issue_type" == "null" ]] && issue_type=""
662
+ local _n=${#_ids}
625
663
 
626
- # Title line (with spinner for in_progress)
627
- local spinner_prefix=""
628
- local spinner_w=0
629
- if [[ "$st" == "in_progress" ]]; then
630
- spinner_prefix="${SPINNER_FRAMES[$((SPINNER_IDX % ${#SPINNER_FRAMES[@]} + 1))]} "
631
- spinner_w=2 # char(1) + space(1)
664
+ # --- Pass 2: Batch compute truncation + widths (single Python call) ---
665
+ local -a _short_titles _title_cws _short_descs _desc_cws
666
+
667
+ if (( _n > 0 )); then
668
+ if (( _has_nonascii )); then
669
+ # Build batch input: 2 lines per card (title, desc)
670
+ # Format: max_w<TAB>prefix<TAB>string
671
+ local _batch_input=""
672
+ for (( _i=1; _i<=_n; _i++ )); do
673
+ _batch_input+="${_title_max_ws[$_i]}"$'\t'"${_title_pfxs[$_i]}"$'\t'"${_titles[$_i]}"$'\n'
674
+ _batch_input+="${_desc_max_w}"$'\t'" "$'\t'"${_descs[$_i]}"$'\n'
675
+ done
676
+
677
+ # Single Python call: truncate each string and compute content width
678
+ local _batch_output
679
+ _coproc_batch_trunc "$_batch_input"
680
+ _batch_output="$_COPROC_RESULT"
681
+
682
+ local _li=0
683
+ while IFS=$'\t' read -r _tr _cw; do
684
+ ((_li++))
685
+ if (( _li % 2 == 1 )); then
686
+ _short_titles+=("$_tr"); _title_cws+=($_cw)
687
+ else
688
+ _short_descs+=("$_tr"); _desc_cws+=($_cw)
689
+ fi
690
+ done <<< "$_batch_output"
691
+ else
692
+ # All-ASCII fast path - no Python needed
693
+ for (( _i=1; _i<=_n; _i++ )); do
694
+ local _t="${_titles[$_i]}" _mw=${_title_max_ws[$_i]}
695
+ (( ${#_t} > _mw )) && _t="${_t:0:$_mw}"
696
+ _short_titles+=("$_t")
697
+ local _fc="${_title_pfxs[$_i]}${_t}"
698
+ _title_cws+=(${#_fc})
699
+
700
+ local _d="${_descs[$_i]}"
701
+ (( ${#_d} > _desc_max_w )) && _d="${_d:0:$_desc_max_w}"
702
+ _short_descs+=("$_d")
703
+ _desc_cws+=($((2 + ${#_d})))
704
+ done
632
705
  fi
633
- local prefix_w=$((4 + ${#id} + spinner_w)) # " #" + id_digits + " " + spinner
634
- local title_w=$((card_inner - prefix_w - 1)) # -1 right margin for width safety
635
- local short=$(truncate_str "$title" $title_w)
636
- local title_content=" ${spinner_prefix}#$id $short"
637
- local title_content_w=$(str_width "$title_content")
638
- local title_pad=$((card_inner - title_content_w))
706
+ fi
707
+
708
+ # --- Pass 3: Render cards ---
709
+ local shown=0
710
+ for (( _i=1; _i<=_n; _i++ )); do
711
+ local id="${_ids[$_i]}"
712
+ local priority="${_priorities[$_i]}"
713
+ local issue_type="${_types[$_i]}"
714
+
715
+ # Title line
716
+ local spinner_prefix=""
717
+ [[ "$st" == "in_progress" ]] && spinner_prefix="${SPINNER_FRAMES[$((SPINNER_IDX % ${#SPINNER_FRAMES[@]} + 1))]} "
718
+ local title_content=" ${spinner_prefix}#${id} ${_short_titles[$_i]}"
719
+ local title_pad=$((card_inner - ${_title_cws[$_i]}))
639
720
  (( title_pad < 0 )) && title_pad=0
640
721
 
641
- # Description line (dimmed, truncated)
642
- local desc_w=$((card_inner - 4))
643
- local desc_short=""
644
- if [[ -n "$desc" && "$desc" != "null" ]]; then
645
- desc_short=$(truncate_str "$desc" $desc_w)
646
- fi
647
- local desc_content=" $desc_short"
648
- local desc_content_w=$(str_width "$desc_content")
649
- local desc_pad=$((card_inner - desc_content_w))
722
+ # Description line
723
+ local desc_content=" ${_short_descs[$_i]}"
724
+ local desc_pad=$((card_inner - ${_desc_cws[$_i]}))
650
725
  (( desc_pad < 0 )) && desc_pad=0
651
726
 
652
- # Priority and type tags on same line (e.g., [P0] [BUG])
727
+ # Priority and type tags
653
728
  local priority_tag="[$priority]"
654
729
  local priority_color="${PRIORITY_COLOR[$priority]:-$A_DIM}"
655
- local type_tag=""
656
- local type_color=""
657
- local tags_content=""
658
- local tags_w=0
730
+ local type_tag="" type_color="" tags_w=0
659
731
  if [[ -n "$issue_type" ]]; then
660
732
  type_tag="[${TYPE_LABEL[$issue_type]:-$issue_type}]"
661
733
  type_color="${TYPE_COLOR[$issue_type]:-$A_DIM}"
662
- # Calculate total width: " [P1] [BUG]"
663
734
  tags_w=$((${#priority_tag} + 1 + ${#type_tag}))
664
735
  else
665
736
  tags_w=${#priority_tag}
666
737
  fi
667
- local tags_pad=$((card_inner - tags_w - 2)) # -2 for leading spaces
738
+ local tags_pad=$((card_inner - tags_w - 2))
668
739
 
669
740
  local border_color="$A_DIM"
670
741
  local text_color="$A_FG"
@@ -688,7 +759,14 @@ build_column_lines() {
688
759
 
689
760
  ((shown++))
690
761
  lines_used=$((lines_used + 5))
691
- done <<< "$issues_data"
762
+ done
763
+
764
+ # Overflow indicator
765
+ if (( count > _n )); then
766
+ local more_text=" +$((count - _n)) more..."
767
+ printf "${A_DIM}%s${A_RESET}%$((col_w - ${#more_text}))s\n" "$more_text" ""
768
+ ((lines_used++))
769
+ fi
692
770
 
693
771
  if (( count == 0 )); then
694
772
  local no_text=" No tasks"
@@ -785,11 +863,8 @@ draw_board() {
785
863
  fi
786
864
  done
787
865
  local -a _py_results
788
- _py_results=("${(@f)$(python3 -c "
789
- import unicodedata,sys
790
- for line in sys.stdin.read().rstrip('\n').split('\n'):
791
- print(sum(2 if unicodedata.east_asian_width(c) in 'FW' else 1 for c in line))
792
- " <<< "$_input")}")
866
+ _coproc_batch_width "$_input"
867
+ _py_results=("${(@f)_COPROC_RESULT}")
793
868
  # Map Python results back to width array
794
869
  local _pi=1
795
870
  for ((_idx=1; _idx<=${#all_widths[@]}; _idx++)); do
@@ -821,9 +896,10 @@ draw_footer() {
821
896
  }
822
897
 
823
898
  read_key() {
899
+ local _rk_timeout="${1:-0.5}"
824
900
  local key result=""
825
- # Timeout for spinner animation refresh (0.5s to prevent key drops)
826
- read -sk1 -t 0.5 key 2>/dev/null || { echo "timeout"; return; }
901
+ # Timeout for spinner animation refresh (default 0.5s)
902
+ read -sk1 -t "$_rk_timeout" key 2>/dev/null || { echo "timeout"; return; }
827
903
 
828
904
  if [[ "$key" == $'\e' ]]; then
829
905
  read -sk1 -t 0.1 c2 2>/dev/null
@@ -967,6 +1043,7 @@ delete_issue() {
967
1043
 
968
1044
  level1_columns() {
969
1045
  IN_TUI=true
1046
+ _start_coproc
970
1047
  local col=0 card=0
971
1048
 
972
1049
  # Hide cursor and disable input echo
@@ -204,6 +204,16 @@ After approval: Delete issue from viban TUI
204
204
 
205
205
  ---
206
206
 
207
+ ## CRITICAL: Status Transition Rule
208
+
209
+ > **NEVER end this command with the issue still in `in_progress`.**
210
+ >
211
+ > Before exiting — whether you completed all phases or stopped early due to errors:
212
+ > ```bash
213
+ > viban review $ISSUE_ID
214
+ > ```
215
+ > This is MANDATORY. If you skip this, the board becomes stale and misleading.
216
+
207
217
  ## CLI Reference
208
218
 
209
219
  | Command | Description |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-plugin-viban",
3
- "version": "1.0.34",
3
+ "version": "1.0.35",
4
4
  "description": "Terminal Kanban TUI for AI-human collaborative issue tracking",
5
5
  "main": "bin/viban",
6
6
  "bin": {
@@ -0,0 +1,72 @@
1
+ #!/usr/bin/env python3
2
+ """Persistent Python coprocess for TUI rendering.
3
+
4
+ Stays resident and handles Unicode display width calculations
5
+ via stdin/stdout protocol, eliminating per-frame Python spawn overhead.
6
+
7
+ Protocol:
8
+ BATCH_TRUNC - truncate strings + compute display widths
9
+ BATCH_WIDTH - compute display widths only
10
+ QUIT - shutdown
11
+ """
12
+
13
+ import sys
14
+ import unicodedata
15
+
16
+
17
+ def display_width(s):
18
+ return sum(2 if unicodedata.east_asian_width(c) in 'FW' else 1 for c in s)
19
+
20
+
21
+ def truncate(s, max_w):
22
+ w = 0
23
+ for i, c in enumerate(s):
24
+ cw = 2 if unicodedata.east_asian_width(c) in 'FW' else 1
25
+ if w + cw > max_w:
26
+ return s[:i]
27
+ w += cw
28
+ return s
29
+
30
+
31
+ def handle_batch_trunc():
32
+ results = []
33
+ for line in sys.stdin:
34
+ line = line.rstrip('\n')
35
+ if line == 'END':
36
+ break
37
+ if not line:
38
+ continue
39
+ parts = line.split('\t', 2)
40
+ max_w = int(parts[0])
41
+ pfx = parts[1]
42
+ txt = parts[2] if len(parts) > 2 else ''
43
+ t = truncate(txt, max_w)
44
+ results.append(f'{t}\t{display_width(pfx + t)}')
45
+ sys.stdout.write('\n'.join(results) + '\nEND\n')
46
+ sys.stdout.flush()
47
+
48
+
49
+ def handle_batch_width():
50
+ results = []
51
+ for line in sys.stdin:
52
+ line = line.rstrip('\n')
53
+ if line == 'END':
54
+ break
55
+ results.append(str(display_width(line)))
56
+ sys.stdout.write('\n'.join(results) + '\nEND\n')
57
+ sys.stdout.flush()
58
+
59
+
60
+ def main():
61
+ for line in sys.stdin:
62
+ cmd = line.strip()
63
+ if cmd == 'QUIT':
64
+ break
65
+ elif cmd == 'BATCH_TRUNC':
66
+ handle_batch_trunc()
67
+ elif cmd == 'BATCH_WIDTH':
68
+ handle_batch_width()
69
+
70
+
71
+ if __name__ == '__main__':
72
+ main()
@@ -205,6 +205,16 @@ After approval: Delete issue from viban TUI
205
205
 
206
206
  ---
207
207
 
208
+ ## CRITICAL: Status Transition Rule
209
+
210
+ > **NEVER end this skill with the issue still in `in_progress`.**
211
+ >
212
+ > Before exiting — whether you completed all phases or stopped early due to errors:
213
+ > ```bash
214
+ > viban review $ISSUE_ID
215
+ > ```
216
+ > This is MANDATORY. If you skip this, the board becomes stale and misleading.
217
+
208
218
  ## CLI Reference
209
219
 
210
220
  | Command | Description |