fix(jira): use shared prompt template with placeholder autocomplete (#801)

* fix(jira): use shared prompt template with placeholder autocomplete

Default prompt for the JIRA watcher now lives in
config/prompts/jira-issue-watch-default.md (loaded via promptcfg.Get),
matching how the GitHub issue/PR-review watchers source their defaults.
The watch dialog swaps the plain Textarea for the ScriptEditor used by
the GitHub dialog so users get {{...}} autocomplete, and the inline
hardcoded default string is gone from both backend and frontend.

* fix(jira): treat whitespace-only stored prompt as empty on hydrate

CodeRabbit caught that w.prompt || DEFAULT keeps a stored "   " value,
which canSave then rejects — dialog reopens in an unsavable state. Fall
back to the default whenever the trimmed value is empty.

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
This commit is contained in:
João Salavisa
2026-05-03 20:48:10 +01:00
committed by GitHub
parent b2dcd29818
commit a59909ca5a
4 changed files with 168 additions and 18 deletions
@@ -0,0 +1,24 @@
You have been assigned a JIRA ticket to work on.
**Ticket:** {{issue.url}}
**Key:** {{issue.key}}
**Summary:** {{issue.summary}}
**Type:** {{issue.type}}
**Status:** {{issue.status}}
**Priority:** {{issue.priority}}
**Assignee:** {{issue.assignee}}
**Reporter:** {{issue.reporter}}
**Project:** {{issue.project}}
## Description
{{issue.description}}
## Instructions
1. Read the ticket carefully and understand the requirements.
2. Explore the codebase to understand the relevant code and architecture.
3. Implement the changes described in the ticket.
4. Write or update tests to cover the changes.
5. Run the test suite to ensure nothing is broken.
6. Commit your changes with a descriptive commit message referencing the ticket key.
@@ -7,6 +7,7 @@ import (
"go.uber.org/zap"
promptcfg "github.com/kandev/kandev/config/prompts"
"github.com/kandev/kandev/internal/events"
"github.com/kandev/kandev/internal/events/bus"
"github.com/kandev/kandev/internal/jira"
@@ -162,12 +163,12 @@ func (s *Service) attachJiraIssueTaskID(ctx context.Context, evt *jira.NewJiraIs
}
// interpolateJiraPrompt replaces {{issue.*}} placeholders with ticket fields.
// When the template is empty, the agent receives a minimal default that links
// to the JIRA ticket — enough context to start without forcing every watch to
// supply boilerplate.
// When the template is empty, falls back to the embedded default in
// config/prompts/jira-issue-watch-default.md — same pattern the GitHub
// issue/PR watchers use so the default lives in one editable place.
func interpolateJiraPrompt(template string, t *jira.JiraTicket) string {
if strings.TrimSpace(template) == "" {
template = "Investigate JIRA ticket {{issue.key}}: {{issue.summary}}\n\n{{issue.url}}"
template = promptcfg.Get("jira-issue-watch-default")
}
r := strings.NewReplacer(
"{{issue.key}}", t.Key,
@@ -15,11 +15,21 @@ import {
} from "@kandev/ui/dialog";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@kandev/ui/select";
import { Textarea } from "@kandev/ui/textarea";
import { IconInfoCircle } from "@tabler/icons-react";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kandev/ui/tooltip";
import { useAppStore } from "@/components/state-provider";
import { useSettingsData } from "@/hooks/domains/settings/use-settings-data";
import { useWorkflows } from "@/hooks/use-workflows";
import { useWorkflowSteps, stepPlaceholder } from "@/hooks/use-workflow-steps";
import { searchJiraTickets } from "@/lib/api/domains/jira-api";
import {
ScriptEditor,
computeEditorHeight,
} from "@/components/settings/profile-edit/script-editor";
import {
JIRA_ISSUE_WATCH_PLACEHOLDERS,
DEFAULT_JIRA_ISSUE_WATCH_PROMPT,
} from "@/components/jira/jira-issue-watch-placeholders";
import type {
CreateJiraIssueWatchInput,
JiraIssueWatch,
@@ -52,7 +62,6 @@ type FormState = {
};
const DEFAULT_JQL = `project = PROJ AND status = "Open" ORDER BY created DESC`;
const DEFAULT_PROMPT = `Investigate JIRA ticket {{issue.key}}: {{issue.summary}}\n\n{{issue.url}}`;
function makeEmptyForm(workspaceId: string): FormState {
return {
@@ -62,7 +71,7 @@ function makeEmptyForm(workspaceId: string): FormState {
workflowStepId: "",
agentProfileId: "",
executorProfileId: "",
prompt: DEFAULT_PROMPT,
prompt: DEFAULT_JIRA_ISSUE_WATCH_PROMPT,
enabled: true,
pollInterval: 300,
};
@@ -76,7 +85,7 @@ function formStateFromWatch(w: JiraIssueWatch): FormState {
workflowStepId: w.workflowStepId,
agentProfileId: w.agentProfileId,
executorProfileId: w.executorProfileId,
prompt: w.prompt || DEFAULT_PROMPT,
prompt: w.prompt.trim() ? w.prompt : DEFAULT_JIRA_ISSUE_WATCH_PROMPT,
enabled: w.enabled,
pollInterval: w.pollIntervalSeconds,
};
@@ -183,22 +192,49 @@ function JQLField({ jql, onChange }: { jql: string; onChange: (v: string) => voi
);
}
function PlaceholdersHelp() {
return (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<IconInfoCircle className="h-3.5 w-3.5 text-muted-foreground/50 hover:text-muted-foreground cursor-help shrink-0" />
</TooltipTrigger>
<TooltipContent className="max-w-xs" align="start">
<p className="text-xs font-medium mb-1">Available placeholders:</p>
<ul className="text-xs space-y-0.5">
{JIRA_ISSUE_WATCH_PLACEHOLDERS.map((p) => (
<li key={p.key}>
<code className="text-[10px] bg-white/15 px-1 rounded">{`{{${p.key}}}`}</code>{" "}
<span className="opacity-70">{p.description}</span>
</li>
))}
</ul>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
}
function PromptField({ value, onChange }: { value: string; onChange: (v: string) => void }) {
return (
<div className="space-y-1.5">
<Label>Task Prompt</Label>
<div className="flex items-center gap-1.5">
<Label>Task Prompt</Label>
<PlaceholdersHelp />
</div>
<p className="text-xs text-muted-foreground">
Sent to the agent for each new ticket. Placeholders: {`{{issue.key}}`},{" "}
{`{{issue.summary}}`}, {`{{issue.url}}`}, {`{{issue.status}}`}, {`{{issue.priority}}`},{" "}
{`{{issue.type}}`}, {`{{issue.assignee}}`}, {`{{issue.reporter}}`}, {`{{issue.project}}`},{" "}
{`{{issue.description}}`}.
The prompt sent to the agent for each new ticket. Type {"{{"} to insert placeholders.
</p>
<Textarea
value={value}
onChange={(e) => onChange(e.target.value)}
rows={5}
className="text-sm resize-y"
/>
<div className="rounded-md border border-border overflow-hidden">
<ScriptEditor
value={value}
onChange={onChange}
language="markdown"
height={computeEditorHeight(value)}
lineNumbers="off"
placeholders={JIRA_ISSUE_WATCH_PLACEHOLDERS}
/>
</div>
</div>
);
}
@@ -0,0 +1,89 @@
import type { ScriptPlaceholder } from "@/components/settings/profile-edit/script-editor-completions";
export const JIRA_ISSUE_WATCH_PLACEHOLDERS: ScriptPlaceholder[] = [
{
key: "issue.key",
description: "Ticket key",
example: "PROJ-42",
executor_types: [],
},
{
key: "issue.summary",
description: "Ticket summary",
example: "Login fails on mobile",
executor_types: [],
},
{
key: "issue.url",
description: "Ticket URL",
example: "https://acme.atlassian.net/browse/PROJ-42",
executor_types: [],
},
{
key: "issue.status",
description: "Status name",
example: "In Progress",
executor_types: [],
},
{
key: "issue.priority",
description: "Priority",
example: "High",
executor_types: [],
},
{
key: "issue.type",
description: "Issue type",
example: "Bug",
executor_types: [],
},
{
key: "issue.assignee",
description: "Assignee display name",
example: "Alice Example",
executor_types: [],
},
{
key: "issue.reporter",
description: "Reporter display name",
example: "Bob Example",
executor_types: [],
},
{
key: "issue.project",
description: "Project key",
example: "PROJ",
executor_types: [],
},
{
key: "issue.description",
description: "Ticket description",
example: "When clicking login...",
executor_types: [],
},
];
export const DEFAULT_JIRA_ISSUE_WATCH_PROMPT = `You have been assigned a JIRA ticket to work on.
**Ticket:** {{issue.url}}
**Key:** {{issue.key}}
**Summary:** {{issue.summary}}
**Type:** {{issue.type}}
**Status:** {{issue.status}}
**Priority:** {{issue.priority}}
**Assignee:** {{issue.assignee}}
**Reporter:** {{issue.reporter}}
**Project:** {{issue.project}}
## Description
{{issue.description}}
## Instructions
1. Read the ticket carefully and understand the requirements.
2. Explore the codebase to understand the relevant code and architecture.
3. Implement the changes described in the ticket.
4. Write or update tests to cover the changes.
5. Run the test suite to ensure nothing is broken.
6. Commit your changes with a descriptive commit message referencing the ticket key.`;