-
Notifications
You must be signed in to change notification settings - Fork 5.1k
Hide native chat UI until terminal lease is ready #10144
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
base: main
Are you sure you want to change the base?
Changes from all commits
e63e157
25a1551
1c43483
eb35b4c
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 |
|---|---|---|
|
|
@@ -1012,6 +1012,7 @@ | |
| const markdownSaveSeqRef = useRef<Map<string, number>>(new Map()) | ||
| const markdownSaveInFlightRef = useRef<Set<string>>(new Set()) | ||
| const subscribeSeqRef = useRef<Map<string, number>>(new Map()) | ||
| const chatStreamRef = useRef<ReturnType<typeof useMobileNativeChatTerminalStream> | null>(null) | ||
| // Why: post-RPC refresh timers capture this screen and must not survive route reuse or unmount. | ||
| const delayedActionTimersRef = useRef<Set<ReturnType<typeof setTimeout>>>(new Set()) | ||
| // Why: highest applyLayout seq seen per handle; drop older scrollback/resized as stale, but a >20 gap resets (fresh subscription/server restart). | ||
|
|
@@ -1299,6 +1300,7 @@ | |
| unsubscribeTerminalRef.current = unsubscribeTerminal | ||
|
|
||
| const clearTerminalCache = useCallback(() => { | ||
| chatStreamRef.current?.clearRetries() | ||
| terminalUnsubsRef.current.forEach((unsub) => unsub()) | ||
| clearNativeChatInputLease() | ||
| terminalUnsubsRef.current.clear() | ||
|
|
@@ -1334,21 +1336,21 @@ | |
| ) | ||
|
|
||
| const subscribeToTerminal = useCallback( | ||
| (handle: string) => { | ||
| (handle: string): boolean | void => { | ||
| const diagnostics = terminalDiagnosticsRef.current | ||
| const logSkippedGate = (reason: string) => | ||
| diagnostics.streamSkipped(handle, reason, handle === activeHandleRef.current) | ||
| if (!client) { | ||
| logSkippedGate('no-client') | ||
| return | ||
| return false | ||
| } | ||
| if (terminalUnsubsRef.current.has(handle)) { | ||
| logSkippedGate('already-subscribed') | ||
| return | ||
| return false | ||
| } | ||
| if (subscribingHandlesRef.current.has(handle)) { | ||
| logSkippedGate('subscribe-in-flight') | ||
| return | ||
| return false | ||
| } | ||
| const covered = nativeChatTerminalStream.isTerminalCoveredByNativeChat( | ||
| showNativeChatRef.current, | ||
|
|
@@ -1359,43 +1361,41 @@ | |
| if (!covered) { | ||
| if (!getTerminalRef(handle)) { | ||
| logSkippedGate('no-webview-ref') | ||
| return | ||
| return false | ||
| } | ||
| if (!webReadyHandlesRef.current.has(handle)) { | ||
| logSkippedGate('webview-not-ready') | ||
| return | ||
| return false | ||
| } | ||
| } | ||
|
|
||
| subscribingHandlesRef.current.add(handle) | ||
| const seq = (subscribeSeqRef.current.get(handle) ?? 0) + 1 | ||
| subscribeSeqRef.current.set(handle, seq) | ||
| diagnostics.streamArmed(handle, seq, viewportRef.current) | ||
|
|
||
| // Why: viewport is embedded in the subscribe params so the server auto-fits before serializing scrollback (no focus→safeFit race). | ||
| diagnostics.streamArmed(handle, seq, covered ? null : viewportRef.current, covered) | ||
| const terminateStream = () => | ||
| chatStreamRef.current?.terminateStream(handle, unsubscribeTerminalRef.current) | ||
| const unsub = subscribeMobileTerminalSafely( | ||
| client, | ||
| { | ||
| nativeChatTerminalStream.buildMobileNativeChatTerminalSubscribeParams({ | ||
| terminal: handle, | ||
| client: { id: deviceTokenRef.current!, type: 'mobile' as const }, | ||
| viewport: nativeChatTerminalStream.mobileNativeChatSubscribeViewport( | ||
| covered, | ||
| viewportRef.current | ||
| ), | ||
| capabilities: nativeChatTerminalStream.mobileNativeChatTerminalCapabilities(covered) | ||
| }, | ||
| clientId: deviceTokenRef.current!, | ||
| covered, | ||
| viewport: viewportRef.current | ||
| }), | ||
| (result) => { | ||
| if (subscribeSeqRef.current.get(handle) !== seq) { | ||
| return | ||
| } | ||
| const data = result as Record<string, unknown> | ||
| diagnostics.firstStreamEvent(handle, seq, data.type) | ||
| diagnostics.firstStreamEvent(handle, seq, data) | ||
| if (data.type === 'end' || data.type === 'error') { | ||
| unsubscribeTerminalRef.current(handle) | ||
| terminateStream() | ||
| return | ||
| } | ||
| if (data.type === 'subscribed') { | ||
| if (nativeChatTerminalStream.isMobileNativeChatLeaseReady(covered, data)) { | ||
| markNativeChatInputLeaseReady(handle) | ||
| chatStreamRef.current?.notifyStreamReady(handle) | ||
| return | ||
| } | ||
|
Comment on lines
1386
to
1400
Contributor
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: rg -n "terminal-unavailable" --type tsRepository: stablyai/orca Length of output: 151 🏁 Script executed: #!/bin/bash
set -u
echo "== locate files =="
git ls-files | rg 'mobile-native-chat-terminal-stream|mobile/app/h/.*/session/|terminals|lease' || true
echo
echo "== search terminal-unavailable without type filter =="
rg -n "terminal-unavailable|isMobileNativeChatLeaseReady|terminalUnavailable|'terminal'|\"terminal\"" . || true
echo
echo "== file size candidates =="
for f in $(git ls-files | rg 'mobile-native-chat-terminal-stream|session/\[worktreeId\].tsx$|mobile/app/h/.*/session/\[worktreeId\].tsx$' || true); do
wc -l "$f"
doneRepository: stablyai/orca Length of output: 50370 🏁 Script executed: #!/bin/bash
set -u
echo "== target file candidate exact =="
f="$(git ls-files | grep -F 'mobile/app/h/[hostId]/session/[worktreeId].tsx' || true)"
echo "$f"
if [ -n "$f" ]; then wc -l "$f"; fi
echo
echo "== focused search exact string =="
rg -n --fixed-strings "terminal-unavailable" "$(git ls-files)" || true
echo
echo "== focused search lease helper =="
rg -n --fixed-strings "isMobileNativeChatLeaseReady" . || true
echo
echo "== focused search mobile native chat stream source/test files =="
git ls-files | rg -n "mobile-native-chat-terminal-stream|nativeChatTerminalStream" || trueRepository: stablyai/orca Length of output: 1722 🏁 Script executed: #!/bin/bash
set -u
echo "== session stream helper =="
sed -n '1,120p' mobile/src/session/mobile-native-chat-terminal-stream.ts
echo
sed -n '1,210p' mobile/src/session/mobile-native-chat-terminal-stream.test.ts
echo
echo "== target app context around stream result flow =="
sed -n '1330,1445p' "mobile/app/h/[hostId]/session/[worktreeId].tsx"
echo
echo "== compose lease/state declarations and controls =="
rg -n -C 3 "markNativeChatInputLeaseReady|chatStreamRef|Composer|composer|leaseReady|terminal-unavailable" "mobile/app/h/[hostId]/session/[worktreeId].tsx"
echo
echo "== use-mobile native hook =="
sed -n '1,230p' mobile/src/session/use-mobile-native-chat-terminal-stream.tsRepository: stablyai/orca Length of output: 23705 🏁 Script executed: #!/bin/bash
set -u
echo "== lock/composer dependency expressions =="
rg -n -C 5 "nativeChatInputLeaseReady|nativeChatInputLockReason|composer|Composer|native chat|chat composer|disabled=.nativeChatInputLeaseReady|disabled.*nativeChat" "mobile/app/h/[hostId]/session/[worktreeId].tsx"
echo
echo "== locate input lease hook source =="
git ls-files | rg 'use-mobile-native-chat-input-lease|mobile-native-chat-input-lease|composer|Composer' | rg -E '\.(ts|tsx|js|jsx)$' | head -80
echo
echo "== inspect terminal unavailable occurrences in all tracked files =="
tmp="$(mktemp)"
git ls-files > "$tmp"
rg -n --fixed-strings "terminal-unavailable" --files-with-from-stdin "$tmp" || true
rm -f "$tmp"Repository: stablyai/orca Length of output: 3689 🏁 Script executed: #!/bin/bash
set -u
echo "== input lease hook candidates =="
git ls-files | grep -F 'mobile/src/session' | grep -E '\.(ts|tsx)$' | grep -F 'mobile-native-chat-input-lease' || true
git ls-files | grep -E 'use-mobile-native-chat-input-lease|mobile-native-chat-input-lease' || true
echo
echo "== composer gate file =="
f="$(git ls-files | grep -E 'inputLockReason|keyboardInset|InputLockReason|KeyboardInset' | head -10 || true)"
printf '%s\n' "$f"
if [ -n "$f" ]; then
IFS=$'\n' read -d '' -ra matches <<< "$f"
for m in "${matches[@]}"; do
[ -n "$m" ] && { echo "--- $m"; wc -l "$m"; }
done
fi
echo
echo "== all terminal-unavailable occurrences =="
tmp="$(mktemp)"
git ls-files > "$tmp"
grep -R --fixed-strings "terminal-unavailable" "$tmp" || true
rm -f "$tmp"Repository: stablyai/orca Length of output: 494 🏁 Script executed: #!/bin/bash
set -u
echo "== use-mobile-native-chat-input-lease source =="
sed -n '1,260p' mobile/src/session/use-mobile-native-chat-input-lease.ts
echo
sed -n '1,220p' mobile/src/session/use-mobile-native-chat-input-lease.test.ts
echo
echo "== composer gate source with all tracked files =="
grep -R --fixed-strings "inputLockReason" . || true
grep -R --fixed-strings "keyboardInset" . || true
echo
echo "== deterministic control-flow probe for covered terminal-unavailable event =="
python3 - <<'PY'
from pathlib import Path
src = Path("mobile/app/h/[hostId]/session/[worktreeId].tsx").read_text()
helper = Path("mobile/src/session/mobile-native-chat-terminal-stream.ts").read_text()
checks = []
checks.append(("terminal-unavailable not present in repository", "terminal-unavailable" not in Path(".").read_text(errors="ignore")))
checks.append(("isMobileNativeChatLeaseReady rejects terminal-unavailable", "'terminal-unavailable'" in helper and "event.type !== 'subscribed'" in helper))
# Extract callback branches in subscribeToTerminal and classify event handling.
# This is a compact, read-only structural summary keyed only by data.type values observed.
for needle in ["terminals", "error", "terminal-unavailable", "subscribed", "resized", "scrollback"]:
checks.append((f"subscribe callback mentions {needle!r}", f"data.type === '{needle}'" in src or f'"' + needle + '"' in src))
def classify(event_type, covered=True):
# Mirror the branch ordering from lines 1388-1486:
if event_type in ("end", "error"):
return "terminateStream"
# NativeChatTerminalStream.isMobileNativeChatLeaseReady(covered, data)
if event_type != "subscribed" or (covered and event_type == "subscribed" and "leaseReady" in {"lease": False} and False):
pass
if event_type == "subscribed" and (not covered or True):
return "markNativeChatInputLeaseReady/notifyStreamReady"
# covered-terminal early return
if covered:
return "terminal-covered-noop"
if event_type == "resized":
return "resized-processed"
if event_type == "scrollback":
return "scrollback-processed"
return "no-match-drop"
for et in ["terminal-unavailable", "subscribed", "error", "end", "resized"]:
checks.append((f"covered {et!r} branch", classify(et)))
for name, value in checks:
print(f"{name}: {value}")
PYRepository: stablyai/orca Length of output: 6442 🌐 Web query:
💡 Result: The terms MobileNativeChatView, inputLockReason, and waiting composer disabled appear to describe custom implementation logic within a chat interface, likely related to handling state while a message or process is pending [1][2]. There is no standard, framework-agnostic API or component named MobileNativeChatView across major chat SDKs (such as Stream Chat, React Native Gifted Chat, or Chatwoot) that defines these specific property or state names [3][4][5][6]. In development contexts, these terms often arise in projects attempting to manage UI states during asynchronous operations, such as: 1. Input Locking: Developers often implement custom logic to disable the composer or "lock" input when a message is currently being sent, processed by a bot, or streaming a response to prevent duplicate sends or inconsistent states [7][6][8]. 2. State Management: The inputLockReason: 'waiting' pattern suggests a state machine or conditional rendering approach where the UI inspects a reason code (e.g., waiting, processing, streaming) to determine whether the composer should be disabled or visually modified [1][2]. 3. Custom Implementation: If you encounter this in your codebase, it is likely a specific component or context provider created to handle chat stream management [7][2]. You should search your local repository for the definition of MobileNativeChatView to find the corresponding state management logic, which typically involves checking a message status or a connection/process state before setting the disabled attribute on the underlying text input [3][9][2]. If you are attempting to implement this behavior, modern best practices involve queuing messages typed during processing rather than simply locking the input, which improves the user experience by allowing them to continue drafting while the backend finishes the previous request [7][2]. Citations:
🏁 Script executed: #!/bin/bash
set -u
echo "== MobileNativeChatView overlay =="
sed -n '1,260p' mobile/src/session/MobileNativeChatView.tsx
echo
sed -n '1,220p' mobile/src/session/MobileNativeChatOverlay.tsx
echo
echo "== MobileNativeChatView tests =="
sed -n '1,240p' mobile/src/session/MobileNativeChatView.test.tsRepository: stablyai/orca Length of output: 19996 🌐 Web query:
💡 Result: The error message "terminal-unavailable" and the status "sendDisabled: waiting for terminal" within the Citations:
Handle PTY-unavailable events instead of waiting forever.
|
||
| // Why: keep the subscription as the input-floor lease but don't mutate covered xterm state; return-to-terminal resubscribes. | ||
|
|
@@ -1538,7 +1538,12 @@ | |
| scheduleDelayedAction(() => getTerminalRef(handle)?.resetZoom(), 200) | ||
| } | ||
| }, | ||
| () => unsubscribeTerminalRef.current(handle) | ||
| () => { | ||
| if (subscribeSeqRef.current.get(handle) !== seq) { | ||
| return | ||
| } | ||
| terminateStream() | ||
| } | ||
| ) | ||
|
|
||
| if (subscribeSeqRef.current.get(handle) === seq) { | ||
|
|
@@ -1551,7 +1556,7 @@ | |
| [client, getTerminalRef, markNativeChatInputLeaseReady, scheduleDelayedAction] | ||
| ) | ||
|
|
||
| const notifyTerminalWebReady = useMobileNativeChatTerminalStream({ | ||
| useMobileNativeChatTerminalStream({ | ||
| showNativeChat, | ||
| activeHandle, | ||
| activeTabType: activeSessionTab?.type ?? null, | ||
|
|
@@ -1560,9 +1565,9 @@ | |
| webReadyRef: webReadyHandlesRef, | ||
| initializedRef: initializedHandlesRef, | ||
| subscribe: subscribeToTerminal, | ||
| unsubscribe: unsubscribeTerminal | ||
| unsubscribe: unsubscribeTerminal, | ||
| controllerRef: chatStreamRef | ||
| }) | ||
|
|
||
| // Why: server does the resize and emits 'resized' on the existing subscription — no client-side state tracking needed. | ||
| const toggleInFlightRef = useRef<Set<string>>(new Set()) | ||
| const toggleDisplayMode = useCallback( | ||
|
|
@@ -2918,7 +2923,7 @@ | |
| (handle: string) => { | ||
| const wasAlreadyReady = webReadyHandlesRef.current.has(handle) | ||
| webReadyHandlesRef.current.add(handle) | ||
| notifyTerminalWebReady(handle, wasAlreadyReady) | ||
| chatStreamRef.current?.notifyWebReady(handle, wasAlreadyReady) | ||
| terminalDiagnosticsRef.current.webViewReady( | ||
| handle, | ||
| wasAlreadyReady, | ||
|
|
@@ -2946,7 +2951,7 @@ | |
| })() | ||
| } | ||
| }, | ||
| [measureViewportOnce, notifyTerminalWebReady, subscribeToTerminal, unsubscribeTerminal] | ||
| [measureViewportOnce, subscribeToTerminal, unsubscribeTerminal] | ||
| ) | ||
|
|
||
| useEffect(() => { | ||
|
|
@@ -5361,4 +5366,4 @@ | |
| /> | ||
| </View> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import path from 'node:path' | ||
|
|
||
| const DESKTOP_BUILD_TIMEOUT_MS = 300_000 | ||
|
|
||
| export async function prepareEmulatorDesktopRuntime({ | ||
| worktree, | ||
| cliOverride, | ||
| runCommand, | ||
| logStep, | ||
| logSuccess | ||
| }) { | ||
| const explicitCli = cliOverride?.trim() | ||
| if (explicitCli) { | ||
| return explicitCli | ||
| } | ||
|
|
||
| logStep('0', 'Building current desktop runtime for mobile pairing...') | ||
| await runCommand('pnpm', ['run', 'build:cli'], { | ||
| cwd: worktree, | ||
| timeout: DESKTOP_BUILD_TIMEOUT_MS | ||
| }) | ||
| await runCommand('pnpm', ['run', 'build:electron-vite'], { | ||
| cwd: worktree, | ||
| timeout: DESKTOP_BUILD_TIMEOUT_MS | ||
| }) | ||
| logSuccess('Current desktop runtime built') | ||
|
|
||
| // Why: pairing against an installed app can silently mix incompatible | ||
| // mobile and desktop protocol/transcript behavior. | ||
| return path.join(worktree, 'config', 'scripts', 'orca-dev.mjs') | ||
| } |
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.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: stablyai/orca
Length of output: 1075
🏁 Script executed:
Repository: stablyai/orca
Length of output: 242
🏁 Script executed:
Repository: stablyai/orca
Length of output: 16463
🏁 Script executed:
Repository: stablyai/orca
Length of output: 7997
🏁 Script executed:
Repository: stablyai/orca
Length of output: 21505
🏁 Script executed:
Repository: stablyai/orca
Length of output: 10393
Guard
deviceTokenRef.currentbefore subscribing the mobile terminal stream.subscribeToTerminal()already checksclient, but it can still build and sendterminal.subscribebeforeloadHosts()populatesdeviceTokenRef.current. Building params withclient: { id: undefined, type: 'mobile' }does not match the guarded RPC-client pattern used forterminal.send, so send only afterdeviceTokenRef.currentis set and return early withsubscribingHandlesRef.current.delete(handle).