Skip to content

feat(conversations): allow selecting a saved prompt in the create conversation modal#2750

Draft
arnestrickmann wants to merge 2 commits into
mainfrom
arne/eng-1726-allow-passing-a-pre-saved-prompt-in-the-add-conversation
Draft

feat(conversations): allow selecting a saved prompt in the create conversation modal#2750
arnestrickmann wants to merge 2 commits into
mainfrom
arne/eng-1726-allow-passing-a-pre-saved-prompt-in-the-add-conversation

Conversation

@arnestrickmann

Copy link
Copy Markdown
Contributor

Description

Adds a Prompt select field to the add-conversation modal so users can apply a pre-saved prompt from the prompt library when creating a conversation — previously only agent and model selection were exposed there, while other creation flows (e.g. the create-task modal) already supported saved prompts.

Implementation notes:

  • The field lists prompts from the prompt library via usePromptLibrary(), defaults to "No prompt", filters out prompts with empty text, and is hidden entirely when the library is empty.
  • Selecting a prompt shows a two-line truncated preview of its text under the select.
  • The selected prompt's text is passed as initialPrompt to createConversation. No backend changes were needed: initialPrompt is already plumbed through both runtime paths — PTY injects it on session start, and ACP stores it in the conversation config and consumes it on first spawn in hydrateConversation.
  • Styled to match the existing Model select field for consistency within the modal form.

Related issues

Fixes ENG-1726 — https://linear.app/general-action/issue/ENG-1726/allow-passing-a-pre-saved-prompt-in-the-add-conversation-modal

Testing

  • typecheck for @emdash/emdash-desktop — passes
  • lint for @emdash/emdash-desktop — passes (remaining warnings are pre-existing in unrelated files)
  • format:check for @emdash/emdash-desktop — passes
  • Conversation-related unit tests (src/renderer/features/tasks/conversations, src/main/core/conversations): 74 passed. initial-conversation-section.test.ts fails with a pre-existing window is not defined import error that reproduces identically without this change.

Screenshot/Recording (if applicable)

N/A

Checklist
  • I kept this PR small and focused
  • I ran a self-review before opening this PR
  • I ran the relevant local checks or explained why not
  • I updated docs when behavior or setup changed
  • I added or updated tests when behavior changed, or explained why not — UI-only wiring of an already-tested code path; no renderer browser test coverage exists for this modal today
  • I only added comments where the logic is not obvious
  • I used Conventional Commits for commit messages and, when possible, the PR title

…versation modal

Adds a Prompt select to the add-conversation modal, listing prompts from
the prompt library. The selected prompt's text is passed as initialPrompt,
which is already wired through both the PTY and ACP creation paths.
@arnestrickmann arnestrickmann marked this pull request as draft July 2, 2026 14:39
@greptile-apps

greptile-apps Bot commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a Prompt select field to the create-conversation modal, allowing users to pick a pre-saved prompt from the prompt library when starting a conversation. The selected prompt's text is forwarded as initialPrompt to createConversation, which already handled that parameter for both PTY and ACP runtime paths — so no backend changes were required.

  • Fetches the prompt library via the existing usePromptLibrary() hook, filters out entries with empty text, and hides the field entirely when no usable prompts exist.
  • Renders an inline two-line text preview of the selected prompt below the select, styled to match the model select field already in the form.

Confidence Score: 4/5

The change is a self-contained UI addition to one modal file; the initialPrompt plumbing it relies on is already tested in both runtime paths.

The two findings are about render efficiency and a minor layout shift when the prompt library loads asynchronously. Neither affects data correctness or the create flow itself.

Only create-conversation-modal.tsx changed; the memoization and async-load presentation concerns are both in that one file.

Important Files Changed

