Skip to content

fix(prompt): add null check for input to prevent runtime errors#1

Merged
pinion05 merged 5 commits into
devfrom
fix(prompt)_addnullcheck#7465
Jan 10, 2026
Merged

fix(prompt): add null check for input to prevent runtime errors#1
pinion05 merged 5 commits into
devfrom
fix(prompt)_addnullcheck#7465

Conversation

@pinion05

@pinion05 pinion05 commented Jan 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • Added null checks and optional chaining (input?.) for the TextareaRenderable ref in Prompt component.
  • Prevents runtime errors ("Cannot read properties of null") when the component unmounts before async operations (setTimeout, events) complete.
  • Fixed pre-push husky hook to explicitly include ~/.bun/bin in PATH to avoid "bun: command not found" errors in some environments.

Issue

  • Fixes potential crash during copy/paste or rapid navigation.

Summary by CodeRabbit

  • Bug Fixes
    • Improved stability of the prompt UI by adding presence checks and early exits so input-related actions (typing, paste, stash/open editor flows, cursor and marker updates) are skipped until the editor/input is available. Prevents crashes and ensures consistent behavior across submit, selection, paste and editor integration paths.

✏️ Tip: You can customize this high-level summary in your review settings.

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@coderabbitai

coderabbitai Bot commented Jan 10, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Added presence checks and optional chaining throughout the prompt React component to prevent operations on an uninitialized input. Guards were applied to event handlers, editor integrations, paste handling, stash/open flows, extmark sync, and ref accessors without changing public APIs.

Changes

Cohort / File(s) Summary
Prompt component defensive guards
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
Introduced null/undefined checks and optional chaining before input access across handlers and flows (onSubmit, paste, PromptAppend, stash/open editor, extmark sync, cursor/layout operations, focus, setText). Early returns added where input absence would previously cause runtime errors. No exported signature changes.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nibble at bugs with careful paws,
Guarding input sing-song without a pause.
Optional chains and checks align,
Now the prompt runs safe — hopping fine. ✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description lacks the required template structure with explicit 'What does this PR do?' and 'How did you verify your code works?' sections; information is present but unstructured. Restructure the description to follow the template: clearly label 'What does this PR do?' section covering null checks and husky fix, and add 'How did you verify your code works?' with test verification steps.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding null checks for the input ref to prevent runtime errors in the Prompt component.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (5)

203-206: Missing null check - potential runtime error.

Line 205 accesses input.focused without optional chaining, which could cause the same "Cannot read properties of null" error this PR aims to fix.

🐛 Proposed fix
           if (autocomplete.visible) return
-          if (!input.focused) return
+          if (!input?.focused) return
           // TODO: this should be its own command

446-456: Missing null checks in DialogStash callback.

Lines 449-452 access input without null checks, which could cause runtime errors if the component unmounts while the dialog is open.

🐛 Proposed fix
         dialog.replace(() => (
           <DialogStash
             onSelect={(entry) => {
+              if (!input) return
               input.setText(entry.input)
               setStore("prompt", { input: entry.input, parts: entry.parts })
               restoreExtmarksFromParts(entry.parts)
               input.gotoBufferEnd()
             }}
           />
         ))

808-817: Missing null checks in keyboard handler.

Lines 809-810 access input without optional chaining or null checks, which could cause runtime errors.

🐛 Proposed fix
               if (keybind.match("input_clear", e) && store.prompt.input !== "") {
-                 input.clear()
-                 input.extmarks.clear()
+                 input?.clear()
+                 input?.extmarks.clear()
                 setStore("prompt", {
                   input: "",
                   parts: [],
                 })

826-837: Missing null checks for input property accesses.

Lines 826, 832 access input.visualCursor.offset without null checks, which could cause runtime errors if input is null.

🐛 Proposed fix
-              if (e.name === "!" && input.visualCursor.offset === 0) {
+              if (e.name === "!" && input?.visualCursor.offset === 0) {
                 setStore("mode", "shell")
                 e.preventDefault()
                 return
               }
               if (store.mode === "shell") {
-                if ((e.name === "backspace" && input.visualCursor.offset === 0) || e.name === "escape") {
+                if ((e.name === "backspace" && input?.visualCursor.offset === 0) || e.name === "escape") {
                   setStore("mode", "normal")
                   e.preventDefault()
                   return

839-862: Missing null checks in history navigation.

Lines 841-842, 853-854, 859-861 access input.cursorOffset, input.plainText, input.visualCursor.visualRow, and input.height without null checks.

🐛 Proposed fix
               if (store.mode === "normal") autocomplete.onKeyDown(e)
               if (!autocomplete.visible) {
                 if (
-                  (keybind.match("history_previous", e) && input.cursorOffset === 0) ||
-                  (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length)
+                  (keybind.match("history_previous", e) && input?.cursorOffset === 0) ||
+                  (keybind.match("history_next", e) && input && input.cursorOffset === input.plainText.length)
                 ) {
                   const direction = keybind.match("history_previous", e) ? -1 : 1
-                  const item = history.move(direction, input.plainText)
+                  const item = input ? history.move(direction, input.plainText) : null
 
                   if (item) {
-                    input.setText(item.input)
+                    input?.setText(item.input)
                     setStore("prompt", item)
                     setStore("mode", item.mode ?? "normal")
                     restoreExtmarksFromParts(item.parts)
                     e.preventDefault()
-                    if (direction === -1) input.cursorOffset = 0
-                    if (direction === 1) input.cursorOffset = input.plainText.length
+                    if (input) {
+                      if (direction === -1) input.cursorOffset = 0
+                      if (direction === 1) input.cursorOffset = input.plainText.length
+                    }
                   }
                   return
                 }
 
-                if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) input.cursorOffset = 0
-                if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1)
-                  input.cursorOffset = input.plainText.length
+                if (keybind.match("history_previous", e) && input?.visualCursor.visualRow === 0 && input) 
+                  input.cursorOffset = 0
+                if (keybind.match("history_next", e) && input?.visualCursor.visualRow === input.height - 1 && input)
+                  input.cursorOffset = input.plainText.length
               }
🧹 Nitpick comments (1)
.husky/pre-push (1)

2-3: LGTM! Consider documenting the assumption.

The PATH export correctly prepends Bun's binary location. This assumes Bun is installed in the default location (~/.bun/bin). If team members use custom Bun installations (e.g., via asdf, mise, or other version managers), this may not work for them.

Optional: Make the path configurable

Consider checking multiple common locations or documenting this requirement:

 #!/bin/sh
+# Note: Assumes Bun is installed in ~/.bun/bin
+# If using a version manager, ensure ~/.bun/bin exists or modify this path
 export PATH="$HOME/.bun/bin:$PATH"
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5c74bff and d06e8c1.

📒 Files selected for processing (2)
  • .husky/pre-push
  • packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (2)
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx (1)
  • input (1320-1322)
packages/opencode/src/session/prompt.ts (1)
  • prompt (150-179)
🔇 Additional comments (13)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (13)

94-101: Good defensive coding for async operations.

The optional chaining prevents crashes if the component unmounts before the setTimeout callback executes.


103-108: LGTM!

Properly guards the effect from running when input is not yet initialized.


163-164: LGTM!

Proper null-safe access to input methods.


250-250: LGTM!

Proper null checks for async editor operations.

Also applies to: 309-309


315-318: LGTM!

Safely handles focus/blur when input might not be initialized.


324-326: LGTM!

The early return guard is the right pattern for functions that require input to be available.


415-416: LGTM!

Proper null checks for stash operations.

Also applies to: 430-435


460-493: LGTM!

The ref API properly handles null input with optional chaining and appropriate fallbacks.


519-519: LGTM!

Proper null-safe handling in the submit flow.

Also applies to: 601-601, 617-617


621-623: LGTM!

The early return guard prevents operations on null input.


656-658: LGTM!

The early return guard prevents operations on null input.


926-932: LGTM!

Proper null check for the async layout update after paste.


934-943: LGTM!

Proper null checks when initializing the textarea with stored prompt data.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (3)

787-869: Fix optional chaining and input validation in history navigation and mode switching.

Two issues exist in this keydown handler:

  1. Line 843: input?.cursorOffset === input?.plainText.length evaluates to undefined === undefined (true) when input is undefined, allowing the condition to pass. The subsequent if (!input) return on line 845 comes too late.

  2. Lines 827, 833, 861, 865: input?.visualCursor.offset and input?.visualCursor.visualRow use incomplete optional chaining. If visualCursor is undefined but input is defined, accessing .offset or .visualRow will throw a TypeError.

Proposed fix
 if (!autocomplete.visible) {
   if (
-    (keybind.match("history_previous", e) && input?.cursorOffset === 0) ||
-    (keybind.match("history_next", e) && input?.cursorOffset === input?.plainText.length)
+    input &&
+    ((keybind.match("history_previous", e) && input.cursorOffset === 0) ||
+      (keybind.match("history_next", e) && input.cursorOffset === input.plainText.length))
   ) {
-    if (!input) return
     const direction = keybind.match("history_previous", e) ? -1 : 1
     const item = history.move(direction, input.plainText)
     ...
   }

-  if (keybind.match("history_previous", e) && input?.visualCursor.visualRow === 0 && input)
+  if (keybind.match("history_previous", e) && input?.visualCursor?.visualRow === 0 && input)
     input.cursorOffset = 0

   if (
     keybind.match("history_next", e) &&
-    input?.visualCursor.visualRow === (input?.height ?? 0) - 1 &&
+    input?.visualCursor?.visualRow === (input?.height ?? 0) - 1 &&
     input
   )
     input.cursorOffset = input.plainText.length
 }

622-699: Add optional chaining guard for visualCursor.offset to match pattern used elsewhere in the file.

Lines 624 and 659 access input.visualCursor.offset directly after checking if (!input), but elsewhere (lines 827, 833, 861, 865) the same property chain uses optional chaining (input?.visualCursor.offset). Ensure defensive programming by using optional chaining consistently, or guard visualCursor explicitly. Note: the proposed fallback to input.cursorOffset may not be semantically equivalent since cursorOffset is a settable property while visualCursor.offset appears to be read-only.


56-60: Fix optional chaining depth on getLayoutNode(): input?.getLayoutNode()?.markDirty()

Line 97: input?.getLayoutNode().markDirty() will still throw if getLayoutNode() returns null during unmount—the ?. only guards input, not the method's return value. Use deep optional chaining: input?.getLayoutNode()?.markDirty().

The same issue appears in the paste handler around line 300+ where a safer if (input) guard is already in place. Apply the fix consistently throughout.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d06e8c1 and ce10e1d.

📒 Files selected for processing (1)
  • packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
🔇 Additional comments (9)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (9)

103-108: Cursor color guarding looks good.


155-179: Command handlers: good defensive guards for focus/input presence.

Also applies to: 203-227


250-311: “Open editor” flow: guards look good; cursorOffset update is appropriately conditional.


324-367: restoreExtmarksFromParts(): early return is the right move.


403-459: Stash flows: safe input access + consistent store reset/restore.


461-494: PromptRef surface: null-safe and behavior-preserving.


496-619: submit(): safer extmark access + clears are now crash-resistant.


941-949: Textarea ref init: good restoration guard.


932-939: Verify if getLayoutNode() can return null/undefined.

The code at line 935 calls input.getLayoutNode().markDirty() without guarding the result of getLayoutNode(). The proposed fix to use optional chaining (?.markDirty()) assumes the method can return null or undefined. However, this cannot be verified without access to the @opentui/core v0.1.70 type definitions, which are not available in the repository. Note that another usage at line ~826 in the same file also chains directly on getLayoutNode() without guarding the result, suggesting the return type may be guaranteed non-null—but this is not definitive.

@pinion05

Copy link
Copy Markdown
Owner Author

@coderabbitai 근데 이렇게 nullabel 처리만 하면 데이터 누락생기는거 아니야?

@coderabbitai

coderabbitai Bot commented Jan 10, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Read the prompt component file to understand the changes
cat packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx

Length of output: 37015


@pinion05

정확히 지적하신 부분이 맞습니다. 현재 방식은 런타임 에러는 막지만, 사용자 액션이나 데이터가 조용히 무시될 수 있는 문제가 있습니다.

데이터 누락이 발생할 수 있는 주요 시나리오


🏁 Script executed:

#!/bin/bash
# Find all the places where operations are silently skipped with optional chaining or early returns
rg -n "if \(!input\) return" packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
rg -n "input\?\." packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx | head -20

Length of output: 951


좋습니다. 구체적으로 어떤 문제가 있는지 확인했습니다:

🚨 데이터 누락 위험 지점

1. 이벤트 기반 텍스트 삽입 (Line 95-99)

sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
  input?.insertText(evt.properties.text)  // input이 null이면 텍스트 손실
  setTimeout(() => {
    input?.getLayoutNode().markDirty()
    input?.gotoBufferEnd()
    renderer.requestRender()
  }, 0)
})
  • 다른 컴포넌트가 텍스트를 append하려 할 때 input이 null이면 텍스트가 영구 손실
  • 사용자는 액션이 무시됐는지 알 수 없음

2. Paste 작업 (Line 623, 658)

function pasteText(text: string, virtualText: string) {
  if (!input) return  // 붙여넣기가 조용히 실패
  // ... 실제 paste 로직
}

async function pasteImage(file: {...}) {
  if (!input) return  // 이미지 업로드 손실
  // ... 실제 image paste 로직
}
  • 사용자가 이미지나 긴 텍스트를 붙여넣었는데 input이 null이면 데이터가 사라짐
  • 에러 메시지나 피드백 없음

3. Stash/History 복원 (Line 430, 449)

onSelect: (dialog) => {
  if (!input) return  // stash 복원 실패
  input.setText(entry.input)
  setStore("prompt", { input: entry.input, parts: entry.parts })
  restoreExtmarksFromParts(entry.parts)
  input.gotoBufferEnd()
}
  • 사용자가 저장된 프롬프트를 복원하려 했는데 조용히 실패
  • store에는 상태가 업데이트되지만 UI에는 반영 안 됨

4. Submit 시점 (Line 520)

const allExtmarks = input?.extmarks.getAllForTypeId(promptPartTypeId) ?? []
  • extmarks가 제대로 안 가져와지면 첨부 파일이 누락된 채로 전송

💡 올바른 해결 방법

단순 null check 대신 다음 접근이 필요합니다:

A. 초기화 완료 보장

const [inputReady, setInputReady] = createSignal(false)

// ref 콜백에서
ref={(r: TextareaRenderable) => {
  input = r
  setInputReady(true)
  // ... 기존 초기화 로직
}}

B. 작업 큐잉

const pendingOperations = new Queue<() => void>()

sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
  const operation = () => {
    input.insertText(evt.properties.text)
    // ...
  }
  
  if (!input) {
    pendingOperations.enqueue(operation)
  } else {
    operation()
  }
})

