Conversation
- no alternate screen - no bells, no whistles, only assistant messages and tool calls Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
| } | ||
|
|
||
| func (e *Editor) HandleInput(data string) { | ||
| if strings.HasPrefix(data, pastePrefix) && strings.HasSuffix(data, pasteSuffix) { |
There was a problem hiding this comment.
🔴 HIGH SEVERITY - CONFIRMED
Undefined constants pastePrefix and pasteSuffix
This code references constants that are never defined, causing a compilation error:
if strings.HasPrefix(data, pastePrefix) && strings.HasSuffix(data, pasteSuffix) {
e.insertString(strings.TrimSuffix(strings.TrimPrefix(data, pastePrefix), pasteSuffix))Impact: Code will not compile.
Fix: Define these constants (likely ANSI bracketed paste sequences):
const (
pastePrefix = "\x1b[200~"
pasteSuffix = "\x1b[201~"
)| } | ||
|
|
||
| func (t *ProcessTerminal) emit(data string) { | ||
| if t.onInput != nil { |
There was a problem hiding this comment.
🔴 HIGH SEVERITY - CONFIRMED
Data race on onInput field
The onInput callback is accessed concurrently without synchronization:
- Written in
Start()(line 58) andDrainInput()(lines 105-107) - Read in
emit()(line 238) from thereadLoopgoroutine
Impact: Race detector will fail; potential crashes or missed input events.
Fix: Protect with a mutex:
type ProcessTerminal struct {
mu sync.RWMutex
onInput func(string)
onResize func()
// ...
}
func (t *ProcessTerminal) emit(data string) {
t.mu.RLock()
handler := t.onInput
t.mu.RUnlock()
if handler != nil {
handler(data)
}
}| w, h := t.Columns(), t.Rows() | ||
| if w != lastW || h != lastH { | ||
| lastW, lastH = w, h | ||
| if t.onResize != nil { |
There was a problem hiding this comment.
🔴 HIGH SEVERITY - CONFIRMED
Data race on onResize field
The onResize callback is accessed concurrently without synchronization:
- Written in
Start()(line 59) - Read in
resizeLoop()(line 167) from a separate goroutine
Impact: Race detector will fail; potential crashes or missed resize events.
Fix: Use the same mutex as onInput (see comment on line 238).
| return lines | ||
| } | ||
|
|
||
| func (t *TUI) doRender() { |
There was a problem hiding this comment.
🔴 HIGH SEVERITY - CONFIRMED
Data race on rendering state fields
The doRender() method reads and writes multiple fields (lines, cursorRow, cursorCol, viewportStart, etc.) without mutex protection, while these fields can be modified by other methods like SetContent() and MoveCursor().
Impact: Race detector will fail; potential rendering corruption or crashes.
Fix: Add a mutex to the TUI struct and lock it in doRender():
type TUI struct {
mu sync.Mutex
// ... existing fields
}
func (t *TUI) doRender() {
t.mu.Lock()
defer t.mu.Unlock()
// ... existing logic
}Also protect other methods that modify these fields.
| default: | ||
| return false | ||
| } | ||
| _ = a.application.ResumeElicitation(context.Background(), action, nil) |
There was a problem hiding this comment.
🟡 MEDIUM SEVERITY - CONFIRMED
ResumeElicitation uses context.Background() instead of app context
This call ignores the app's cancellation context and discards errors:
_ = a.application.ResumeElicitation(context.Background(), action, nil)Impact: Operation won't be cancelled when the app shuts down; errors are silently ignored.
Fix: Use the app's context and log errors:
if err := a.application.ResumeElicitation(a.ctx, action, nil); err != nil {
log.Printf("ResumeElicitation failed: %v", err)
}| var buffer strings.Builder | ||
| buffer.WriteString("\x1b[?2026h") | ||
| prevViewportBottom := prevViewportTop + height - 1 | ||
| moveTargetRow := firstChanged |
There was a problem hiding this comment.
🟡 MEDIUM SEVERITY - CONFIRMED
Negative moveTargetRow when appendStart=true and firstChanged=0
When appendStart is true, the code decrements firstChanged:
moveTargetRow := firstChanged
if appendStart {
moveTargetRow--
}If firstChanged is 0, moveTargetRow becomes -1, which is invalid for ANSI cursor positioning.
Impact: Incorrect terminal output or rendering glitches.
Fix: Clamp to 0:
moveTargetRow := maxInt(0, firstChanged)
if appendStart && moveTargetRow > 0 {
moveTargetRow--
}| viewportTop := maxInt(0, totalLines-height) | ||
| targetRow := pos.Row | ||
| currentScreenRow := t.hardwareCursorRow - viewportTop | ||
| targetScreenRow := targetRow - viewportTop |
There was a problem hiding this comment.
🟡 MEDIUM SEVERITY - CONFIRMED
Negative screen row in positionHardwareCursor
When the cursor is above the viewport, targetScreenRow can be negative:
targetScreenRow := targetRow - viewportTop
delta := targetScreenRow - currentScreenRowImpact: Invalid ANSI escape sequences sent to the terminal.
Fix: Clamp to viewport bounds:
targetScreenRow := maxInt(0, minInt(height-1, targetRow - viewportTop))| for { | ||
| select { | ||
| case <-t.stopCh: | ||
| return |
There was a problem hiding this comment.
🟡 MEDIUM SEVERITY - LIKELY
Potential goroutine leak in readLoop
The readLoop goroutine may not exit cleanly if Stop() is called while blocked on I/O:
func (t *ProcessTerminal) readLoop() {
for {
select {
case <-t.stopCh:
return
default:
}
// Blocking read - may not check stopCh for a long time
data, err := t.reader.ReadString('\n')
// ...
}
}Impact: Goroutine may not terminate until the next I/O operation completes.
Fix: Use a context-aware reader or set a read deadline:
t.pty.SetReadDeadline(time.Now().Add(100 * time.Millisecond))| runes := []rune(ansi.Strip(text)) | ||
| half := (width - 1) / 2 | ||
| head := string(runes[:minInt(len(runes), half)]) | ||
| tailLen := min(len(runes), maxInt(1, width-ansi.StringWidth(head)-1)) |
There was a problem hiding this comment.
🟡 MEDIUM SEVERITY - LIKELY
Potential out-of-bounds slice in middleTruncate
When the text contains wide characters (e.g., emoji, CJK), ansi.StringWidth counts display width, not rune count. This can cause tailLen to exceed len(runes):
tailLen := min(len(runes), maxInt(1, width-ansi.StringWidth(head)-1))
tail := string(runes[len(runes)-tailLen:])Impact: Panic on slice out-of-bounds.
Fix: Add a bounds check:
tailLen := min(len(runes), maxInt(1, width-ansi.StringWidth(head)-1))
if tailLen > len(runes) {
tailLen = len(runes)
}
tail := string(runes[len(runes)-tailLen:])
With diffing renderer that only updates what need to be updated.
I am very unsure about the name...
Kapture.2026-03-24.at.20.14.21.mp4