Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 69 additions & 52 deletions app/actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ async function submit(formData?: FormData, skip?: boolean) {
message.role !== 'tool' &&
message.type !== 'followup' &&
message.type !== 'related' &&
message.type !== 'end'
message.type !== 'end' &&
message.type !== 'resolution_search_result'
);

// The user's prompt for this action is static.
Expand All @@ -75,79 +76,94 @@ async function submit(formData?: FormData, skip?: boolean) {
...aiState.get(),
messages: [
...aiState.get().messages,
{ id: nanoid(), role: 'user', content }
{ id: nanoid(), role: 'user', content, type: 'input' }
]
});
messages.push({ role: 'user', content });

// Call the simplified agent, which now returns data directly.
const analysisResult = await resolutionSearch(messages) as any;

// Create a streamable value for the summary and mark it as done.
// Create a streamable value for the summary.
const summaryStream = createStreamableValue<string>();
summaryStream.done(analysisResult.summary || 'Analysis complete.');

// Update the UI stream with the BotMessage component.
uiStream.update(
<BotMessage content={summaryStream.value} />
);
async function processResolutionSearch() {
try {
// Call the simplified agent, which now returns data directly.
const analysisResult = await resolutionSearch(messages) as any;

messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' });
// Mark the summary stream as done with the result.
summaryStream.done(analysisResult.summary || 'Analysis complete.');

const sanitizedMessages: CoreMessage[] = messages.map(m => {
if (Array.isArray(m.content)) {
return {
...m,
content: m.content.filter(part => part.type !== 'image')
} as CoreMessage
}
return m
})
messages.push({ role: 'assistant', content: analysisResult.summary || 'Analysis complete.' });

const relatedQueries = await querySuggestor(uiStream, sanitizedMessages);
uiStream.append(
<Section title="Follow-up">
const sanitizedMessages: CoreMessage[] = messages.map(m => {
if (Array.isArray(m.content)) {
return {
...m,
content: m.content.filter(part => part.type !== 'image')
} as CoreMessage
}
return m
})

const relatedQueries = await querySuggestor(uiStream, sanitizedMessages);
uiStream.append(
<Section title="Follow-up">
<FollowupPanel />
</Section>
);
</Section>
);

await new Promise(resolve => setTimeout(resolve, 500));
await new Promise(resolve => setTimeout(resolve, 500));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Clarify purpose of 500ms delay.

The setTimeout delay before finalizing AI state is also present in the main flow (line 451), but its purpose isn't documented. If this is to allow UI animations to complete, consider adding a comment explaining the rationale for maintainability.

🤖 Prompt for AI Agents
In `@app/actions.tsx` at line 114, The 500ms silent delay implemented as "await
new Promise(resolve => setTimeout(resolve, 500))" lacks context; replace it with
an inline comment explaining why it's needed (e.g., to wait for UI
animations/transitions, debounce state updates, or allow async rendering to
settle) and ensure the same explanatory comment is added to the duplicate
occurrence of that exact expression elsewhere in the codebase so maintainers
understand its purpose and can adjust the duration safely.


const groupeId = nanoid();
const groupeId = nanoid();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Typo: groupeId should likely be groupId.

This appears throughout the file (lines 116, 184, 227, etc.). If intentional (perhaps French?), consider adding a comment; otherwise, a codebase-wide rename would improve clarity.

🤖 Prompt for AI Agents
In `@app/actions.tsx` at line 116, The identifier "groupeId" is a typo and should
be renamed to "groupId" for clarity; locate all occurrences of the variable
(e.g., the const declaration groupeId and every subsequent use inside functions
in app/actions.tsx) and perform a consistent rename to groupId (or add a
clarifying comment if the French spelling was intentional), updating references
where it's passed, returned, or used so no references break (ensure function
names or handlers that reference groupeId are also updated).