Filename Overview
apps/emdash-desktop/src/renderer/features/tasks/conversations/create-conversation-modal.tsx Adds Prompt select UI and wires selectedPrompt into createConversation; implementation is correct but savedPrompts is recomputed on every render without memoization, and the prompt field can flash in after the async load completes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Modal Opens] --> B[usePromptLibrary fetches library]
    B --> C{Data loaded?}
    C -- No --> D[savedPrompts empty - Prompt field hidden]
    C -- Yes --> E[Filter out prompts with empty text]
    E --> F{savedPrompts non-empty?}
    F -- No --> G[Prompt field hidden]
    F -- Yes --> H[Prompt Select rendered with No prompt default]
    H --> I{User selects a prompt?}
    I -- No --> J[selectedPromptId = null]
    I -- Yes --> K[selectedPromptId = id, preview text shown]
    J --> L[handleCreateConversation - initialPrompt = undefined]
    K --> M[handleCreateConversation - initialPrompt = prompt text]
    L --> N[createConversation called]
    M --> N
    N --> O{ConversationType}
    O -- pty --> P[PTY injects initialPrompt on session start]
    O -- acp --> Q[ACP stores in config, consumed in hydrateConversation]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[Modal Opens] --> B[usePromptLibrary fetches library]
    B --> C{Data loaded?}
    C -- No --> D[savedPrompts empty - Prompt field hidden]
    C -- Yes --> E[Filter out prompts with empty text]
    E --> F{savedPrompts non-empty?}
    F -- No --> G[Prompt field hidden]
    F -- Yes --> H[Prompt Select rendered with No prompt default]
    H --> I{User selects a prompt?}
    I -- No --> J[selectedPromptId = null]
    I -- Yes --> K[selectedPromptId = id, preview text shown]
    J --> L[handleCreateConversation - initialPrompt = undefined]
    K --> M[handleCreateConversation - initialPrompt = prompt text]
    L --> N[createConversation called]
    M --> N
    N --> O{ConversationType}
    O -- pty --> P[PTY injects initialPrompt on session start]
    O -- acp --> Q[ACP stores in config, consumed in hydrateConversation]
Loading
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
apps/emdash-desktop/src/renderer/features/tasks/conversations/create-conversation-modal.tsx:53-55
`savedPrompts` is recomputed from a `.filter()` call on every render, which also forces `selectedPrompt` (a `.find()` on the filtered array) to be re-evaluated on every render. Because `selectedPrompt` is in the `useCallback` dependency array, the callback is also recreated on every render while the component is mounted, even when neither the library data nor the selected ID has changed. Wrapping both derived values in `useMemo` keeps them stable between renders and avoids the unnecessary recreations.

```suggestion
  const { value: promptLibrary } = usePromptLibrary();
  const savedPrompts = useMemo(
    () => promptLibrary.filter((p) => p.prompt.trim().length > 0),
    [promptLibrary]
  );
  const selectedPrompt = useMemo(
    () => savedPrompts.find((p) => p.id === selectedPromptId) ?? null,
    [savedPrompts, selectedPromptId]
  );
```

### Issue 2 of 2
apps/emdash-desktop/src/renderer/features/tasks/conversations/create-conversation-modal.tsx:163-190
**Prompt field flashes in after async load**

`usePromptLibrary` starts with `data = undefined`, so `value` falls back to `[]` and `savedPrompts.length > 0` is `false` — the Prompt field is hidden on the initial render. Once the React Query fetch resolves, if the library is non-empty, the field appears and the form visually grows. This is the same pattern used by the Model field (it too is hidden until `useAgents` data arrives), so the precedent is already set. It's worth noting in case you want to add a loading placeholder or a `Skeleton` matching the field height to prevent the layout shift.

Reviews (1): Last reviewed commit: "feat(conversations): allow selecting a s..." | Re-trigger Greptile

Comment on lines +53 to +55
const { value: promptLibrary } = usePromptLibrary();
const savedPrompts = promptLibrary.filter((p) => p.prompt.trim().length > 0);
const selectedPrompt = savedPrompts.find((p) => p.id === selectedPromptId) ?? null;

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.

P2 savedPrompts is recomputed from a .filter() call on every render, which also forces selectedPrompt (a .find() on the filtered array) to be re-evaluated on every render. Because selectedPrompt is in the useCallback dependency array, the callback is also recreated on every render while the component is mounted, even when neither the library data nor the selected ID has changed. Wrapping both derived values in useMemo keeps them stable between renders and avoids the unnecessary recreations.

