Skip to content

Commit b6d00d3

Browse files
committed
🤖 refactor: rename heartbeat default prompt field
1 parent f3a2722 commit b6d00d3

File tree

10 files changed

+179
-96
lines changed

10 files changed

+179
-96
lines changed

src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.test.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -87,25 +87,25 @@ describe("WorkspaceHeartbeatModal", () => {
8787
cleanupDom = null;
8888
});
8989

90-
test("reveals the message field when enabled and saves a custom message", async () => {
90+
test("reveals the default prompt field when enabled and saves a custom default prompt", async () => {
9191
settingsByWorkspaceId.set(
9292
"ws-1",
9393
createHeartbeatSettings({
9494
enabled: false,
95-
message: "Review the current workspace status before acting.",
95+
defaultPrompt: "Review the current workspace status before acting.",
9696
})
9797
);
9898
const onOpenChange = mock((_open: boolean) => undefined);
9999
const view = render(
100100
<WorkspaceHeartbeatModal workspaceId="ws-1" open={true} onOpenChange={onOpenChange} />
101101
);
102102

103-
expect(view.queryByLabelText("Heartbeat message")).toBeNull();
103+
expect(view.queryByLabelText("Heartbeat default prompt")).toBeNull();
104104

105105
fireEvent.click(view.getByRole("switch", { name: "Enable workspace heartbeats" }));
106106

107107
const messageField = (await waitFor(() =>
108-
view.getByLabelText("Heartbeat message")
108+
view.getByLabelText("Heartbeat default prompt")
109109
)) as HTMLTextAreaElement;
110110
expect(messageField.value).toBe("Review the current workspace status before acting.");
111111
expect(messageField.placeholder).toBe(HEARTBEAT_DEFAULT_MESSAGE_BODY);
@@ -125,15 +125,15 @@ describe("WorkspaceHeartbeatModal", () => {
125125
next: {
126126
enabled: true,
127127
intervalMs: HEARTBEAT_DEFAULT_INTERVAL_MS,
128-
message: "Check the pending review queue and summarize next steps.",
128+
defaultPrompt: "Check the pending review queue and summarize next steps.",
129129
},
130130
},
131131
]);
132132
});
133133
expect(onOpenChange).toHaveBeenCalledWith(false);
134134
});
135135

136-
test("saves messages longer than 1000 characters without a client-side cap", async () => {
136+
test("saves default prompts longer than 1000 characters without a client-side cap", async () => {
137137
expect(LONG_HEARTBEAT_MESSAGE.length).toBeGreaterThan(1_000);
138138
settingsByWorkspaceId.set("ws-1", createHeartbeatSettings({ enabled: true }));
139139

@@ -143,7 +143,7 @@ describe("WorkspaceHeartbeatModal", () => {
143143
);
144144

145145
const messageField = (await waitFor(() =>
146-
view.getByLabelText("Heartbeat message")
146+
view.getByLabelText("Heartbeat default prompt")
147147
)) as HTMLTextAreaElement;
148148
expect(messageField.getAttribute("maxlength")).toBeNull();
149149

@@ -162,20 +162,20 @@ describe("WorkspaceHeartbeatModal", () => {
162162
next: {
163163
enabled: true,
164164
intervalMs: HEARTBEAT_DEFAULT_INTERVAL_MS,
165-
message: LONG_HEARTBEAT_MESSAGE,
165+
defaultPrompt: LONG_HEARTBEAT_MESSAGE,
166166
},
167167
},
168168
]);
169169
});
170170
expect(onOpenChange).toHaveBeenCalledWith(false);
171171
});
172172

