Skip to content
Closed
Show file tree
Hide file tree
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
15 changes: 13 additions & 2 deletions packages/app/src/app/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -723,12 +723,14 @@ export default function App() {
setTemplateDraftPrompt,
templateDraftScope,
setTemplateDraftScope,
templateDraftAutoRun,
setTemplateDraftAutoRun,
workspaceTemplates,
globalTemplates,
openTemplateModal,
saveTemplate,
deleteTemplate,
runTemplate,
applyTemplate,
loadWorkspaceTemplates,
} = templateState;

Expand Down Expand Up @@ -1474,8 +1476,15 @@ export default function App() {
applyThemeMode(isDark ? "dark" : "light");
});

// Listen for setPrompt events from template system (when autoRun is disabled)
const handleSetPrompt = (e: CustomEvent<string>) => {
setPrompt(e.detail);
};
window.addEventListener("openwork:setPrompt", handleSetPrompt as EventListener);

onCleanup(() => {
unsubscribeTheme();
window.removeEventListener("openwork:setPrompt", handleSetPrompt as EventListener);
});

createEffect(() => {
Expand Down Expand Up @@ -2090,7 +2099,7 @@ export default function App() {
setTemplateDraftScope(scope);
},
openTemplateModal,
runTemplate,
applyTemplate,
deleteTemplate,
refreshSkills: (options?: { force?: boolean }) => refreshSkills(options).catch(() => undefined),
refreshPlugins: (scopeOverride?: PluginScope) =>
Expand Down Expand Up @@ -2332,12 +2341,14 @@ export default function App() {
description={templateDraftDescription()}
prompt={templateDraftPrompt()}
scope={templateDraftScope()}
autoRun={templateDraftAutoRun()}
onClose={() => setTemplateModalOpen(false)}
onSave={saveTemplate}
onTitleChange={setTemplateDraftTitle}
onDescriptionChange={setTemplateDraftDescription}
onPromptChange={setTemplateDraftPrompt}
onScopeChange={setTemplateDraftScope}
onAutoRunChange={setTemplateDraftAutoRun}
/>

<ReloadWorkspaceToast
Expand Down
21 changes: 21 additions & 0 deletions packages/app/src/app/components/template-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ export type TemplateModalProps = {
description: string;
prompt: string;
scope: "workspace" | "global";
autoRun: boolean;
onClose: () => void;
onSave: () => void;
onTitleChange: (value: string) => void;
onDescriptionChange: (value: string) => void;
onPromptChange: (value: string) => void;
onScopeChange: (value: "workspace" | "global") => void;
onAutoRunChange: (value: boolean) => void;
};

export default function TemplateModal(props: TemplateModalProps) {
Expand Down Expand Up @@ -88,6 +90,25 @@ export default function TemplateModal(props: TemplateModalProps) {
/>
<div class="mt-1 text-xs text-gray-10">{translate("templates.prompt_hint")}</div>
</label>

{/* Auto-run Toggle */}
<div class="flex items-center justify-between bg-gray-1 p-3 rounded-xl border border-gray-6 gap-3">
<div class="min-w-0">
<div class="text-sm text-gray-12">{translate("templates.auto_run_label")}</div>
<div class="text-xs text-gray-7">{translate("templates.auto_run_hint")}</div>
</div>
<button
type="button"
class={`px-3 py-1 rounded-full text-xs font-medium border transition-colors shrink-0 ${
props.autoRun
? "bg-gray-12/10 text-gray-12 border-gray-6/20"
: "text-gray-10 border-gray-6 hover:text-gray-12"
}`}
onClick={() => props.onAutoRunChange(!props.autoRun)}
>
{props.autoRun ? "On" : "Off"}
</button>
</div>
</div>

<div class="mt-6 flex justify-end gap-2">
Expand Down
8 changes: 4 additions & 4 deletions packages/app/src/app/pages/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export type DashboardViewProps = {
setTemplateDraftScope: (value: "workspace" | "global") => void;
openTemplateModal: () => void;
resetTemplateDraft?: (scope?: "workspace" | "global") => void;
runTemplate: (template: WorkspaceTemplate) => void;
applyTemplate: (template: WorkspaceTemplate) => void;
deleteTemplate: (templateId: string) => void;
refreshSkills: (options?: { force?: boolean }) => void;
refreshPlugins: (scopeOverride?: PluginScope) => void;
Expand Down Expand Up @@ -537,15 +537,15 @@ export default function DashboardView(props: DashboardViewProps) {
<For each={quickTemplates()}>
{(t) => (
<button
onClick={() => props.runTemplate(t)}
onClick={() => props.applyTemplate(t)}
class="group p-5 rounded-2xl bg-gray-2/30 border border-gray-6/50 hover:bg-gray-2 hover:border-gray-7 transition-all text-left"
>
<div class="w-10 h-10 rounded-full bg-gray-4 flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<FileText size={20} class="text-indigo-11" />
</div>
<h4 class="font-medium text-gray-12 mb-1">{t.title}</h4>
<p class="text-sm text-gray-10">
{t.description || "Run a saved workflow"}
{t.description || "Apply a saved workflow"}
</p>
</button>
)}
Expand Down Expand Up @@ -698,7 +698,7 @@ export default function DashboardView(props: DashboardViewProps) {
setTemplateDraftScope={props.setTemplateDraftScope}
openTemplateModal={props.openTemplateModal}
resetTemplateDraft={props.resetTemplateDraft}
runTemplate={props.runTemplate}
applyTemplate={props.applyTemplate}
deleteTemplate={props.deleteTemplate}
/>
</Match>
Expand Down
10 changes: 5 additions & 5 deletions packages/app/src/app/pages/session.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -429,7 +429,7 @@ export default function SessionView(props: SessionViewProps) {
}
setPrevArtifactCount(count);
});

createEffect(() => {
const files = props.workingFiles;
const count = files.length;
Expand Down Expand Up @@ -824,7 +824,7 @@ export default function SessionView(props: SessionViewProps) {
}
`}
</style>

<Show when={props.messages.length === 0}>
<div class="text-center py-20 space-y-4">
<div class="w-16 h-16 bg-gray-2 rounded-3xl mx-auto flex items-center justify-center border border-gray-6">
Expand All @@ -837,7 +837,7 @@ export default function SessionView(props: SessionViewProps) {
</div>
</Show>

<MessageList
<MessageList
messages={props.messages}
artifacts={props.artifacts}
developerMode={props.developerMode}
Expand Down Expand Up @@ -904,7 +904,7 @@ export default function SessionView(props: SessionViewProps) {

<div ref={(el) => (messagesEndEl = el)} />
</div>

<Minimap containerRef={() => chatContainerEl} messages={props.messages} />

<Show when={artifactToast()}>
Expand All @@ -914,7 +914,7 @@ export default function SessionView(props: SessionViewProps) {
</Show>
</div>

<Composer
<Composer
prompt={props.prompt}
setPrompt={props.setPrompt}
busy={props.busy}
Expand Down
10 changes: 5 additions & 5 deletions packages/app/src/app/pages/templates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type TemplatesViewProps = {
setTemplateDraftScope: (value: "workspace" | "global") => void;
openTemplateModal: () => void;
resetTemplateDraft?: (scope?: "workspace" | "global") => void;
runTemplate: (template: WorkspaceTemplate) => void;
applyTemplate: (template: WorkspaceTemplate) => void;
deleteTemplate: (templateId: string) => void;
};

Expand Down Expand Up @@ -68,9 +68,9 @@ export default function TemplatesView(props: TemplatesViewProps) {
<div class="mt-2 text-xs text-gray-7 font-mono">{formatRelativeTime(t.createdAt)}</div>
</div>
<div class="shrink-0 flex gap-2">
<Button variant="secondary" onClick={() => props.runTemplate(t)} disabled={props.busy}>
<Button variant="secondary" onClick={() => props.applyTemplate(t)} disabled={props.busy}>
<Play size={16} />
Run
Apply
</Button>
<Button variant="danger" onClick={() => props.deleteTemplate(t.id)} disabled={props.busy}>
<Trash2 size={16} />
Expand All @@ -97,9 +97,9 @@ export default function TemplatesView(props: TemplatesViewProps) {
<div class="mt-2 text-xs text-gray-7 font-mono">{formatRelativeTime(t.createdAt)}</div>
</div>
<div class="shrink-0 flex gap-2">
<Button variant="secondary" onClick={() => props.runTemplate(t)} disabled={props.busy}>
<Button variant="secondary" onClick={() => props.applyTemplate(t)} disabled={props.busy}>
<Play size={16} />
Run
Apply
</Button>
<Button variant="danger" onClick={() => props.deleteTemplate(t.id)} disabled={props.busy}>
<Trash2 size={16} />
Expand Down
73 changes: 58 additions & 15 deletions packages/app/src/app/template-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,21 +34,23 @@ export function createTemplateState(options: {
const [templateDraftDescription, setTemplateDraftDescription] = createSignal("");
const [templateDraftPrompt, setTemplateDraftPrompt] = createSignal("");
const [templateDraftScope, setTemplateDraftScope] = createSignal<"workspace" | "global">("workspace");
const [templateDraftAutoRun, setTemplateDraftAutoRun] = createSignal(true);

const workspaceTemplates = createMemo(() => templates().filter((t) => t.scope === "workspace"));
const globalTemplates = createMemo(() => templates().filter((t) => t.scope === "global"));

function openTemplateModal() {
const seedTitle = options.selectedSession()?.title ?? "";
const seedPrompt = options.lastPromptSent() || options.prompt();
const nextDraft = buildTemplateDraft({ seedTitle, seedPrompt, scope: "workspace" });
const nextDraft = buildTemplateDraft({ seedTitle, seedPrompt, scope: "workspace", autoRun: true });

resetTemplateDraft(
{
setTitle: setTemplateDraftTitle,
setDescription: setTemplateDraftDescription,
setPrompt: setTemplateDraftPrompt,
setScope: setTemplateDraftScope,
setAutoRun: setTemplateDraftAutoRun,
},
nextDraft.scope,
);
Expand All @@ -59,10 +61,11 @@ export function createTemplateState(options: {
}

async function saveTemplate() {
const draft = buildTemplateDraft({ scope: templateDraftScope() });
const draft = buildTemplateDraft({ scope: templateDraftScope(), autoRun: templateDraftAutoRun() });
draft.title = templateDraftTitle().trim();
draft.description = templateDraftDescription().trim();
draft.prompt = templateDraftPrompt().trim();
draft.autoRun = templateDraftAutoRun();

if (!draft.title || !draft.prompt) {
options.setError(t("app.error.title_prompt_required", currentLocale()));
Expand Down Expand Up @@ -140,7 +143,8 @@ export function createTemplateState(options: {
setGlobalTemplatesLoaded(true);
}

async function runTemplate(template: WorkspaceTemplate) {
// Apply a template to create a new session
async function applyTemplate(template: WorkspaceTemplate) {
if (options.isDemoMode()) {
options.setView("session");
return;
Expand All @@ -149,6 +153,12 @@ export function createTemplateState(options: {
const c = options.client();
if (!c) return;

// Check autoRun setting (default to true for backwards compatibility)
const shouldAutoRun = template.autoRun !== false;

// Trim the prompt before using
const trimmedPrompt = template.prompt.trim();

options.setBusy(true);
options.setError(null);

Expand All @@ -160,19 +170,24 @@ export function createTemplateState(options: {
await options.selectSession(session.id);
options.setView("session");

const model = options.defaultModel();
if (shouldAutoRun) {
const model = options.defaultModel();

await c.session.promptAsync({
sessionID: session.id,
model,
variant: options.modelVariant() ?? undefined,
parts: [{ type: "text", text: template.prompt }],
});
await c.session.promptAsync({
sessionID: session.id,
model,
variant: options.modelVariant() ?? undefined,
parts: [{ type: "text", text: trimmedPrompt }],
});

options.setSessionModelById((current) => ({
...current,
[session.id]: model,
}));
options.setSessionModelById((current) => ({
...current,
[session.id]: model,
}));
} else {
// Don't auto-run: populate the prompt input and let user send manually
window.dispatchEvent(new CustomEvent("openwork:setPrompt", { detail: trimmedPrompt }));
}
} catch (e) {
const message = e instanceof Error ? e.message : t("app.unknown_error", currentLocale());
options.setError(addOpencodeCacheHint(message));
Expand Down Expand Up @@ -206,18 +221,33 @@ export function createTemplateState(options: {
const parsedFrontmatter = parseTemplateFrontmatter(raw);
if (parsedFrontmatter) {
const meta = parsedFrontmatter.data;

const title = typeof meta.title === "string" ? meta.title : t("common.untitled", currentLocale());
const promptText = parsedFrontmatter.body ?? "";
if (!promptText.trim()) return false;

const createdAtValue = Number(meta.createdAt);

// Convert string "true"/"false" to actual booleans (frontmatter parser returns strings)
let autoRunValue: boolean;
if (typeof meta.autoRun === "boolean") {
autoRunValue = meta.autoRun;
} else if (meta.autoRun === "false") {
autoRunValue = false;
} else if (meta.autoRun === "true") {
autoRunValue = true;
} else {
autoRunValue = true; // default
}

pushTemplate({
id: typeof meta.id === "string" ? meta.id : fallbackId,
title,
description: typeof meta.description === "string" ? meta.description : "",
prompt: promptText,
createdAt: Number.isFinite(createdAtValue) && createdAtValue > 0 ? createdAtValue : Date.now(),
scope: "workspace",
autoRun: autoRunValue,
});
return true;
}
Expand All @@ -229,13 +259,24 @@ export function createTemplateState(options: {
const promptText = typeof parsed.prompt === "string" ? parsed.prompt : "";
if (!promptText.trim()) return false;

// For JSON, autoRun should already be a boolean if properly serialized
let jsonAutoRunValue: boolean;
if (typeof parsed.autoRun === "boolean") {
jsonAutoRunValue = parsed.autoRun;
} else if (parsed.autoRun === "false" || parsed.autoRun === false) {
jsonAutoRunValue = false;
} else {
jsonAutoRunValue = true; // default
}

pushTemplate({
id: typeof parsed.id === "string" ? parsed.id : fallbackId,
title,
description: typeof parsed.description === "string" ? parsed.description : "",
prompt: promptText,
createdAt: typeof parsed.createdAt === "number" ? parsed.createdAt : Date.now(),
scope: "workspace",
autoRun: jsonAutoRunValue,
});

return true;
Expand Down Expand Up @@ -306,12 +347,14 @@ export function createTemplateState(options: {
setTemplateDraftPrompt,
templateDraftScope,
setTemplateDraftScope,
templateDraftAutoRun,
setTemplateDraftAutoRun,
workspaceTemplates,
globalTemplates,
openTemplateModal,
saveTemplate,
deleteTemplate,
runTemplate,
applyTemplate,
loadWorkspaceTemplates,
};
}
1 change: 1 addition & 0 deletions packages/app/src/app/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export type Template = {
description: string;
prompt: string;
createdAt: number;
autoRun?: boolean;
};

export type SkillCard = {
Expand Down
Loading