aiState.done({
...aiState.get(),
messages: [
aiState.done({
...aiState.get(),
messages: [
...aiState.get().messages,
{
id: groupeId,
role: 'assistant',
content: analysisResult.summary || 'Analysis complete.',
type: 'response'
id: groupeId,
role: 'assistant',
content: analysisResult.summary || 'Analysis complete.',
type: 'response'
},
{
id: groupeId,
role: 'assistant',
content: JSON.stringify(analysisResult),
type: 'resolution_search_result'
id: groupeId,
role: 'assistant',
content: JSON.stringify(analysisResult),
type: 'resolution_search_result'
},
{
id: groupeId,
role: 'assistant',
content: JSON.stringify(relatedQueries),
type: 'related'
id: groupeId,
role: 'assistant',
content: JSON.stringify(relatedQueries),
type: 'related'
},
Comment on lines +118 to 139
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The code writes JSON.stringify(analysisResult) into aiState as type: 'resolution_search_result' and now correctly filters that message type out of the model context. However, this still persists potentially large payloads in state. If analysisResult can be sizable (e.g., includes raw extracted text, intermediate reasoning, or large arrays), this can bloat session state and slow rendering/serialization.

If you only need a subset for UI/tooling, consider storing a reduced shape (e.g., {summary, citations, matchesCount, topMatches}) or compressing/limiting fields.

Suggestion

Persist a minimal, stable schema instead of the full analysisResult.

const persisted = {
  summary: analysisResult.summary,
  // keep only what you actually render/use later
  results: analysisResult.results?.slice(0, 20),
};

content: JSON.stringify(persisted)

Reply with "@CharlieHelps yes please" if you’d like me to add a commit that trims the persisted payload and adds a small helper for shaping it.

{
id: groupeId,
role: 'assistant',
content: 'followup',
type: 'followup'
id: groupeId,
role: 'assistant',
content: 'followup',
type: 'followup'
}
]
});
]
});
} catch (error) {
console.error('Error in resolution search:', error);
summaryStream.error(error);
} finally {
isGenerating.done(false);
uiStream.done();
}
Comment on lines +148 to +154
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Error path leaves AI state unfinalized.

When an error occurs, aiState.done() is never called, which could leave the state in an inconsistent condition. The summaryStream.error() signals the UI, but the AI state remains open.

🐛 Proposed fix
       } catch (error) {
         console.error('Error in resolution search:', error);
         summaryStream.error(error);
+        aiState.done({
+          ...aiState.get(),
+          messages: [
+            ...aiState.get().messages,
+            {
+              id: nanoid(),
+              role: 'assistant',
+              content: 'An error occurred during analysis.',
+              type: 'response'
+            }
+          ]
+        });
       } finally {
         isGenerating.done(false);
         uiStream.done();
       }
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 148 - 154, The catch path logs and calls
summaryStream.error(error) but never finalizes the AI state; add a call to
aiState.done(false) so the AI state is closed on error. Update the
error-handling block (the catch/finally surrounding summaryStream.error) to
invoke aiState.done(false) alongside the existing isGenerating.done(false) and
uiStream.done() calls so aiState is always finalized when an exception occurs.

Comment on lines +151 to +154
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing isCollapsed.done() call in resolution search flow.

The isCollapsed streamable value (created at line 43) is never finalized in the resolution_search branch. Compare with the main flow at line 376 where isCollapsed.done(true) is called. This could cause the UI to remain in an unexpected collapsed state.

🐛 Proposed fix
       } finally {
         isGenerating.done(false);
         uiStream.done();
+        isCollapsed.done(true);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} finally {
isGenerating.done(false);
uiStream.done();
}
} finally {
isGenerating.done(false);
uiStream.done();
isCollapsed.done(true);
}
🤖 Prompt for AI Agents
In `@app/actions.tsx` around lines 151 - 154, The resolution_search flow is
missing a finalization call for the isCollapsed streamable, which leaves the UI
collapsed state unresolved; in the same finally block that currently calls
isGenerating.done(false) and uiStream.done(), also call isCollapsed.done(true)
(matching the main flow's behavior) so the stream is finalized—update the
finally in the resolution_search branch to invoke isCollapsed.done(true)
alongside isGenerating.done(false) and uiStream.done().

Comment on lines +148 to +154
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

summaryStream.error(error); is passed the caught value directly. In JS, catch (error) can be anything (string, object). If createStreamableValue().error(...) expects an Error, the UI may render poorly or crash depending on how error values are handled.

Since this code is user-facing and runs in the background, it’s worth normalizing the error value before streaming it.

Suggestion

Normalize the caught value into an Error (or a string message) before calling summaryStream.error.

} catch (err) {
  const error = err instanceof Error ? err : new Error(String(err));
  console.error('Error in resolution search:', error);
  summaryStream.error(error);
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this change.

}

// Start the background process without awaiting it.
processResolutionSearch();

// Immediately update the UI stream with the BotMessage component.
uiStream.update(
<Section title="response">
<BotMessage content={summaryStream.value} />
</Section>
);

isGenerating.done(false);
uiStream.done();
return {
id: nanoid(),
isGenerating: isGenerating.value,
Expand All @@ -161,7 +177,8 @@ async function submit(formData?: FormData, skip?: boolean) {
message.role !== 'tool' &&
message.type !== 'followup' &&
message.type !== 'related' &&
message.type !== 'end'
message.type !== 'end' &&
message.type !== 'resolution_search_result'
)

const groupeId = nanoid()
Expand Down