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:
@@ -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.`;
|
||||
Reference in New Issue
Block a user