Conversation
|
@e-jung is attempting to deploy a commit to the Manaflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughOpenCode integration now installs a cmux wrapper, embeds a session plugin, bridges OpenCode events to cmux hooks, handles retrying agent status, updates shell integrations, documents setup behavior, and adds installation and wrapper coverage. ChangesOpenCode integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Shell
participant cmuxOpencodeWrapper
participant cmux
participant OpenCode
participant CMUXSessionRestore
Shell->>cmuxOpencodeWrapper: invoke opencode
cmuxOpencodeWrapper->>cmux: install OpenCode hooks when eligible
cmuxOpencodeWrapper->>OpenCode: exec real opencode
OpenCode->>CMUXSessionRestore: emit lifecycle and interaction events
CMUXSessionRestore->>cmux: run opencode hook with session payload
cmux->>CMUXSessionRestore: process retrying status
Suggested reviewers: Important Pre-merge checks failedPlease resolve all errors before merging. Addressing warnings is optional. ❌ Failed checks (2 errors, 1 warning)
✅ Passed checks (22 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR adds automatic OpenCode sidebar status integration. The main changes are:
Confidence Score: 5/5No additional blocking issue qualifies for this follow-up review.
Important Files Changed
Reviews (5): Last reviewed commit: "Handle failed OpenCode hook delivery" | Re-trigger Greptile |
| "agent.generic.status.retrying": { | ||
| "extractionState": "manual", | ||
| "localizations": { | ||
| "ar": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "bs": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "da": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "de": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "en": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "es": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "fr": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "it": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "ja": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "再試行中" | ||
| } | ||
| }, | ||
| "ko": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "nb": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "pl": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "pt-BR": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "ru": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "th": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "tr": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "uk": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "zh-Hans": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| }, | ||
| "zh-Hant": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Retrying" | ||
| } | ||
| } | ||
| } | ||
| }, | ||
| "agent.hermes.notification.body.approvalCommand": { | ||
| "extractionState": "manual", | ||
| "localizations": { |
There was a problem hiding this comment.
Most new locale entries contain the English value Retrying, including Arabic, German, Spanish, Korean, and both Chinese variants. Those users will see English in the sidebar, and copied English placeholders do not satisfy the repository's localization requirement.
Rule Used: Flag production user-facing text that is not fully... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| } else if (isRunningStatus(openCodeStatusType(event))) { | ||
| sendHook("prompt-submit", ctx, event); | ||
| } |
There was a problem hiding this comment.
Running States Inflate Prompt Depth
Each active, busy, running, streaming, thinking, or working event becomes a new prompt-submit. If OpenCode emits multiple running states during one turn, the store increments active prompt depth several times, while the eventual idle event decrements it once, leaving the finished session marked as running.
| case "session.error": | ||
| sendHook("notification", ctx, event, { | ||
| message: openCodeEventMessage(event), | ||
| reason: "error", | ||
| }); | ||
| break; |
There was a problem hiding this comment.
A terminal session.error only sends a notification and does not run the stop path that decrements active prompt depth. When OpenCode does not follow the error with a separate idle event, the failed turn remains logically active and the session can continue to appear running or restorable.
| case "session.updated": | ||
| if (props.info && props.info.time && props.info.time.archived) { | ||
| sendHook("session-end", ctx, event); | ||
| dropSession(sessionIdFor(event)); | ||
| } else { | ||
| sendHook("session-start", ctx, event); | ||
| } |
There was a problem hiding this comment.
Every non-archived session.updated event is treated as session-start, including ordinary metadata changes such as title updates. The downstream start handler clears notification-emission state and publishes start telemetry, so mid-session updates can replay start events and allow an already emitted notification to be sent again.
| function sessionIdFor(event) { | ||
| const props = eventProperties(event); | ||
| return firstString( | ||
| props.info && props.info.id, | ||
| props.sessionID, | ||
| props.sessionId, | ||
| props.session_id, | ||
| props.session && props.session.id, | ||
| event && event.sessionID, | ||
| event && event.sessionId, | ||
| event && event.id | ||
| ); | ||
| } |
There was a problem hiding this comment.
The final fallback accepts event.id as the persistent session identifier. For an event shape where id identifies the event rather than its session, each status event creates or updates a different cmux session record; after enough events, these phantom keys evict the real in-memory session context and route lifecycle updates under the wrong identity.
Rule Used: Flag correctness-critical detection/identity deriv... (source)
| } | ||
| _cmux_install_cli_wrapper claude _CMUX_CLAUDE_WRAPPER cmux-claude-wrapper | ||
| _cmux_install_cli_wrapper grok _CMUX_GROK_WRAPPER | ||
| _cmux_install_cli_wrapper opencode _CMUX_OPENCODE_WRAPPER cmux-opencode-wrapper |
There was a problem hiding this comment.
Existing OpenCode Function Is Replaced
Unlike the guarded Claude registration and the fish integration, the bash/zsh helper unconditionally removes an opencode alias and installs its own function. A user function that sets required environment or arguments is silently discarded when cmux shell integration loads, changing how OpenCode starts; preserve an existing override or compose the wrapper with it in both shells.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| sendHook("notification", ctx, event, { | ||
| message: "OpenCode reported an error", | ||
| reason: "error", | ||
| }); |
There was a problem hiding this comment.
Error text dropped in notifications
Low Severity
On session.error, the session plugin sends a fixed OpenCode reported an error notification message instead of the event’s error text via openCodeEventMessage, so cmux never surfaces the provider’s actual failure reason in sidebar notifications.
Reviewed by Cursor Bugbot for commit d6f8235. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@CLI/CMUXCLI`+OpenCodeSessionPluginSource.swift:
- Around line 268-294: Update sendHook to return whether spawnSync completed
successfully, then adjust sendStartOnce, sendPromptSubmitOnce, and
sendStopIfActive to commit their session-state flag changes only after
successful hook delivery, or restore the prior flag when delivery fails. Ensure
failed or timed-out cmux processes do not permanently alter started or
activeTurn state.
In `@tests/test_opencode_plugin_install.py`:
- Around line 233-252: Add failure-path coverage to the OpenCode session event
tests around the existing fake_cmux setup and assertions: provide a fake_cmux
variant that exits non-zero or is unavailable, then verify that a failed hook
transition is not marked as sent and is retried on the next matching event.
Preserve the existing successful dedup assertions for session-start,
prompt-submit, and stop.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 163189f2-dcb8-4481-b7c9-68fdc4ea603d
📒 Files selected for processing (3)
CLI/CMUXCLI+OpenCodeSessionPluginSource.swiftResources/Localizable.xcstringstests/test_opencode_plugin_install.py
There was a problem hiding this comment.
♻️ Duplicate comments (2)
tests/test_opencode_plugin_install.py (1)
224-313: 📐 Maintainability & Code Quality | 🔵 TrivialNo failure-path test coverage (still unresolved).
fake_cmuxalways exits 0, so this suite cannot catch the state-desync issue where a failed hook process still marks the transition as sent. Consider adding afake_cmuxvariant that exits non-zero or is missing, then asserting the corresponding hook is retried on the next matching event.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/test_opencode_plugin_install.py` around lines 224 - 313, Add failure-path coverage to the event tests around the existing hooks.event sequence by using a fake_cmux variant that exits non-zero or is unavailable. Assert that the corresponding hook transition is retried on the next matching event, proving failed hook execution does not mark the transition as sent.CLI/CMUXCLI+OpenCodeSessionPluginSource.swift (1)
269-296: 🩺 Stability & Availability | 🟠 MajorRoll back lifecycle flags when hook delivery fails (still unresolved).
sendStartOnce,sendPromptSubmitOnce, andsendStopIfActivestill flip session flags before callingsendHook, andsendHookstill ignores thespawnSyncresult (including timeouts, which return normally rather than throwing). If thecmuxprocess fails or times out, the transition is lost for the rest of the session because the flag stays set.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CLI/CMUXCLI`+OpenCodeSessionPluginSource.swift around lines 269 - 296, Update sendHook to report whether spawnSync delivery succeeded, including treating process errors and timeouts as failures, then have sendStartOnce, sendPromptSubmitOnce, and sendStopIfActive revert their sessionState flags when delivery fails. Preserve the existing successful-transition behavior while ensuring failed or timed-out hooks do not consume the lifecycle transition.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@CLI/CMUXCLI`+OpenCodeSessionPluginSource.swift:
- Around line 269-296: Update sendHook to report whether spawnSync delivery
succeeded, including treating process errors and timeouts as failures, then have
sendStartOnce, sendPromptSubmitOnce, and sendStopIfActive revert their
sessionState flags when delivery fails. Preserve the existing
successful-transition behavior while ensuring failed or timed-out hooks do not
consume the lifecycle transition.
In `@tests/test_opencode_plugin_install.py`:
- Around line 224-313: Add failure-path coverage to the event tests around the
existing hooks.event sequence by using a fake_cmux variant that exits non-zero
or is unavailable. Assert that the corresponding hook transition is retried on
the next matching event, proving failed hook execution does not mark the
transition as sent.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f2b7acf8-3eba-400e-821a-6546fa5bb7c9
📒 Files selected for processing (2)
CLI/CMUXCLI+OpenCodeSessionPluginSource.swifttests/test_opencode_plugin_install.py
…ication events in agent-hooks and feed docs
… and Feed plugins
d6f8235 to
9b5fac9
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6e9a8f0. Configure here.
| }); | ||
| } else if (isRunningStatus(openCodeStatusType(event))) { | ||
| sendPromptSubmitOnce(ctx, event); | ||
| } |
There was a problem hiding this comment.
Retrying pill sticks after recovery
Medium Severity
When a session.status retry happens during an active turn, the sidebar shows localized Retrying. If OpenCode then emits a running or streaming status without an idle stop in between, sendPromptSubmitOnce exits early because activeTurn is already true, so retrying is never cleared and no hook updates cmux back to Running.
Reviewed by Cursor Bugbot for commit 6e9a8f0. Configure here.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_opencode_wrapper.py`:
- Around line 29-104: Extend run_wrapper so the test can omit the fake
real_dir/opencode executable and exercise the wrapper-only PATH fallback, while
preserving the current default behavior for existing callers. Add coverage for
this scenario and assert the wrapper terminates without recursive invocation,
including the expected return code and captured outputs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 518719d6-208a-4ac8-9800-43f83cf6b51b
📒 Files selected for processing (12)
CLI/CMUXCLI+OpenCodeSessionPluginSource.swiftCLI/cmux.swiftResources/Localizable.xcstringsResources/bin/cmux-opencode-wrapperResources/shell-integration/cmux-bash-integration.bashResources/shell-integration/cmux-zsh-integration.zshResources/shell-integration/fish/config.fishcmux.xcodeproj/project.pbxprojdocs/agent-hooks.mddocs/feed.mdtests/test_opencode_plugin_install.pytests/test_opencode_wrapper.py
| def run_wrapper(*, inside_cmux: bool, hooks_disabled: bool = False, args: list[str] | None = None) -> tuple[int, str, str, str]: | ||
| with tempfile.TemporaryDirectory(prefix="cmux-opencode-wrapper-test-") as td: | ||
| tmp = Path(td) | ||
| wrapper_dir = tmp / "wrapper-bin" | ||
| real_dir = tmp / "real-bin" | ||
| wrapper_dir.mkdir(parents=True, exist_ok=True) | ||
| real_dir.mkdir(parents=True, exist_ok=True) | ||
|
|
||
| wrapper = wrapper_dir / "cmux-opencode-wrapper" | ||
| shutil.copy2(SOURCE_WRAPPER, wrapper) | ||
| wrapper.chmod(0o755) | ||
|
|
||
| real_log = tmp / "real.log" | ||
| cmux_log = tmp / "cmux.log" | ||
| env_log = tmp / "env.log" | ||
| socket_path = tmp / "cmux.sock" | ||
|
|
||
| make_executable( | ||
| real_dir / "opencode", | ||
| """#!/usr/bin/env bash | ||
| set -euo pipefail | ||
| printf '%s\\n' "$*" >> "$REAL_LOG" | ||
| printf 'cmux_bin=%s\\n' "${CMUX_OPENCODE_CMUX_BIN-}" >> "$ENV_LOG" | ||
| """, | ||
| ) | ||
| make_executable( | ||
| wrapper_dir / "cmux", | ||
| """#!/usr/bin/env bash | ||
| set -euo pipefail | ||
| printf '%s\\n' "$*" >> "$CMUX_LOG" | ||
| if [[ "${1:-}" == "--socket" ]]; then | ||
| shift 2 | ||
| fi | ||
| if [[ "${1:-}" == "ping" ]]; then | ||
| exit 0 | ||
| fi | ||
| exit 0 | ||
| """, | ||
| ) | ||
|
|
||
| unix_socket: socket.socket | None = None | ||
| if inside_cmux: | ||
| unix_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||
| unix_socket.bind(str(socket_path)) | ||
|
|
||
| env = os.environ.copy() | ||
| env["PATH"] = f"{wrapper_dir}:{real_dir}:{env.get('PATH', '/usr/bin:/bin')}" | ||
| env["REAL_LOG"] = str(real_log) | ||
| env["CMUX_LOG"] = str(cmux_log) | ||
| env["ENV_LOG"] = str(env_log) | ||
| if inside_cmux: | ||
| env["CMUX_SURFACE_ID"] = "surface:opencode" | ||
| env["CMUX_SOCKET_PATH"] = str(socket_path) | ||
| else: | ||
| env.pop("CMUX_SURFACE_ID", None) | ||
| env.pop("CMUX_SOCKET_PATH", None) | ||
| if hooks_disabled: | ||
| env["CMUX_OPENCODE_HOOKS_DISABLED"] = "1" | ||
| else: | ||
| env.pop("CMUX_OPENCODE_HOOKS_DISABLED", None) | ||
|
|
||
| try: | ||
| result = subprocess.run( | ||
| [str(wrapper), *(args or ["run", "task"])], | ||
| env=env, | ||
| capture_output=True, | ||
| text=True, | ||
| check=False, | ||
| timeout=10, | ||
| ) | ||
| finally: | ||
| if unix_socket is not None: | ||
| unix_socket.close() | ||
|
|
||
| return result.returncode, read_text(cmux_log), read_text(real_log), read_text(env_log) | ||
|
|
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add coverage for the "no real opencode on PATH" fallback.
Every run_wrapper invocation places a fake opencode binary in real_dir ahead of the wrapper on PATH, so find_real_opencode always succeeds. There's no fixture exercising the case where the wrapper is the only opencode-named entry reachable — which is exactly the path flagged as an infinite-recursion risk in Resources/bin/cmux-opencode-wrapper (Lines 157-160). Adding a scenario where real_dir is empty/absent would catch that regression directly.
Also applies to: 106-170
🧰 Tools
🪛 ast-grep (0.44.1)
[error] 90-97: Command coming from incoming request
Context: subprocess.run(
[str(wrapper), *(args or ["run", "task"])],
env=env,
capture_output=True,
text=True,
check=False,
timeout=10,
)
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').
(subprocess-from-request)
🪛 Ruff (0.15.20)
[error] 91-91: subprocess call: check for execution of untrusted input
(S603)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/test_opencode_wrapper.py` around lines 29 - 104, Extend run_wrapper so
the test can omit the fake real_dir/opencode executable and exercise the
wrapper-only PATH fallback, while preserving the current default behavior for
existing callers. Add coverage for this scenario and assert the wrapper
terminates without recursive invocation, including the expected return code and
captured outputs.


Summary
cmux-opencode-wrapperand shell integration wiring so OpenCode sessions launched inside cmux can auto-install the cmux OpenCode plugins.Retryingsidebar status, focused wrapper/plugin tests, Xcode project wiring, and authoritative docs for the OpenCode wrapper/status behavior.Validation
No-mistakes gate on
fm/cmux-1425-rebuild:tests/test_opencode_wrapper.pyand direct Bun validation of the embedded OpenCode plugin event-to-hook mapping.docs/agent-hooks.mdanddocs/feed.md; README and 20 localized README copies intentionally left unchanged because the README statement is true but non-exhaustive.check-pbxproj.sh, Python compile, JSON validation, wrapper shellcheck/bash syntax, Swift file-length budget for this change).Additional M1 Air validation before publish:
./scripts/reload.sh --tag cmux-1425-rebuildusing the repo'sCMUX_SKIP_ZIG_BUILD=1helper-stub mode.CMUX_CLI_BIN=... python3 tests/test_opencode_plugin_install.py.python3 tests/test_opencode_wrapper.pypassed.zsh -npassed on the zsh shell integration.Known limitations documented, not fixed here:
CMUX_SKIP_ZIG_BUILD=1stub path.fishwas not installed on the M1 Air host, sofish -ncould not be run there.Preserved work:
2f54f7b109commit/branch unchanged; this PR is rebuilt from currentorigin/main.Need help on this PR? Tag
/codesmithwith what you need. Autofix is disabled.Note
Medium Risk
Changes agent hook bridging, session store updates, and shell
opencodeinterception; mistakes could mis-report running/idle state or add latency on every session launch inside cmux.Overview
Adds OpenCode sidebar integration by moving the embedded session plugin into
CMUXCLI+OpenCodeSessionPluginSource.swiftand expanding what it reports: turn lifecycle (prompt-submit/stopwith once-per-turn guards and retries), notifications for permissions, questions, and errors, and a retrying status path into cmux.Introduces
cmux-opencode-wrapper, wired through bash/zsh/fish and the app Copy CLI bundle. Inside a live cmux terminal (socket ping +CMUX_SURFACE_ID), session-likeopencodeinvocations best-effort runcmux hooks opencode install --yesbefore exec’ing the real binary;--help, admin subcommands, and outside-cmux paths pass through without install latency.CLI handling for OpenCode
notificationhooks withcmux_status: retryingupdates the session store, resume binding, PID, lifecycle, and a localized Retrying status pill (agent.generic.status.retrying).Docs (
agent-hooks.md,feed.md) describe wrapper-based setup and the new behaviors; tests cover the wrapper gating and extended plugin event→hook mapping.Reviewed by Cursor Bugbot for commit 6e9a8f0. Bugbot is set up for automated code reviews on this repo. Configure here.
Summary by cubic
Adds OpenCode sidebar status with a localized “Retrying” pill and a new
opencodewrapper that auto-installs cmux OpenCode plugins only inside cmux terminals. Moves the session plugin toCMUXCLI+OpenCodeSessionPluginSource.swift, wires the wrapper into bash/zsh/fish and the Copy CLI, and updates docs/tests; supports Linear CMUX-1425.New Features
prompt-submit, idle/done/stopped ->stop, archived/deleted ->session-end; forwards permission prompts, questions, errors; includes launch metadata and recent message context.cmux-opencode-wrapperresolves the real binary, pings the socket, and installs session + Feed plugins only for session-like invocations inside a live cmux terminal (skips--help/admin).Bug Fixes
Written for commit 6e9a8f0. Summary will update on new commits.
Summary by CodeRabbit
opencoderun.opencodecommand wrappers for bash, zsh, and fish.