Suggested change
const { value: promptLibrary } = usePromptLibrary();
const savedPrompts = promptLibrary.filter((p) => p.prompt.trim().length > 0);
const selectedPrompt = savedPrompts.find((p) => p.id === selectedPromptId) ?? null;
const { value: promptLibrary } = usePromptLibrary();
const savedPrompts = useMemo(
() => promptLibrary.filter((p) => p.prompt.trim().length > 0),
[promptLibrary]
);
const selectedPrompt = useMemo(
() => savedPrompts.find((p) => p.id === selectedPromptId) ?? null,
[savedPrompts, selectedPromptId]
);
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/emdash-desktop/src/renderer/features/tasks/conversations/create-conversation-modal.tsx
Line: 53-55

Comment:
`savedPrompts` is recomputed from a `.filter()` call on every render, which also forces `selectedPrompt` (a `.find()` on the filtered array) to be re-evaluated on every render. Because `selectedPrompt` is in the `useCallback` dependency array, the callback is also recreated on every render while the component is mounted, even when neither the library data nor the selected ID has changed. Wrapping both derived values in `useMemo` keeps them stable between renders and avoids the unnecessary recreations.

```suggestion
  const { value: promptLibrary } = usePromptLibrary();
  const savedPrompts = useMemo(
    () => promptLibrary.filter((p) => p.prompt.trim().length > 0),
    [promptLibrary]
  );
  const selectedPrompt = useMemo(
    () => savedPrompts.find((p) => p.id === selectedPromptId) ?? null,
    [savedPrompts, selectedPromptId]
  );
```

How can I resolve this? If you propose a fix, please make it concise.

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!

Comment on lines +163 to +190
{savedPrompts.length > 0 ? (
<Field>
<FieldLabel>Prompt</FieldLabel>
<Select
value={selectedPromptId ?? ''}
onValueChange={(val) => setSelectedPromptId(val || null)}
>
<SelectTrigger>
<SelectValue placeholder="No prompt">
{selectedPrompt ? selectedPrompt.title : 'No prompt'}
</SelectValue>
</SelectTrigger>
<SelectContent>
<SelectItem value="">No prompt</SelectItem>
{savedPrompts.map((prompt) => (
<SelectItem key={prompt.id} value={prompt.id}>
{prompt.title}
</SelectItem>
))}
</SelectContent>
</Select>
{selectedPrompt ? (
<p className="text-muted-foreground line-clamp-2 text-xs">
{selectedPrompt.prompt}
</p>
) : null}
</Field>
) : null}

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.

P2 Prompt field flashes in after async load

usePromptLibrary starts with data = undefined, so value falls back to [] and savedPrompts.length > 0 is false — the Prompt field is hidden on the initial render. Once the React Query fetch resolves, if the library is non-empty, the field appears and the form visually grows. This is the same pattern used by the Model field (it too is hidden until useAgents data arrives), so the precedent is already set. It's worth noting in case you want to add a loading placeholder or a Skeleton matching the field height to prevent the layout shift.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/emdash-desktop/src/renderer/features/tasks/conversations/create-conversation-modal.tsx
Line: 163-190

Comment:
**Prompt field flashes in after async load**

`usePromptLibrary` starts with `data = undefined`, so `value` falls back to `[]` and `savedPrompts.length > 0` is `false` — the Prompt field is hidden on the initial render. Once the React Query fetch resolves, if the library is non-empty, the field appears and the form visually grows. This is the same pattern used by the Model field (it too is hidden until `useAgents` data arrives), so the precedent is already set. It's worth noting in case you want to add a loading placeholder or a `Skeleton` matching the field height to prevent the layout shift.

How can I resolve this? If you propose a fix, please make it concise.

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!

… create conversation

  modal

  Replaces the saved-prompt select and bespoke agent/model/toggle fields
  with the shared InitialConversationField, so users get a free-text
  initial message with prompt library insertion via the + popover.
  Exposes createDisabled from useInitialConversationState to keep the
  Create button gated on agent availability.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant