Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit b7ae794

Browse files
authored
feat: Centralize Twig slash command handling (split out from session work) (#1064)
1 parent 5bf406f commit b7ae794

3 files changed

Lines changed: 95 additions & 48 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { AvailableCommand } from "@agentclientprotocol/sdk";
2+
import { track } from "@renderer/lib/analytics";
3+
import { ANALYTICS_EVENTS, type FeedbackType } from "@shared/types/analytics";
4+
import { toast } from "@utils/toast";
5+
6+
interface CommandContext {
7+
taskId: string;
8+
repoPath: string | null | undefined;
9+
session: {
10+
taskRunId?: string;
11+
logUrl?: string;
12+
events: unknown[];
13+
} | null;
14+
taskRun: { id?: string; log_url?: string } | null;
15+
}
16+
17+
interface TwigCommand {
18+
name: string;
19+
description: string;
20+
input?: { hint: string };
21+
execute: (
22+
args: string | undefined,
23+
context: CommandContext,
24+
) => Promise<void> | void;
25+
}
26+
27+
function makeFeedbackCommand(
28+
name: string,
29+
feedbackType: FeedbackType,
30+
label: string,
31+
): TwigCommand {
32+
return {
33+
name,
34+
description: `Capture ${label.toLowerCase()} feedback`,
35+
input: { hint: "optional comment" },
36+
execute(args, ctx) {
37+
track(ANALYTICS_EVENTS.TASK_FEEDBACK, {
38+
task_id: ctx.taskId,
39+
task_run_id: ctx.session?.taskRunId ?? ctx.taskRun?.id,
40+
log_url: ctx.session?.logUrl ?? ctx.taskRun?.log_url,
41+
event_count: ctx.session?.events.length ?? 0,
42+
feedback_type: feedbackType,
43+
feedback_comment: args?.trim() || undefined,
44+
});
45+
toast.success(`${label} feedback captured`);
46+
},
47+
};
48+
}
49+
50+
const commands: TwigCommand[] = [
51+
makeFeedbackCommand("good", "good", "Positive"),
52+
makeFeedbackCommand("bad", "bad", "Negative"),
53+
makeFeedbackCommand("feedback", "general", "General"),
54+
];
55+
56+
export const TWIG_COMMANDS: AvailableCommand[] = commands.map((cmd) => ({
57+
name: cmd.name,
58+
description: cmd.description,
59+
input: cmd.input,
60+
}));
61+
62+
const commandMap = new Map(commands.map((cmd) => [cmd.name, cmd]));
63+
64+
export async function tryExecuteTwigCommand(
65+
text: string,
66+
context: CommandContext,
67+
): Promise<boolean> {
68+
const match = text.match(/^\/(\S+)(?:\s+(.*))?$/);
69+
if (!match) return false;
70+
71+
const cmd = commandMap.get(match[1]);
72+
if (!cmd) return false;
73+
74+
await cmd.execute(match[2], context);
75+
return true;
76+
}

‎apps/twig/src/renderer/features/message-editor/suggestions/getSuggestions.ts‎

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,11 @@
11
import type { AvailableCommand } from "@agentclientprotocol/sdk";
2+
import { TWIG_COMMANDS } from "@features/message-editor/commands";
23
import { getAvailableCommandsForTask } from "@features/sessions/stores/sessionStore";
34
import { fetchRepoFiles, searchFiles } from "@hooks/useRepoFiles";
45
import Fuse, { type IFuseOptions } from "fuse.js";
56
import { useDraftStore } from "../stores/draftStore";
67
import type { CommandSuggestionItem, FileSuggestionItem } from "../types";
78

8-
const TWIG_COMMANDS: AvailableCommand[] = [
9-
{
10-
name: "good",
11-
description: "Capture positive feedback",
12-
input: { hint: "optional comment" },
13-
},
14-
{
15-
name: "bad",
16-
description: "Capture negative feedback",
17-
input: { hint: "optional comment" },
18-
},
19-
{
20-
name: "feedback",
21-
description: "Capture general feedback",
22-
input: { hint: "optional comment" },
23-
},
24-
];
25-
269
const COMMAND_LIMIT = 5;
2710

2811
const COMMAND_FUSE_OPTIONS: IFuseOptions<AvailableCommand> = {

‎apps/twig/src/renderer/features/task-detail/components/TaskLogsPanel.tsx‎

Lines changed: 18 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { BackgroundWrapper } from "@components/BackgroundWrapper";
22
import { ErrorBoundary } from "@components/ErrorBoundary";
3+
import { tryExecuteTwigCommand } from "@features/message-editor/commands";
34
import { useDraftStore } from "@features/message-editor/stores/draftStore";
45
import { SessionView } from "@features/sessions/components/SessionView";
56
import { getSessionService } from "@features/sessions/service/service";
@@ -13,13 +14,11 @@ import { WorkspaceSetupPrompt } from "@features/task-detail/components/Workspace
1314
import { useWorkspaceStore } from "@features/workspace/stores/workspaceStore";
1415
import { useConnectivity } from "@hooks/useConnectivity";
1516
import { Box, Button, Flex, Spinner, Text } from "@radix-ui/themes";
16-
import { track } from "@renderer/lib/analytics";
1717
import { logger } from "@renderer/lib/logger";
1818
import { useNavigationStore } from "@renderer/stores/navigationStore";
1919
import { useTaskDirectoryStore } from "@renderer/stores/taskDirectoryStore";
2020
import { trpcVanilla } from "@renderer/trpc/client";
2121
import type { Task } from "@shared/types";
22-
import { ANALYTICS_EVENTS, type FeedbackType } from "@shared/types/analytics";
2322
import { useQueryClient } from "@tanstack/react-query";
2423
import { getTaskRepository } from "@utils/repository";
2524
import { toast } from "@utils/toast";
@@ -168,29 +167,19 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
168167

169168
const handleSendPrompt = useCallback(
170169
async (text: string) => {
171-
const feedbackMatch = text.match(/^\/(good|bad|feedback)(?:\s+(.*))?$/);
172-
if (feedbackMatch) {
173-
const rawType = feedbackMatch[1];
174-
const feedbackType: FeedbackType =
175-
rawType === "feedback" ? "general" : (rawType as FeedbackType);
176-
const comment = feedbackMatch[2]?.trim() || undefined;
177-
track(ANALYTICS_EVENTS.TASK_FEEDBACK, {
178-
task_id: taskId,
179-
task_run_id: session?.taskRunId ?? task.latest_run?.id,
180-
log_url: session?.logUrl ?? task.latest_run?.log_url,
181-
event_count: events.length,
182-
feedback_type: feedbackType,
183-
feedback_comment: comment,
184-
});
185-
const label =
186-
feedbackType === "good"
187-
? "Positive"
188-
: feedbackType === "bad"
189-
? "Negative"
190-
: "General";
191-
toast.success(`${label} feedback captured`);
192-
return;
193-
}
170+
const handled = await tryExecuteTwigCommand(text, {
171+
taskId,
172+
repoPath,
173+
session: session
174+
? {
175+
taskRunId: session.taskRunId,
176+
logUrl: session.logUrl,
177+
events,
178+
}
179+
: null,
180+
taskRun: task.latest_run ?? null,
181+
});
182+
if (handled) return;
194183

195184
try {
196185
markAsViewed(taskId);
@@ -215,13 +204,12 @@ export function TaskLogsPanel({ taskId, task }: TaskLogsPanelProps) {
215204
},
216205
[
217206
taskId,
207+
repoPath,
218208
markActivity,
219209
markAsViewed,
220-
events.length,
221-
session?.logUrl,
222-
session?.taskRunId,
223-
task.latest_run?.id,
224-
task.latest_run?.log_url,
210+
events,
211+
session,
212+
task.latest_run,
225213
],
226214
);
227215

0 commit comments

Comments
 (0)