// input이 준비되면 큐 처리
createEffect(() => {
  if (inputReady()) {
    while (!pendingOperations.isEmpty()) {
      pendingOperations.dequeue()()
    }
  }
})

C. 사용자 피드백

function pasteText(text: string, virtualText: string) {
  if (!input) {
    toast.show({
      variant: "error",
      message: "Input not ready. Please try again.",
      duration: 2000
    })
    return
  }
  // ... 실제 로직
}

D. 상태와 DOM 분리

// store에는 항상 저장
setStore("prompt", "input", value)
setStore("prompt", "parts", [...parts, newPart])

// DOM 업데이트는 input이 있을 때만
createEffect(() => {
  if (input && inputReady()) {
    input.setText(store.prompt.input)
    restoreExtmarksFromParts(store.prompt.parts)
  }
})

🔍 테스트 시나리오

다음 상황에서 데이터 손실 여부를 확인해야 합니다:

  1. 컴포넌트 mount 전에 TuiEvent.PromptAppend 발생
  2. 빠른 페이지 전환 중 stash 복원
  3. Editor에서 긴 텍스트 반환 직후 unmount
  4. Paste 이벤트와 동시에 route navigation

현재 PR은 크래시는 막지만 이런 경우들에서 데이터가 조용히 사라질 수 있습니다. 위의 개선 방법 중 최소한 작업 큐잉이나 사용자 피드백은 추가하는 게 좋을 것 같습니다.

@pinion05

pinion05 commented Jan 10, 2026

Copy link
Copy Markdown
Owner Author

@gemini-code-assist 리뷰해줘
/gemini

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively addresses the risk of runtime errors by adding null checks for the input ref throughout the Prompt component. The changes, primarily using optional chaining and if guards, are appropriate and well-executed. I've included a few suggestions to further simplify some of the conditional logic, which will enhance readability and maintainability. Overall, this is a solid fix that improves the component's stability.

Comment on lines +104 to +107
if (input) {
if (props.disabled) input.cursorColor = theme.backgroundElement
if (!props.disabled) input.cursorColor = theme.text
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The nested if statements can be simplified into a single expression using a ternary operator. This makes the code more concise and easier to read.

    if (input) {
      input.cursorColor = props.disabled ? theme.backgroundElement : theme.text
    }

Comment on lines +861 to 868
if (keybind.match("history_previous", e) && input?.visualCursor.visualRow === 0 && input)
input.cursorOffset = 0
if (
keybind.match("history_next", e) &&
input?.visualCursor.visualRow === (input?.height ?? 0) - 1 &&
input
)
input.cursorOffset = input.plainText.length

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This logic can be simplified by wrapping it in a single if (input) block. This improves readability by reducing redundant checks and making the code cleaner.

                  if (input) {
                    if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) {
                      input.cursorOffset = 0
                    }
                    if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1) {
                      input.cursorOffset = input.plainText.length
                    }
                  }

@sonarqubecloud

Copy link
Copy Markdown

@pinion05

Copy link
Copy Markdown
Owner Author

/ask 해당 pr 은 크래시를 방지하는 수준의 작업범위로 산정하는것이 올바를까?

@qodo-code-review

Copy link
Copy Markdown

Ask

해당 pr 은 크래시를 방지하는 수준의 작업범위로 산정하는것이 올바를까?

Answer:

결론부터 말하면, “크래시를 방지하는 수준(안정화/가드 추가)”의 작업범위로 산정하는 것이 대체로 타당합니다. 이 PR의 핵심 변경은 기능 추가/로직 변경이라기보다, 이미 존재하던 비동기 콜백·이벤트 경로에서 input(TextareaRenderable ref)이 null이 될 수 있는 타이밍을 방어해서 “Cannot read properties of null” 류의 런타임 크래시를 막는 성격이 강합니다.

근거(디프 기준):

  • input.insertText, input.extmarks.*, input.cursorOffset, input.visualCursor.*, input.getLayoutNode().markDirty()여러 경로에서 input 접근을 optional chaining/early return으로 감쌈 → 언마운트/참조 미설정 시 크래시 방지.
  • setTimeout 내부에서도 input null 가능성을 고려해 input?.getLayoutNode?.()?.markDirty()처럼 방어 → 특히 “언마운트 후 setTimeout 실행” 패턴은 대표적인 크래시 원인.
  • pasteText, pasteImage, restoreExtmarksFromPartsif (!input) return 추가 → 입력 컴포넌트가 없으면 동작을 스킵하도록 변경(크래시 방지 목적).

다만 산정 시 “순수 크래시 방지”로만 보기 애매한 포인트가 2가지 있습니다(범위가 커 보이진 않지만, 리스크/영향은 있음):

  1. 동작 변화(스킵): 이전에는 input이 있다고 가정하고 수행하던 동작들이 이제는 input이 없으면 조용히 return/skip 됩니다. 즉 크래시는 없어지지만, 특정 타이밍에는 “명령이 먹지 않는” 상태가 될 수 있습니다(의도된 트레이드오프).
  2. 초기화 동작 추가: ref={(r) => { input = r; if (store.prompt.input && input) { input.setText(...); restoreExtmarksFromParts(...) } }}가 추가되어, ref 세팅 시점에 store 상태를 input에 다시 주입합니다. 이것도 안정화 목적이지만, “재마운트/리렌더링” 타이밍에서 렌더/상태 동기화 방식이 달라질 수 있어 QA 포인트는 생깁니다.

정리하면:

  • 작업의 의도/내용은 안정화(크래시 방지) 패치로 보는 게 맞고,
  • 다만 “가드 추가로 인한 일부 기능 스킵 + ref 초기화 로직 추가”가 있으니, 산정 라벨을 굳이 나눈다면 ‘크래시 방지(안정화) + 경미한 동작 변경/회복 로직’ 정도로 보는 게 가장 정확합니다.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (3)

320-322: Potential crash: input may not be assigned when onMount executes.

The input ref is assigned in the textarea's ref callback (line 942), but the execution order between ref callbacks and onMount isn't strictly guaranteed in all scenarios. If input is undefined here, this will throw.

Consider adding a guard or using an effect that waits for input to be defined:

Suggested fix
 onMount(() => {
-  promptPartTypeId = input.extmarks.registerType("prompt-part")
+  if (input) {
+    promptPartTypeId = input.extmarks.registerType("prompt-part")
+  }
 })

Alternatively, move this registration into the ref callback where input is guaranteed to exist.


369-371: Missing null check causes potential crash.

syncExtmarksWithPromptParts accesses input.extmarks directly without a null guard, unlike other functions in this PR. This will throw if input is undefined when called from onContentChange (line 784).

Proposed fix
 function syncExtmarksWithPromptParts() {
+  if (!input) return
   const allExtmarks = input.extmarks.getAllForTypeId(promptPartTypeId)

448-454: User action silently ignored when input is null.

If the user selects a stash entry but input is null, the function returns early without applying the selection or providing feedback. The selected entry is effectively lost from the user's perspective.

Consider showing a toast notification or keeping the dialog open with an error state when the operation cannot complete.

🤖 Fix all issues with AI agents
In @packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx:
- Around line 474-481: The setter set(prompt) writes the prompt to state via
setStore("prompt", prompt) but skips updating the UI when input is null, causing
ref.set(prompt) to appear only partially applied; change the logic so when input
is null you still persist the desired prompt and schedule/apply the UI updates
once input becomes available (e.g., add an effect or enqueue a pendingPrompt
that calls input.setText(prompt.input), restoreExtmarksFromParts(prompt.parts)
and input.gotoBufferEnd() when input is initialized), or at minimum document
that set() only updates store until input exists; reference the set(prompt)
method, setStore("prompt", prompt), input, restoreExtmarksFromParts and
input.gotoBufferEnd() when making the change.
- Around line 622-624: In pasteText, avoid silently dropping pasted content when
input is null: either enqueue the paste (store text in a pendingPaste variable
or PasteQueue and apply it when input becomes non-null) or surface user feedback
(call the existing toast/notification helper, e.g., showToast or notify, with a
message like "Paste deferred — input not ready"). Update the pasteText function
to detect input === null and perform the chosen action (enqueue pending paste
tied to the input lifecycle or show a toast) and ensure any queued paste is
applied in the input initialization code (where input is set up).
- Around line 657-659: pasteImage currently returns early and silently drops
image data when input is null (same issue as pasteText); update pasteImage to
adopt the same mitigation used for pasteText—either enqueue the incoming file
for later processing when input becomes available or surface a user-visible
notification/error indicating the paste failed. Locate pasteImage and pasteText
(and the shared input/visualCursor.offset usage) and implement the chosen
strategy consistently so no image data is lost silently when input is null.
🧹 Nitpick comments (2)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (2)

861-868: Consider simplifying the conditional chain.

The multiple optional chaining operators make this condition hard to read. Consider extracting to a helper or using early guards.

Suggested simplification
-                  if (keybind.match("history_previous", e) && input?.visualCursor.visualRow === 0 && input)
-                    input.cursorOffset = 0
-                  if (
-                    keybind.match("history_next", e) &&
-                    input?.visualCursor.visualRow === (input?.height ?? 0) - 1 &&
-                    input
-                  )
+                  if (!input) return
+                  if (keybind.match("history_previous", e) && input.visualCursor.visualRow === 0) {
+                    input.cursorOffset = 0
+                  }
+                  if (keybind.match("history_next", e) && input.visualCursor.visualRow === input.height - 1) {
                     input.cursorOffset = input.plainText.length
+                  }

941-950: Minor: redundant && input check.

Since input = r is assigned on line 942 and r is always a valid TextareaRenderable when the ref callback fires, the && input check on line 943 is redundant.

Optional cleanup
              ref={(r: TextareaRenderable) => {
                input = r
-               if (store.prompt.input && input) {
+               if (store.prompt.input) {
                  input.setText(store.prompt.input)
                  restoreExtmarksFromParts(store.prompt.parts)
                }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ce10e1d and be329aa.

📒 Files selected for processing (1)
  • packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (3)
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx (1)
  • input (1320-1322)
packages/ui/src/context/data.tsx (1)
  • store (44-46)
packages/opencode/src/tool/webfetch.ts (2)
  • text (149-151)
  • text (160-164)
🔇 Additional comments (5)
packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx (5)

103-108: LGTM!

The guard correctly prevents accessing properties on a non-existent input. Since this is purely visual styling, no data loss concerns apply here.


155-178: LGTM!

The null checks here are appropriate:

  • For "Clear prompt", the command is already marked disabled: true indicating it requires additional conditions.
  • For "Submit prompt", the early return when input isn't focused is correct behavior.

324-326: LGTM, but callers may have inconsistent state.

The early return prevents crashes, but some callers (e.g., stash operations at lines 430-435) update the store before calling this function. If input is null, the store reflects new state while the UI doesn't. This is part of the broader silent data loss concern flagged in PR comments.


933-939: LGTM!

The guard in the setTimeout callback correctly handles the case where input may have been unmounted before the async callback executes. Since this is only for layout refresh after paste (not the paste itself), there's no data loss risk.


519-533: The pasted text expansion pattern is defensive but protected by the pasteText guard.

The optional chaining on input at line 520 correctly defaults to an empty array if input is null. However, this scenario cannot actually occur because pasteText (line 623) returns early if input is null, preventing any text parts from being added to store.prompt.parts in the first place. Since text parts can only be created when input is defined, the loop at lines 523-533 will only run when there are extmarks to process.

The defensive code is good practice, but there's no practical risk of data loss here.

Comment on lines 94 to 101
sdk.event.on(TuiEvent.PromptAppend.type, (evt) => {
input.insertText(evt.properties.text)
input?.insertText(evt.properties.text)
setTimeout(() => {
input.getLayoutNode().markDirty()
input.gotoBufferEnd()
input?.getLayoutNode?.()?.markDirty()
input?.gotoBufferEnd()
renderer.requestRender()
}, 0)
})

@coderabbitai coderabbitai Bot Jan 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent data loss when input is null.

If PromptAppend fires before the input is mounted or after it unmounts, evt.properties.text is silently dropped. Consider queuing pending text when input is unavailable and flushing when it becomes ready, or notifying the user that the operation could not be applied.

@pinion05 pinion05 Jan 10, 2026

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai
근거 = 텍스트 인풋은 단순 입력이기때문에 사용자경험에 있어 잠깐의 렉으로 느껴질뿐이지 오히려 큐로 처리하면 한번에 큐가 쏟아져 오히려 사용자 경험이 저해된다고 생각됨.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question:

근거 = 텍스트 인풋은 단순 입력이기때문에 사용자경험에 있어 잠깐의 렉으로 느껴질뿐이지 오히려 큐로 처리하면 한번에 큐가 쏟아져 오히려 사용자 경험이 저해된다고 생각됨.

Answer:

이 이슈( input 이 null 일 때 text가 “조용히” 유실되는 문제 )는 “큐잉”만이 해법이 아니라, UX 관점에서 더 안전한 대안(실패를 명시적으로 알리기/재시도 유도하기)이 충분히 합리적입니다.
앞으로 Qodo Merge가 유사 상황에서 “큐로 몰아서 처리” 같은 제안을 과도하게 밀지 않도록, /improve 제안 시 UX 트레이드오프(지연/버스트 입력/예상치 못한 일괄 반영)를 고려해서 실패 알림/드롭 방지(guard + user feedback) 쪽을 우선 제안하도록 가이딩할 수 있습니다.

아래처럼 pr_code_suggestions.extra_instructions에 프로젝트 선호(큐잉 지양, 명시적 실패 처리 선호)를 넣어두면 됩니다:

[pr_code_suggestions]
extra_instructions = """\
- UX 측면에서 '입력 이벤트를 큐에 쌓았다가 나중에 한 번에 flush' 하는 패턴은 지양한다(버스트 입력/예상치 못한 일괄 반영 위험).
- 대신 'silent data loss' 방지를 위해 다음 중 하나를 우선 제안한다:
  1) input이 준비되지 않았을 때 명시적으로 실패를 반환/토스트 등으로 사용자에게 안내
  2) 즉시 적용 불가 시 재시도 버튼/단일 최신값만 보존(last-write-wins) 같은 최소한의 보존 전략
  3) 안전한 guard + telemetry/logging으로 드롭을 관측 가능하게 만들기
- 해결책 제안 시 "사용자 경험(렉/지연/버스트)" trade-off를 짧게 설명한다.
"""

Relevant Sources:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipped: comment is from another GitHub bot.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@pinion05 I understand your reasoning. You've made a conscious trade-off: accepting that PromptAppend events might be dropped during mount/unmount transitions (perceived as brief lag) rather than implementing a queue that could flush all pending events at once and create a jarring UX.

That's a valid design decision for this use case. Thank you for clarifying the intent behind this approach.

Comment on lines 474 to 481
set(prompt) {
input.setText(prompt.input)
input?.setText(prompt.input)
setStore("prompt", prompt)
restoreExtmarksFromParts(prompt.parts)
input.gotoBufferEnd()
if (input) {
restoreExtmarksFromParts(prompt.parts)
input.gotoBufferEnd()
}
},

@coderabbitai coderabbitai Bot Jan 10, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

set() updates store but may not update UI.

When input is null, setStore("prompt", prompt) executes but setText, restoreExtmarksFromParts, and gotoBufferEnd are skipped. External callers using ref.set(prompt) may expect the prompt to be fully applied, but only the store is updated.

Consider documenting this behavior or ensuring the UI syncs when input becomes available (e.g., via an effect).

🤖 Prompt for AI Agents
In @packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx around lines
474 - 481, The setter set(prompt) writes the prompt to state via
setStore("prompt", prompt) but skips updating the UI when input is null, causing
ref.set(prompt) to appear only partially applied; change the logic so when input
is null you still persist the desired prompt and schedule/apply the UI updates
once input becomes available (e.g., add an effect or enqueue a pendingPrompt
that calls input.setText(prompt.input), restoreExtmarksFromParts(prompt.parts)
and input.gotoBufferEnd() when input is initialized), or at minimum document
that set() only updates store until input exists; reference the set(prompt)
method, setStore("prompt", prompt), input, restoreExtmarksFromParts and
input.gotoBufferEnd() when making the change.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요, pinion05님. 질문해주셔서 감사합니다. CodeRabbit 봇의 지적은 유효하며, input이 아직 초기화되지 않았을 때 set 메서드가 호출될 경우 UI 업데이트가 완전히 이루어지지 않을 수 있다는 점에 대한 것입니다.

현재 코드에서 setStore("prompt", prompt)input의 상태와 관계없이 항상 실행되어 프롬프트 상태를 저장합니다. 그리고 ref 콜백(파일 packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx의 대략 943-946번째 줄)에서 input이 사용 가능해지면 store.prompt의 내용을 input.setTextrestoreExtmarksFromParts를 통해 UI에 적용하고 있습니다.

하지만 set 메서드에서 input.gotoBufferEnd() 호출도 input이 null일 때 건너뛰어지는데, ref 콜백에서는 이 부분이 처리되지 않고 있습니다. 따라서 input이 나중에 초기화될 때 커서가 텍스트의 끝으로 이동하지 않을 수 있습니다.

가장 올바른 방향은 input이 사용 가능해질 때 gotoBufferEnd()도 함께 호출하여 UI 동기화를 완료하는 것입니다. ref 콜백을 다음과 같이 수정하는 것을 고려해볼 수 있습니다:

                if (store.prompt.input && input) {
                  input.setText(store.prompt.input)
                  restoreExtmarksFromParts(store.prompt.parts)
                  input.gotoBufferEnd() // 이 줄을 추가합니다.
                }

이렇게 하면 set 메서드가 input이 null일 때 호출되더라도, input이 나중에 초기화될 때 프롬프트의 내용과 커서 위치가 모두 올바르게 복원될 것입니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ask

@gemini-code-assist 그러면 어떤방향이 올바를까?

Answer:

결론부터 말하면 “store를 source of truth로 두고, input이 생기는 순간(store 값을) UI에 동기화” 하는 방향이 가장 안전하고 일관적입니다.

지금 PR에서 ref={(r) => { input = r; if (store.prompt.input && input) { ... }}}input이 초기화될 때 store → UI를 재적용하는 로직을 이미 추가하셨기 때문에, ref.set(prompt)가 input이 null인 시점에 호출되더라도 store에는 값이 남고, 나중에 input이 붙으면 UI가 따라오는 구조가 됩니다. 이 방향이 맞습니다.

다만 더 “정석”으로 정리하려면 아래 둘 중 하나를 추천합니다:

  1. 명시적인 동기화 effect 추가(권장)

    • “ref에서 input이 잡혔는지”를 신호로 만들고, input이 존재할 때마다 store 값이 UI에 반영되도록 createEffect로 보장합니다.
    • 이렇게 하면 ref 콜백에 로직이 몰리지 않고, set(), stash pop, editor open 등 다양한 경로에서도 동기화 일관성이 좋아집니다.
  2. 현재 방식 유지 + 동작을 문서화

    • set()은 항상 store는 업데이트하지만, input이 아직 없으면 UI 적용은 나중에(ref 연결 시점)에 일어난다”를 주석/문서로 명확히 해두는 방식입니다.
    • 이미 ref에서 store 값을 복원하는 코드가 들어갔으니, 최소한 이 의도를 남겨두면 오해가 줄어듭니다.

요약: CodeRabbit이 지적한 “UI가 안 바뀔 수 있음”은 input이 붙을 때 store에서 복원하는 동기화 장치가 없을 때의 문제인데, 이번 PR은 그 장치를 ref에서 추가했으니 방향은 옳습니다. 다만 유지보수 관점에서는 ref 콜백 대신 effect로 동기화를 일반화하는 쪽이 더 깔끔합니다.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/gemini
어떤방향의 개선이 이상적일까?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pinion05님, 좋은 질문 감사합니다. qodo-code-review[bot]님의 의견처럼, store를 "source of truth"로 유지하고 input이 사용 가능해지는 시점에 UI를 동기화하는 방식이 가장 이상적이고 견고한 접근 방식입니다.

현재 set 메서드는 input이 null일 때 setStore("prompt", prompt)만 실행하고, input이 존재할 때는 UI 업데이트를 시도합니다. 하지만 input의 초기화 시점과 set 메서드 호출 시점의 비동기적인 특성 때문에, UI가 완전히 동기화되지 않거나 불일치가 발생할 가능성이 있습니다.

가장 이상적인 개선 방향은 다음과 같습니다:

  1. set 메서드는 오직 store.prompt만 업데이트하도록 합니다. input에 직접 접근하는 로직은 모두 제거합니다. 이렇게 하면 set 메서드의 책임이 명확해지고, input의 존재 여부에 관계없이 항상 상태를 일관되게 업데이트할 수 있습니다.
  2. createEffect를 사용하여 store.promptinput의 변화에 반응하여 UI를 동기화합니다.effectinput 객체가 유효하고 store.prompt의 내용이 변경될 때마다 input.setText(), restoreExtmarksFromParts(), 그리고 input.gotoBufferEnd()를 호출하여 UI를 store의 최신 상태로 반영하도록 합니다.

이 접근 방식은 상태 관리와 UI 렌더링 로직을 명확하게 분리하여 코드의 예측 가능성과 유지보수성을 높여줍니다. input이 나중에 초기화되더라도 store의 최신 상태가 자동으로 UI에 반영되므로, input이 null일 때 set이 호출되어도 문제가 발생하지 않습니다.

따라서, set 메서드는 다음과 같이 수정하는 것이 좋습니다:

    set(prompt) {
      setStore("prompt", prompt)
    },

그리고 Prompt 컴포넌트 내부에 다음과 같은 createEffect를 추가하여 UI 동기화를 처리합니다 (예시):

  createEffect(() => {
    // input이 존재하고 store.prompt가 변경될 때마다 UI를 동기화합니다.
    if (input && store.prompt) {
      input.setText(store.prompt.input)
      restoreExtmarksFromParts(store.prompt.parts)
      input.gotoBufferEnd()
    }
  })

또한, ref 콜백 (패치 파일 packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx의 943-946번째 줄)에서 input.setTextrestoreExtmarksFromParts를 호출하는 부분은 이 새로운 createEffect가 동일한 역할을 수행하므로 제거하는 것이 좋습니다. 제 이전 코멘트에서 input.gotoBufferEnd()ref 콜백에 추가하는 것을 제안했지만, 이 "이상적인" 접근 방식에서는 createEffect가 모든 UI 동기화 로직을 담당하게 됩니다.

이러한 변경을 통해 Prompt 컴포넌트의 상태 관리 로직이 더욱 견고하고 명확해질 것입니다.

Suggested change
set(prompt) {
input.setText(prompt.input)
input?.setText(prompt.input)
setStore("prompt", prompt)
restoreExtmarksFromParts(prompt.parts)
input.gotoBufferEnd()
if (input) {
restoreExtmarksFromParts(prompt.parts)
input.gotoBufferEnd()
}
},
set(prompt) {
setStore("prompt", prompt)
},

Comment on lines 622 to 624
function pasteText(text: string, virtualText: string) {
if (!input) return
const currentOffset = input.visualCursor.offset

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Pasted text silently dropped when input is null.

The early return prevents crashes, but user-initiated paste operations are silently lost without feedback. Users may not realize their content wasn't pasted. Consider queuing the paste or showing a toast notification.

🤖 Prompt for AI Agents
In @packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx around lines
622 - 624, In pasteText, avoid silently dropping pasted content when input is
null: either enqueue the paste (store text in a pendingPaste variable or
PasteQueue and apply it when input becomes non-null) or surface user feedback
(call the existing toast/notification helper, e.g., showToast or notify, with a
message like "Paste deferred — input not ready"). Update the pasteText function
to detect input === null and perform the chosen action (enqueue pending paste
tied to the input lifecycle or show a toast) and ensure any queued paste is
applied in the input initialization code (where input is set up).

Comment on lines 657 to 659
async function pasteImage(file: { filename?: string; content: string; mime: string }) {
if (!input) return
const currentOffset = input.visualCursor.offset

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same silent data loss concern for image paste.

Like pasteText, image paste is silently dropped when input is null. Consider a consistent mitigation strategy (queuing or user notification) for both functions.

🤖 Prompt for AI Agents
In @packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx around lines
657 - 659, pasteImage currently returns early and silently drops image data when
input is null (same issue as pasteText); update pasteImage to adopt the same
mitigation used for pasteText—either enqueue the incoming file for later
processing when input becomes available or surface a user-visible
notification/error indicating the paste failed. Locate pasteImage and pasteText
(and the shared input/visualCursor.offset usage) and implement the chosen
strategy consistently so no image data is lost silently when input is null.

@pinion05
pinion05 merged this pull request into dev Jan 10, 2026
18 of 20 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant