• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / codegate-ui / 13281988581

12 Feb 2025 09:19AM CUT coverage: 68.137% (+0.6%) from 67.507%
13281988581

Pull #304

github

web-flow
Merge 844f93a5c into 00bf20466
Pull Request #304: feat: extend new revert logic to "custom instructions"

357 of 584 branches covered (61.13%)

Branch coverage included in aggregate %.

24 of 25 new or added lines in 4 files covered. (96.0%)

755 of 1048 relevant lines covered (72.04%)

70.46 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.54
/src/features/workspace/components/workspace-custom-instructions.tsx
1
import Editor, { type Theme } from "@monaco-editor/react";
2
import {
3
  Button,
4
  Card,
5
  CardBody,
6
  CardFooter,
7
  DarkModeContext,
8
  Dialog,
9
  DialogCloseButton,
10
  DialogContent,
11
  DialogFooter,
12
  DialogHeader,
13
  DialogModal,
14
  DialogModalOverlay,
15
  DialogTitle,
16
  DialogTrigger,
17
  FieldGroup,
18
  Form,
19
  Input,
20
  Link,
21
  Loader,
22
  SearchField,
23
  SearchFieldClearButton,
24
  Text,
25
  TextLink,
26
} from "@stacklok/ui-kit";
27
import {
28
  Dispatch,
29
  FormEvent,
30
  SetStateAction,
31
  useCallback,
32
  useContext,
33
  useEffect,
34
  useMemo,
35
  useState,
36
} from "react";
37

38
import { twMerge } from "tailwind-merge";
39
import {
40
  V1GetWorkspaceCustomInstructionsData,
41
  V1GetWorkspaceCustomInstructionsResponse,
42
  V1SetWorkspaceCustomInstructionsData,
43
} from "@/api/generated";
44

45
import {
46
  QueryCacheNotifyEvent,
47
  QueryClient,
48
  useQueryClient,
49
} from "@tanstack/react-query";
50
import { v1GetWorkspaceCustomInstructionsQueryKey } from "@/api/generated/@tanstack/react-query.gen";
51
import { useQueryGetWorkspaceCustomInstructions } from "../hooks/use-query-get-workspace-custom-instructions";
52
import { useMutationSetWorkspaceCustomInstructions } from "../hooks/use-mutation-set-workspace-custom-instructions";
53
import Fuse from "fuse.js";
54
import systemPrompts from "../constants/built-in-system-prompts.json";
55
import { MessageTextSquare02, SearchMd } from "@untitled-ui/icons-react";
56
import { invalidateQueries } from "@/lib/react-query-utils";
57
import { useFormState } from "@/hooks/useFormState";
58
import { FormButtons } from "@/components/FormButtons";
59

60
type DarkModeContextValue = {
61
  preference: "dark" | "light" | null;
62
  override: "dark" | "light" | null;
63
};
64

65
function inferDarkMode(
66
  contextValue:
67
    | null
68
    | [DarkModeContextValue, Dispatch<SetStateAction<DarkModeContextValue>>],
69
): Theme {
70
  if (contextValue === null) return "light";
44!
71

72
  // Handle override
73
  if (contextValue[0].override === "dark") return "vs-dark";
44!
74
  if (contextValue[0].override === "light") return "light";
44!
75

76
  // Handle preference
77
  if (contextValue[0].preference === "dark") return "vs-dark";
44!
78
  return "light";
44✔
79
}
80

81
function EditorLoadingUI() {
82
  return (
83
    // arbitrary value to match the monaco editor height
84
    // eslint-disable-next-line tailwindcss/no-unnecessary-arbitrary-value
85
    <div className="min-h-[20rem] w-full flex items-center justify-center">
86
      <Loader className="my-auto" />
87
    </div>
88
  );
89
}
90

91
function isGetWorkspaceCustomInstructionsQuery(
92
  queryKey: unknown,
93
  options: V1GetWorkspaceCustomInstructionsData,
94
): boolean {
95
  return (
19✔
96
    Array.isArray(queryKey) &&
38✔
97
    queryKey[0]._id ===
98
      v1GetWorkspaceCustomInstructionsQueryKey(options)[0]?._id
99
  );
100
}
101

102
function getCustomInstructionsFromEvent(
103
  event: QueryCacheNotifyEvent,
104
): string | null {
105
  if ("action" in event === false || "data" in event.action === false)
6!
106
    return null;
×
107
  return (
6✔
108
    (
6!
109
      event.action.data as
110
        | V1GetWorkspaceCustomInstructionsResponse
111
        | undefined
112
        | null
113
    )?.prompt ?? null
114
  );
115
}
116

117
function useCustomInstructionsValue({
118
  initialValue,
119
  options,
120
  queryClient,
121
}: {
122
  initialValue: string;
123
  options: V1GetWorkspaceCustomInstructionsData;
124
  queryClient: QueryClient;
125
}) {
126
  const formState = useFormState({ prompt: initialValue });
44✔
127
  const { values, updateFormValues } = formState;
44✔
128

129
  // Subscribe to changes in the workspace system prompt value in the query cache
130
  useEffect(() => {
44✔
131
    const queryCache = queryClient.getQueryCache();
43✔
132
    const unsubscribe = queryCache.subscribe((event) => {
43✔
133
      if (
153✔
134
        event.type === "updated" &&
221✔
135
        event.action.type === "success" &&
136
        isGetWorkspaceCustomInstructionsQuery(
137
          event.query.options.queryKey,
138
          options,
139
        )
140
      ) {
141
        const prompt: string | null = getCustomInstructionsFromEvent(event);
6✔
142
        if (prompt === values.prompt || prompt === null) return;
6✔
143

144
        updateFormValues({ prompt });
4✔
145
      }
146
    });
147

148
    return () => {
43✔
149
      return unsubscribe();
43✔
150
    };
151
  }, [options, queryClient, updateFormValues, values.prompt]);
152

153
  return { ...formState };
44✔
154
}
155

156
type PromptPresetPickerProps = {
157
  onActivate: (text: string) => void;
158
};
159

160
function PromptPresetPicker({ onActivate }: PromptPresetPickerProps) {
161
  const [query, setQuery] = useState<string>("");
×
162

163
  const fuse = new Fuse(systemPrompts, {
×
164
    keys: ["name", "text"],
165
  });
166

167
  const handleActivate = useCallback(
×
168
    (prompt: string) => {
169
      onActivate(prompt);
×
170
    },
171
    [onActivate],
172
  );
173

174
  return (
175
    <>
176
      <DialogHeader>
177
        <DialogTitle>Choose a prompt template</DialogTitle>
178
        <DialogCloseButton />
179
      </DialogHeader>
180
      <DialogContent className="p-0 relative">
181
        <div
182
          className="bg-base pt-4 px-4 pb-2 mb-2 sticky top-0 z-10"
183
          style={{
184
            boxShadow: "0px 4px 4px 4px var(--bg-base)",
185
          }}
186
        >
187
          <SearchField className="w-full" value={query} onChange={setQuery}>
188
            <FieldGroup>
189
              <Input
190
                isBorderless
191
                icon={<SearchMd />}
192
                placeholder="Type to search"
193
                autoFocus
194
              />
195
              {query.length > 0 ? <SearchFieldClearButton /> : null}
×
196
            </FieldGroup>
197
          </SearchField>
198
        </div>
199

200
        <div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 px-4 pb-4">
201
          {fuse.search(query.length > 0 ? query : " ").map(({ item }) => {
×
202
            return (
203
              <Card className="min-h-96">
204
                <h2 className="font-bold p-2 flex gap-2 items-center">
205
                  <MessageTextSquare02 className="size-4" />
206
                  <div className="truncate">{item.name}</div>
207
                </h2>
208
                <pre className="h-72 overflow-hidden text-wrap text-sm bg-gray-50 p-2 overflow-y-auto">
209
                  {item.text}
210
                </pre>
211
                <div className="flex gap-4 justify-between p-2">
212
                  <div className="h-full items-center">
213
                    <div className="flex h-full items-center max-w-52 text-clip">
214
                      {item.contributors.map((contributor) => (
215
                        <Link
216
                          className="font-bold text-sm no-underline text-secondary flex gap-1 items-center hover:bg-gray-200 h-full px-2 rounded-md"
217
                          target="_blank"
218
                          href={`https://github.com/${contributor}/`}
219
                        >
220
                          <img
221
                            className="size-6 rounded-full"
222
                            src={`https://github.com/${contributor}.png?size=24`}
223
                          />
224
                          <span className="truncate">{contributor}</span>
225
                        </Link>
226
                      ))}
227
                    </div>
228
                  </div>
229
                  <Button
230
                    slot="close"
231
                    variant="secondary"
232
                    onPress={() => {
233
                      handleActivate(item.text);
×
234
                    }}
235
                  >
236
                    Use prompt
237
                  </Button>
238
                </div>
239
              </Card>
240
            );
241
          })}
242
        </div>
243
      </DialogContent>
244
      <DialogFooter>
245
        <span className="ml-auto">
246
          Prompt templates sourced from{" "}
247
          <TextLink
248
            className="text-primary"
249
            href="https://github.com/PatrickJS/awesome-cursorrules"
250
          >
251
            PatrickJS/awesome-cursorrules
252
          </TextLink>
253
        </span>
254
      </DialogFooter>
255
    </>
256
  );
257
}
258

259
export function WorkspaceCustomInstructions({
260
  className,
261
  workspaceName,
262
  isArchived,
263
}: {
264
  className?: string;
265
  workspaceName: string;
266
  isArchived: boolean | undefined;
267
}) {
268
  const context = useContext(DarkModeContext);
44✔
269
  const theme: Theme = inferDarkMode(context);
44✔
270

271
  const options: V1GetWorkspaceCustomInstructionsData &
272
    Omit<V1SetWorkspaceCustomInstructionsData, "body"> = useMemo(
44✔
273
    () => ({
7✔
274
      path: { workspace_name: workspaceName },
275
    }),
276
    [workspaceName],
277
  );
278

279
  const queryClient = useQueryClient();
44✔
280

281
  const {
282
    data: customInstructionsResponse,
283
    isPending: isCustomInstructionsPending,
284
  } = useQueryGetWorkspaceCustomInstructions(options);
44✔
285
  const { mutateAsync, isPending: isMutationPending } =
286
    useMutationSetWorkspaceCustomInstructions(options);
44✔
287

288
  const formState = useCustomInstructionsValue({
44✔
289
    initialValue: customInstructionsResponse?.prompt ?? "",
58✔
290
    options,
291
    queryClient,
292
  });
293

294
  const { values, updateFormValues } = formState;
44✔
295

296
  const handleSubmit = useCallback(
44✔
297
    (value: string) => {
298
      mutateAsync(
1✔
299
        { ...options, body: { prompt: value } },
300
        {
301
          onSuccess: () =>
302
            invalidateQueries(queryClient, [
1✔
303
              v1GetWorkspaceCustomInstructionsQueryKey,
304
            ]),
305
        },
306
      );
307
    },
308
    [mutateAsync, options, queryClient],
309
  );
310

311
  return (
312
    <Form
313
      onSubmit={(e: FormEvent) => {
314
        e.preventDefault();
1✔
315
        handleSubmit(values.prompt);
1✔
316
      }}
317
      validationBehavior="aria"
318
    >
319
      <Card className={twMerge(className, "shrink-0")}>
320
        <CardBody>
321
          <Text className="text-primary">Custom instructions</Text>
322
          <Text className="text-secondary mb-4">
323
            Pass custom instructions to your LLM to augment its behavior, and
324
            save time & tokens.
325
          </Text>
326
          <div className="border border-gray-200 rounded overflow-hidden">
327
            {isCustomInstructionsPending ? (
328
              <EditorLoadingUI />
14✔
329
            ) : (
330
              <Editor
331
                options={{
332
                  minimap: { enabled: false },
333
                  readOnly: isArchived,
334
                }}
335
                value={values.prompt}
336
                onChange={(v) => updateFormValues({ prompt: v ?? "" })}
21!
337
                height="20rem"
338
                defaultLanguage="Markdown"
339
                theme={theme}
340
                className={twMerge("bg-base", isArchived ? "opacity-25" : "")}
30!
341
              />
342
            )}
343
          </div>
344
        </CardBody>
345
        <CardFooter className="justify-end gap-2">
346
          <FormButtons
347
            isPending={isMutationPending}
348
            formState={formState}
349
            canSubmit={
350
              !isArchived && !isCustomInstructionsPending && !isMutationPending
118✔
351
            }
352
          >
353
            <DialogTrigger>
354
              <Button variant="secondary">Use a preset</Button>
355
              <DialogModalOverlay isDismissable>
356
                <DialogModal isDismissable>
357
                  <Dialog
358
                    width="lg"
359
                    className="min-h-[44rem]"
360
                    style={{ maxWidth: "min(calc(100vw - 64px), 1200px)" }}
361
                  >
362
                    <PromptPresetPicker
363
                      onActivate={(prompt: string) => {
NEW
364
                        updateFormValues({ prompt });
×
365
                      }}
366
                    />
367
                  </Dialog>
368
                </DialogModal>
369
              </DialogModalOverlay>
370
            </DialogTrigger>
371
          </FormButtons>
372
        </CardFooter>
373
      </Card>
374
    </Form>
375
  );
376
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc