Skip to content

Commit a18161a

Browse files
ZvonimirZvonimir
authored andcommitted
Merge remote-tracking branch 'origin/main' into feat/pdf-to-markdown-tool
2 parents 71ee667 + 4ef0e13 commit a18161a

8 files changed

Lines changed: 525 additions & 47 deletions

File tree

.codex/review-prompt.md

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,11 @@ When both exist, report blockers first.
2424

2525
### Review Method
2626

27-
Do two passes:
27+
Do three passes:
2828

29-
1. **Blockers pass** — Scan for correctness bugs, security issues, API/schema contract breaks, missing migrations, data integrity risks, and missing tests for changed behavior. These are `🔴 Bug` comments.
30-
2. **Maintainability pass** — Scan for code bloat, readability issues, naming problems, pattern violations, hardcoded values. These are `🟡 Issue`, `🔵 Nit`, or `💡 Suggestion` comments.
29+
1. **Context + risk-map pass (mandatory)** — Read full touched files (not only hunk lines) and identify high-risk zones: platform/runtime boundaries, event handlers, async cleanup (`finally`), auth/validation boundaries, and repeated predicates/guards.
30+
2. **Blockers pass** — Scan for correctness bugs, security issues, API/schema contract breaks, missing migrations, data integrity risks, and missing tests for changed behavior. These are `🔴 Bug` comments.
31+
3. **Maintainability pass** — Scan for code bloat, readability issues, naming problems, pattern violations, hardcoded values, and architecture drift in touched areas. These are `🟡 Issue`, `🔵 Nit`, or `💡 Suggestion` comments.
3132

3233
### Comment Gate
3334

@@ -53,9 +54,12 @@ If any check fails, skip the comment.
5354
- Boundary conditions — empty arrays, null inputs, zero values, maximum values.
5455
- Error handling — swallowed errors, missing error propagation, unhelpful error messages. Do not flag missing error handling for internal code that cannot reasonably fail.
5556
- Streaming/multipart handlers — verify a request cannot send multiple responses (e.g., multi-file parts triggering repeated `res.json()` calls). If a route expects one file, ensure parser limits and single-response guards exist.
57+
- Unsafe runtime assumptions hidden by type assertions (`as ...`, `as any`, non-null `!`) when values come from events, external I/O, or platform-specific APIs.
58+
- Platform/runtime compatibility assumptions — usage of globals/APIs (`window`, `document`, `Node`, `process`, browser-only APIs) in cross-runtime code paths must be guarded.
5659

5760
#### Security
5861

62+
5963
- Injection risks (SQL, command, XSS) when handling user input.
6064
- Hardcoded secrets — API keys, passwords, tokens in code.
6165
- Missing input validation at system boundaries (user input, external APIs). Not for internal function calls.
@@ -77,18 +81,25 @@ If any check fails, skip the comment.
7781
- Changed behavior must have updated tests reflecting the new expectations.
7882
- If tests are present but brittle (testing implementation details rather than behavior), flag it.
7983
- For single-file upload endpoints, look for regression coverage of multi-file/malformed multipart inputs and confirm no double-response behavior.
84+
- Prefer tests that validate production behavior directly. If a test re-implements production decision logic locally, and could stay green while runtime behavior regresses, flag it and suggest importing shared runtime logic or testing via a higher-level behavior path.
8085

8186
Missing tests for changed behavior are blockers (`🔴 Bug`) only when the change affects user-facing behavior, API contracts, or data integrity. Missing tests for internal refactors or trivial changes are `🟡 Issue`.
8287

8388
### Pass 2: Maintainability
8489

90+
#### Architecture Direction (Touched Area)
91+
92+
- Evaluate whether the diff makes the touched area more or less maintainable (coupling, cohesion, readability of control flow).
93+
- Flag **architecture drift** when business decisions become more scattered (same guard/predicate duplicated across multiple call paths, UI/state/network logic further entangled, or test/runtime logic diverging).
94+
- When the same invariant-like predicate appears repeatedly in changed code, prefer a named helper/shared utility if it clearly reduces divergence risk.
95+
8596
#### Code Bloat and Unnecessary Complexity
8697

8798
- **Excessive code** — More lines than necessary. Could this be done in fewer lines without sacrificing clarity?
8899
- **Over-engineering** — Abstractions, helpers, or utilities for one-time operations. Premature generalization. Feature flags or config for things that could just be code.
89100
- **Speculative generality** — Code handling hypothetical future requirements nobody asked for.
90101
- **Dead code** — Unused variables, unreachable branches, commented-out code.
91-
- **Duplicate code** — Same logic repeated instead of extracted. But do not suggest extraction for only 2-3 similar lines — that is premature abstraction.
102+
- **Duplicate code** — Same logic repeated instead of extracted. Do not suggest extraction for only 2-3 similar lines unless the repeated logic encodes a correctness invariant across multiple paths (e.g., identical guard logic in multiple `finally` blocks).
92103

93104
#### Readability and Naming
94105

@@ -169,4 +180,4 @@ The `line` field must refer to the line number in the new version of the file (r
169180

170181
## Summary
171182

172-
Write a brief (2–4 sentence) overall assessment in the `summary` field. Lead with blockers if any exist. Mention whether the PR is clean/minimal or has code quality issues. If the PR looks good, say so.
183+
Write a brief (2–4 sentence) overall assessment in the `summary` field. Lead with blockers if any exist. Mention whether the PR is clean/minimal or has code quality issues. Include one sentence on maintainability direction in touched areas (improved / neutral / worsened, and why). If the PR looks good, say so.

apps/agent/src/app/(protected)/chat.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ import {
4343
toToolExecutionMode,
4444
toToolExecutionSettings,
4545
} from "@/shared/toolExecutionMode";
46+
import {
47+
isThinkingVisible,
48+
shouldStopGenerating,
49+
} from "@/shared/thinkingIndicator";
4650

4751
function normalizeStreamingMarkdown(content: string): string {
4852
const fencePattern = /^(`{3,})[^`]*$/gm;
@@ -321,7 +325,9 @@ export default function ChatPage() {
321325

322326
addAssistantCompletion(completion);
323327
} finally {
324-
setIsGenerating(false);
328+
if (shouldStopGenerating(pendingToolCalls.current.size)) {
329+
setIsGenerating(false);
330+
}
325331
}
326332
}
327333

@@ -418,7 +424,9 @@ export default function ChatPage() {
418424
timeout: 5000,
419425
});
420426
} finally {
421-
setIsGenerating(false);
427+
if (shouldStopGenerating(pendingToolCalls.current.size)) {
428+
setIsGenerating(false);
429+
}
422430
}
423431
}
424432

@@ -440,7 +448,9 @@ export default function ChatPage() {
440448
timeout: 5000,
441449
});
442450
} finally {
443-
setIsGenerating(false);
451+
if (shouldStopGenerating(pendingToolCalls.current.size)) {
452+
setIsGenerating(false);
453+
}
444454
}
445455
}
446456

@@ -477,7 +487,9 @@ export default function ChatPage() {
477487

478488
addAssistantCompletion(completion);
479489
} finally {
480-
setIsGenerating(false);
490+
if (shouldStopGenerating(pendingToolCalls.current.size)) {
491+
setIsGenerating(false);
492+
}
481493
}
482494
}
483495

@@ -792,7 +804,7 @@ export default function ChatPage() {
792804

793805
return <View key={i}>{messageContent}</View>;
794806
})}
795-
{isGenerating && streamingContent === null && <Chat.Thinking />}
807+
{isThinkingVisible(isGenerating, streamingContent) && <Chat.Thinking />}
796808
{streamingContent !== null && (
797809
<Chat.Message icon="assistant">
798810
<Markdown>

apps/agent/src/components/Chat/Input.tsx

Lines changed: 57 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,20 @@ import {
3636
getChatInputHeightFromText,
3737
} from "../../shared/chatInputHeight";
3838
import { getChatInputKeyAction } from "../../shared/chatInputKeyPress";
39+
import {
40+
SCROLLBAR_HIDE_DELAY_MS,
41+
SCROLLBAR_TRACK_INSET,
42+
SCROLLBAR_MIN_THUMB_HEIGHT,
43+
SCROLLBAR_THUMB_WIDTH,
44+
SCROLLBAR_TRACK_WIDTH,
45+
SCROLLBAR_THUMB_BORDER_RADIUS,
46+
SCROLLBAR_THUMB_OPACITY,
47+
SCROLLBAR_TRACK_RIGHT,
48+
} from "../../shared/customScrollbar";
3949

4050
import ChatInputFilesSelected from "./Input/FilesSelected";
4151
import ChatInputToolsSelector from "./Input/ToolsSelector";
4252

43-
const CHAT_INPUT_SCROLLBAR_HIDE_DELAY_MS = 5000;
44-
const CHAT_INPUT_SCROLLBAR_TRACK_INSET = 8;
45-
const CHAT_INPUT_SCROLLBAR_MIN_THUMB_HEIGHT = 24;
4653
const CHAT_INPUT_TEST_ID = "chat-input";
4754
const CHAT_INPUT_SELECTOR = `[data-testid="${CHAT_INPUT_TEST_ID}"]`;
4855

@@ -99,7 +106,7 @@ export default function ChatInput({
99106
const [inputHeight, setInputHeight] = useState(CHAT_INPUT_MIN_HEIGHT);
100107
const [isCustomScrollbarVisible, setIsCustomScrollbarVisible] = useState(false);
101108
const [customScrollbarThumbTop, setCustomScrollbarThumbTop] = useState(
102-
CHAT_INPUT_SCROLLBAR_TRACK_INSET,
109+
SCROLLBAR_TRACK_INSET,
103110
);
104111
const [customScrollbarThumbHeight, setCustomScrollbarThumbHeight] = useState(0);
105112
const hideScrollbarTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
@@ -112,6 +119,7 @@ export default function ChatInput({
112119
startScrollTop: number;
113120
} | null>(null);
114121
const restoreUserSelectRef = useRef<string | null>(null);
122+
const modeDropdownContainerRef = useRef<View>(null);
115123
const isBusy = disabled || isUploading;
116124
const isWeb = Platform.OS === "web";
117125
const isInputAtMaxHeight = inputHeight >= CHAT_INPUT_MAX_HEIGHT;
@@ -122,6 +130,29 @@ export default function ChatInput({
122130
(m) => m.value === toolExecutionMode,
123131
)!;
124132

133+
// Close mode dropdown on click outside (web only)
134+
useEffect(() => {
135+
if (!isModeDropdownOpen || !isWeb) return;
136+
const handler = (e: MouseEvent) => {
137+
// Ignore clicks inside the dropdown container (trigger + menu)
138+
const container = modeDropdownContainerRef.current;
139+
if (
140+
isHTMLElement(container) &&
141+
typeof Node !== "undefined" &&
142+
e.target instanceof Node &&
143+
container.contains(e.target)
144+
)
145+
return;
146+
setIsModeDropdownOpen(false);
147+
};
148+
// setTimeout(0) prevents the opening click from immediately triggering close
149+
const id = setTimeout(() => document.addEventListener("click", handler), 0);
150+
return () => {
151+
clearTimeout(id);
152+
document.removeEventListener("click", handler);
153+
};
154+
}, [isModeDropdownOpen, isWeb]);
155+
125156
const clearHideScrollbarTimeout = () => {
126157
if (!hideScrollbarTimeoutRef.current) return;
127158
clearTimeout(hideScrollbarTimeoutRef.current);
@@ -138,7 +169,7 @@ export default function ChatInput({
138169
hideScrollbarTimeoutRef.current = setTimeout(() => {
139170
setIsCustomScrollbarVisible(false);
140171
hideScrollbarTimeoutRef.current = null;
141-
}, CHAT_INPUT_SCROLLBAR_HIDE_DELAY_MS);
172+
}, SCROLLBAR_HIDE_DELAY_MS);
142173
};
143174

144175
const updateCustomScrollbar = ({
@@ -155,27 +186,27 @@ export default function ChatInput({
155186
viewportHeight >= CHAT_INPUT_MAX_HEIGHT &&
156187
contentHeight > viewportHeight + 1;
157188
if (!isScrollable) {
158-
setCustomScrollbarThumbTop(CHAT_INPUT_SCROLLBAR_TRACK_INSET);
189+
setCustomScrollbarThumbTop(SCROLLBAR_TRACK_INSET);
159190
setCustomScrollbarThumbHeight(0);
160191
hideCustomScrollbar();
161192
return;
162193
}
163194

164195
const trackHeight = Math.max(
165196
0,
166-
viewportHeight - CHAT_INPUT_SCROLLBAR_TRACK_INSET * 2,
197+
viewportHeight - SCROLLBAR_TRACK_INSET * 2,
167198
);
168199
if (trackHeight <= 0) return;
169200

170201
const thumbHeight = Math.max(
171-
CHAT_INPUT_SCROLLBAR_MIN_THUMB_HEIGHT,
202+
SCROLLBAR_MIN_THUMB_HEIGHT,
172203
Math.min(trackHeight, (viewportHeight / contentHeight) * trackHeight),
173204
);
174205
const maxScrollTop = Math.max(1, contentHeight - viewportHeight);
175206
const clampedScrollTop = Math.min(maxScrollTop, Math.max(0, scrollTop));
176207
const maxThumbOffset = Math.max(0, trackHeight - thumbHeight);
177208
const thumbTop =
178-
CHAT_INPUT_SCROLLBAR_TRACK_INSET +
209+
SCROLLBAR_TRACK_INSET +
179210
(clampedScrollTop / maxScrollTop) * maxThumbOffset;
180211

181212
setCustomScrollbarThumbTop(thumbTop);
@@ -203,10 +234,10 @@ export default function ChatInput({
203234

204235
const trackHeight = Math.max(
205236
1,
206-
viewportHeight - CHAT_INPUT_SCROLLBAR_TRACK_INSET * 2,
237+
viewportHeight - SCROLLBAR_TRACK_INSET * 2,
207238
);
208239
const thumbHeight = Math.max(
209-
CHAT_INPUT_SCROLLBAR_MIN_THUMB_HEIGHT,
240+
SCROLLBAR_MIN_THUMB_HEIGHT,
210241
Math.min(trackHeight, (viewportHeight / contentHeight) * trackHeight),
211242
);
212243
const maxThumbOffset = Math.max(1, trackHeight - thumbHeight);
@@ -345,7 +376,7 @@ export default function ChatInput({
345376
setMessage("");
346377
setSelectedFiles([]);
347378
setInputHeight(CHAT_INPUT_MIN_HEIGHT);
348-
setCustomScrollbarThumbTop(CHAT_INPUT_SCROLLBAR_TRACK_INSET);
379+
setCustomScrollbarThumbTop(SCROLLBAR_TRACK_INSET);
349380
setCustomScrollbarThumbHeight(0);
350381
inputScrollTopRef.current = 0;
351382
hideCustomScrollbar();
@@ -497,7 +528,7 @@ export default function ChatInput({
497528
</Text>
498529
</Pressable>
499530

500-
<View style={styles.modeSelectorContainer}>
531+
<View ref={modeDropdownContainerRef} style={styles.modeSelectorContainer}>
501532
<Pressable
502533
style={[
503534
styles.modeSelectorTrigger,
@@ -535,14 +566,20 @@ export default function ChatInput({
535566
]}
536567
>
537568
{TOOL_EXECUTION_MODE_OPTIONS.map((option) => {
569+
const isSelected = option.value === toolExecutionMode;
538570
return (
539571
<Pressable
540572
key={option.value}
541-
style={[
573+
style={(state) => [
542574
styles.modeSelectorOption,
543-
option.value === toolExecutionMode && {
575+
isSelected && {
544576
backgroundColor: colors.backgroundFlat,
545577
},
578+
"hovered" in state &&
579+
(state as any).hovered &&
580+
!isSelected && {
581+
backgroundColor: colors.backgroundFlat + "66",
582+
},
546583
]}
547584
onPress={() => {
548585
onToolExecutionModeChange?.(option.value);
@@ -639,16 +676,16 @@ const styles = StyleSheet.create({
639676
inputScrollbarTrack: {
640677
position: "absolute",
641678
top: 0,
642-
right: 5,
679+
right: SCROLLBAR_TRACK_RIGHT,
643680
bottom: 0,
644-
width: 8,
681+
width: SCROLLBAR_TRACK_WIDTH,
645682
},
646683
inputScrollbarThumb: {
647684
position: "absolute",
648685
right: 0,
649-
width: 6,
650-
borderRadius: 999,
651-
opacity: 0.72,
686+
width: SCROLLBAR_THUMB_WIDTH,
687+
borderRadius: SCROLLBAR_THUMB_BORDER_RADIUS,
688+
opacity: SCROLLBAR_THUMB_OPACITY,
652689
},
653690
toolbar: {
654691
flexDirection: "row",

0 commit comments

Comments
 (0)