173-
test("reopens with the saved message for the same workspace and does not bleed across workspaces", async () => {
173+
test("reopens with the saved default prompt for the same workspace and does not bleed across workspaces", async () => {
174174
settingsByWorkspaceId.set(
175175
"ws-1",
176176
createHeartbeatSettings({
177177
enabled: true,
178-
message: LONG_HEARTBEAT_MESSAGE,
178+
defaultPrompt: LONG_HEARTBEAT_MESSAGE,
179179
})
180180
);
181181
settingsByWorkspaceId.set("ws-2", createHeartbeatSettings({ enabled: true }));
@@ -189,7 +189,7 @@ describe("WorkspaceHeartbeatModal", () => {
189189
);
190190

191191
await waitFor(() => {
192-
expect((view.getByLabelText("Heartbeat message") as HTMLTextAreaElement).value).toBe(
192+
expect((view.getByLabelText("Heartbeat default prompt") as HTMLTextAreaElement).value).toBe(
193193
LONG_HEARTBEAT_MESSAGE
194194
);
195195
});
@@ -210,7 +210,7 @@ describe("WorkspaceHeartbeatModal", () => {
210210
);
211211

212212
await waitFor(() => {
213-
expect((view.getByLabelText("Heartbeat message") as HTMLTextAreaElement).value).toBe(
213+
expect((view.getByLabelText("Heartbeat default prompt") as HTMLTextAreaElement).value).toBe(
214214
LONG_HEARTBEAT_MESSAGE
215215
);
216216
});
@@ -224,16 +224,18 @@ describe("WorkspaceHeartbeatModal", () => {
224224
);
225225

226226
await waitFor(() => {
227-
expect((view.getByLabelText("Heartbeat message") as HTMLTextAreaElement).value).toBe("");
227+
expect((view.getByLabelText("Heartbeat default prompt") as HTMLTextAreaElement).value).toBe(
228+
""
229+
);
228230
});
229231
});
230232

231-
test("clearing the message removes the override instead of saving whitespace", async () => {
233+
test("clearing the default prompt removes the override instead of saving whitespace", async () => {
232234
settingsByWorkspaceId.set(
233235
"ws-1",
234236
createHeartbeatSettings({
235237
enabled: true,
236-
message: "Review the open PR status before sending a follow-up.",
238+
defaultPrompt: "Review the open PR status before sending a follow-up.",
237239
})
238240
);
239241

@@ -246,7 +248,7 @@ describe("WorkspaceHeartbeatModal", () => {
246248
);
247249

248250
const messageField = (await waitFor(() =>
249-
view.getByLabelText("Heartbeat message")
251+
view.getByLabelText("Heartbeat default prompt")
250252
)) as HTMLTextAreaElement;
251253
fireEvent.input(messageField, { target: { value: " " } });
252254
await waitFor(() => {
@@ -261,7 +263,7 @@ describe("WorkspaceHeartbeatModal", () => {
261263
next: {
262264
enabled: true,
263265
intervalMs: HEARTBEAT_DEFAULT_INTERVAL_MS,
264-
message: "",
266+
defaultPrompt: "",
265267
},
266268
},
267269
]);

src/browser/components/WorkspaceHeartbeatModal/WorkspaceHeartbeatModal.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,13 @@ function getValidationErrorMessage(value: string): string | null {
8282
return null;
8383
}
8484

85-
function normalizeDraftMessage(value: string): string | undefined {
85+
function normalizeDraftDefaultPrompt(value: string): string | undefined {
8686
const trimmedValue = value.trim();
8787
return trimmedValue.length > 0 ? trimmedValue : undefined;
8888
}
8989

90-
function getDraftMessageForSave(value: string): string {
91-
return normalizeDraftMessage(value) ?? "";
90+
function getDraftDefaultPromptForSave(value: string): string {
91+
return normalizeDraftDefaultPrompt(value) ?? "";
9292
}
9393

9494
export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
@@ -99,14 +99,14 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
9999
const [draftIntervalMinutes, setDraftIntervalMinutes] = useState(
100100
formatIntervalMinutes(HEARTBEAT_DEFAULT_INTERVAL_MS)
101101
);
102-
const [draftMessage, setDraftMessage] = useState("");
102+
const [draftDefaultPrompt, setDraftDefaultPrompt] = useState("");
103103
const [draftDirty, setDraftDirty] = useState(false);
104104
const previousOpenRef = useRef(props.open);
105105
const previousWorkspaceIdRef = useRef(props.workspaceId);
106-
const messageTextareaRef = useRef<HTMLTextAreaElement | null>(null);
106+
const defaultPromptTextareaRef = useRef<HTMLTextAreaElement | null>(null);
107107
const lastSyncedSettingsRef = useRef<Pick<
108108
typeof settings,
109-
"enabled" | "intervalMs" | "message"
109+
"enabled" | "intervalMs" | "defaultPrompt"
110110
> | null>(null);
111111

112112
useEffect(() => {
@@ -117,7 +117,7 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
117117
lastSyncedSettings == null ||
118118
lastSyncedSettings.enabled !== settings.enabled ||
119119
lastSyncedSettings.intervalMs !== settings.intervalMs ||
120-
lastSyncedSettings.message !== settings.message;
120+
lastSyncedSettings.defaultPrompt !== settings.defaultPrompt;
121121

122122
previousOpenRef.current = props.open;
123123
previousWorkspaceIdRef.current = props.workspaceId;
@@ -130,12 +130,12 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
130130
if (didOpen || workspaceChanged || (!draftDirty && settingsChanged)) {
131131
setDraftEnabled(settings.enabled);
132132
setDraftIntervalMinutes(formatIntervalMinutes(settings.intervalMs));
133-
setDraftMessage(settings.message ?? "");
133+
setDraftDefaultPrompt(settings.defaultPrompt ?? "");
134134
setDraftDirty(false);
135135
lastSyncedSettingsRef.current = {
136136
enabled: settings.enabled,
137137
intervalMs: settings.intervalMs,
138-
message: settings.message,
138+
defaultPrompt: settings.defaultPrompt,
139139
};
140140
}
141141
}, [
@@ -145,7 +145,7 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
145145
props.workspaceId,
146146
settings.enabled,
147147
settings.intervalMs,
148-
settings.message,
148+
settings.defaultPrompt,
149149
]);
150150

151151
const validationError = getValidationErrorMessage(draftIntervalMinutes);
@@ -182,7 +182,9 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
182182
intervalMs: parsedMinutes * MS_PER_MINUTE,
183183
// Read directly from the textarea on save so the final keystroke is preserved even if the
184184
// click lands before React finishes flushing the last state update.
185-
message: getDraftMessageForSave(messageTextareaRef.current?.value ?? draftMessage),
185+
defaultPrompt: getDraftDefaultPromptForSave(
186+
defaultPromptTextareaRef.current?.value ?? draftDefaultPrompt
187+
),
186188
});
187189
if (didSave) {
188190
props.onOpenChange(false);
@@ -260,25 +262,25 @@ export function WorkspaceHeartbeatModal(props: WorkspaceHeartbeatModalProps) {
260262

261263
{draftEnabled && (
262264
<div className="mt-4 space-y-2">
263-
<label htmlFor="workspace-heartbeat-message" className="block">
264-
<div className="text-foreground text-sm font-medium">Message</div>
265+
<label htmlFor="workspace-heartbeat-default-prompt" className="block">
266+
<div className="text-foreground text-sm font-medium">Default prompt</div>
265267
<div className="text-muted mt-1 text-xs">
266-
Leave empty to use the default heartbeat message.
268+
Leave empty to use the default heartbeat prompt.
267269
</div>
268270
</label>
269271
<textarea
270-
ref={messageTextareaRef}
271-
id="workspace-heartbeat-message"
272+
ref={defaultPromptTextareaRef}
273+
id="workspace-heartbeat-default-prompt"
272274
rows={4}
273-
value={draftMessage}
275+
value={draftDefaultPrompt}
274276
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
275-
setDraftMessage(event.target.value);
277+
setDraftDefaultPrompt(event.target.value);
276278
setDraftDirty(true);
277279
}}
278280
disabled={isSaving}
279281
className="border-border-medium bg-background-secondary text-foreground focus:border-accent focus:ring-accent min-h-[120px] w-full resize-y rounded-md border p-3 text-sm leading-relaxed focus:ring-1 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
280282
placeholder={HEARTBEAT_DEFAULT_MESSAGE_BODY}
281-
aria-label="Heartbeat message"
283+
aria-label="Heartbeat default prompt"
282284
/>
283285
</div>
284286
)}

src/browser/hooks/useWorkspaceHeartbeat.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,9 @@ function normalizeHeartbeatSettings(
3333
return getDefaultHeartbeatSettings();
3434
}
3535

36-
const trimmedMessage = heartbeat.message?.trim();
37-
return trimmedMessage
38-
? { ...heartbeat, message: trimmedMessage }
36+
const trimmedDefaultPrompt = heartbeat.defaultPrompt?.trim();
37+
return trimmedDefaultPrompt
38+
? { ...heartbeat, defaultPrompt: trimmedDefaultPrompt }
3939
: {
4040
enabled: heartbeat.enabled,
4141
intervalMs: heartbeat.intervalMs,

src/browser/utils/chatCommands.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,12 @@ describe("processSlashCommand - heartbeat-set", () => {
282282
);
283283
});
284284

285-
test("enables workspace heartbeats with the requested interval without clearing the saved message", async () => {
285+
test("enables workspace heartbeats with the requested interval without clearing the saved default prompt", async () => {
286286
const heartbeatGet = mock(() =>
287287
Promise.resolve({
288288
enabled: true as const,
289289
intervalMs: 45 * 60 * 1000,
290-
message: "Review the workspace status before taking action.",
290+
defaultPrompt: "Review the workspace status before taking action.",
291291
})
292292
);
293293
const heartbeatSet = mock(() => Promise.resolve({ success: true, data: undefined }));
@@ -314,7 +314,7 @@ describe("processSlashCommand - heartbeat-set", () => {
314314
workspaceId: "test-ws",
315315
enabled: true,
316316
intervalMs: 30 * 60 * 1000,
317-
message: "Review the workspace status before taking action.",
317+
defaultPrompt: "Review the workspace status before taking action.",
318318
});
319319
expect(context.setToast).toHaveBeenCalledWith(
320320
expect.objectContaining({
@@ -358,12 +358,12 @@ describe("processSlashCommand - heartbeat-set", () => {
358358
);
359359
});
360360

361-
test("preserves the configured interval and message when disabling workspace heartbeats", async () => {
361+
test("preserves the configured interval and default prompt when disabling workspace heartbeats", async () => {
362362
const heartbeatGet = mock(() =>
363363
Promise.resolve({
364364
enabled: true as const,
365365
intervalMs: 45 * 60 * 1000,
366-
message: "Review the workspace status before taking action.",
366+
defaultPrompt: "Review the workspace status before taking action.",
367367
})
368368
);
369369
const heartbeatSet = mock(() => Promise.resolve({ success: true, data: undefined }));
@@ -389,7 +389,7 @@ describe("processSlashCommand - heartbeat-set", () => {
389389
workspaceId: "test-ws",
390390
enabled: false,
391391
intervalMs: 45 * 60 * 1000,
392-
message: "Review the workspace status before taking action.",
392+
defaultPrompt: "Review the workspace status before taking action.",
393393
});
394394
expect(context.setToast).toHaveBeenCalledWith(
395395
expect.objectContaining({
@@ -432,7 +432,7 @@ describe("processSlashCommand - heartbeat-set", () => {
432432
Promise.resolve({
433433
enabled: true as const,
434434
intervalMs: 45 * 60 * 1000,
435-
message: "Review the workspace status before taking action.",
435+
defaultPrompt: "Review the workspace status before taking action.",
436436
})
437437
);
438438
const heartbeatSet = mock(() =>
@@ -459,7 +459,7 @@ describe("processSlashCommand - heartbeat-set", () => {
459459
workspaceId: "test-ws",
460460
enabled: true,
461461
intervalMs: 30 * 60 * 1000,
462-
message: "Review the workspace status before taking action.",
462+
defaultPrompt: "Review the workspace status before taking action.",
463463
});
464464
expect(context.setToast).toHaveBeenCalledWith(
465465
expect.objectContaining({

src/browser/utils/chatCommands.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ export async function processSlashCommand(
371371
}
372372

373373
// Preserve the stored cadence when toggling heartbeats off so re-enabling restores it,
374-
// and keep any saved custom heartbeat message when commands only change cadence.
374+
// and keep any saved custom heartbeat default prompt when commands only change cadence.
375375
const intervalMs =
376376
parsed.minutes === null
377377
? (currentHeartbeatSettings?.intervalMs ?? HEARTBEAT_DEFAULT_INTERVAL_MS)
@@ -380,10 +380,10 @@ export async function processSlashCommand(
380380
workspaceId: context.workspaceId,
381381
enabled: parsed.minutes !== null,
382382
intervalMs,
383-
// Omit message when the best-effort read failed; WorkspaceService preserves the
384-
// persisted custom message when this field is absent.
385-
...(currentHeartbeatSettings?.message != null
386-
? { message: currentHeartbeatSettings.message }
383+
// Omit defaultPrompt when the best-effort read failed; WorkspaceService preserves
384+
// the persisted custom prompt when this field is absent.
385+
...(currentHeartbeatSettings?.defaultPrompt != null
386+
? { defaultPrompt: currentHeartbeatSettings.defaultPrompt }
387387
: {}),
388388
});
389389

src/common/orpc/schemas/workspace.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ export const WorkspaceHeartbeatSettingsSchema = z.object({
3939
intervalMs: z.number().int().min(HEARTBEAT_MIN_INTERVAL_MS).max(HEARTBEAT_MAX_INTERVAL_MS).meta({
4040
description: "Heartbeat interval in milliseconds for this workspace.",
4141
}),
42-
message: z.string().optional().meta({
43-
description:
44-
"Optional custom instruction body appended after the fixed workspace heartbeat lead-in.",
42+
defaultPrompt: z.string().optional().meta({
43+
description: "Optional custom prompt appended after the fixed workspace heartbeat lead-in.",
4544
}),
4645
});
4746

src/node/orpc/router.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3057,7 +3057,7 @@ export const router = (authToken?: string) => {
30573057
context.workspaceService.setHeartbeatSettings(input.workspaceId, {
30583058
enabled: input.enabled,
30593059
intervalMs: input.intervalMs,
3060-
...(input.message != null ? { message: input.message } : {}),
3060+
...(input.defaultPrompt != null ? { defaultPrompt: input.defaultPrompt } : {}),
30613061
})
30623062
),
30633063
},

0 commit comments

Comments
 (0)