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

stacklok / codegate-ui / 13031292266

29 Jan 2025 12:27PM UTC coverage: 68.733% (+1.7%) from 67.071%
13031292266

Pull #220

github

web-flow
Merge 04fad425d into 25a531eba
Pull Request #220: feat(muxes): model overrides matcher

387 of 673 branches covered (57.5%)

Branch coverage included in aggregate %.

41 of 60 new or added lines in 8 files covered. (68.33%)

2 existing lines in 1 file now uncovered.

844 of 1118 relevant lines covered (75.49%)

63.88 hits per line

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

75.32
/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
  DialogHeader,
12
  DialogModal,
13
  DialogModalOverlay,
14
  DialogTitle,
15
  DialogTrigger,
16
  FieldGroup,
17
  Input,
18
  Link,
19
  Loader,
20
  SearchField,
21
  SearchFieldClearButton,
22
  Text,
23
} from "@stacklok/ui-kit";
24
import {
25
  Dispatch,
26
  SetStateAction,
27
  useCallback,
28
  useContext,
29
  useEffect,
30
  useMemo,
31
  useState,
32
} from "react";
33

34
import { twMerge } from "tailwind-merge";
35
import {
36
  V1GetWorkspaceCustomInstructionsData,
37
  V1GetWorkspaceCustomInstructionsResponse,
38
  V1SetWorkspaceCustomInstructionsData,
39
} from "@/api/generated";
40

41
import {
42
  QueryCacheNotifyEvent,
43
  QueryClient,
44
  useQueryClient,
45
} from "@tanstack/react-query";
46
import { v1GetWorkspaceCustomInstructionsQueryKey } from "@/api/generated/@tanstack/react-query.gen";
47
import { useQueryGetWorkspaceCustomInstructions } from "../hooks/use-query-get-workspace-custom-instructions";
48
import { useMutationSetWorkspaceCustomInstructions } from "../hooks/use-mutation-set-workspace-custom-instructions";
49
import { Bot, Download, Search } from "lucide-react";
50
import Fuse from "fuse.js";
51
import systemPrompts from "../constants/built-in-system-prompts.json";
52

53
type DarkModeContextValue = {
54
  preference: "dark" | "light" | null;
55
  override: "dark" | "light" | null;
56
};
57

58
function inferDarkMode(
59
  contextValue:
60
    | null
61
    | [DarkModeContextValue, Dispatch<SetStateAction<DarkModeContextValue>>],
62
): Theme {
63
  if (contextValue === null) return "light";
39!
64

65
  // Handle override
66
  if (contextValue[0].override === "dark") return "vs-dark";
39!
67
  if (contextValue[0].override === "light") return "light";
39!
68

69
  // Handle preference
70
  if (contextValue[0].preference === "dark") return "vs-dark";
39!
71
  return "light";
39✔
72
}
73

74
function EditorLoadingUI() {
75
  return (
76
    // arbitrary value to match the monaco editor height
77
    // eslint-disable-next-line tailwindcss/no-unnecessary-arbitrary-value
78
    <div className="min-h-[20rem] w-full flex items-center justify-center">
79
      <Loader className="my-auto" />
80
    </div>
81
  );
82
}
83

84
function isGetWorkspaceCustomInstructionsQuery(
85
  queryKey: unknown,
86
  options: V1GetWorkspaceCustomInstructionsData,
87
): boolean {
88
  return (
15✔
89
    Array.isArray(queryKey) &&
30✔
90
    queryKey[0]._id ===
91
      v1GetWorkspaceCustomInstructionsQueryKey(options)[0]?._id
92
  );
93
}
94

95
function getCustomInstructionsFromEvent(
96
  event: QueryCacheNotifyEvent,
97
): string | null {
98
  if ("action" in event === false || "data" in event.action === false)
5!
99
    return null;
×
100
  return (
5✔
101
    (
5!
102
      event.action.data as
103
        | V1GetWorkspaceCustomInstructionsResponse
104
        | undefined
105
        | null
106
    )?.prompt ?? null
107
  );
108
}
109

110
function useCustomInstructionsValue({
111
  initialValue,
112
  options,
113
  queryClient,
114
}: {
115
  initialValue: string;
116
  options: V1GetWorkspaceCustomInstructionsData;
117
  queryClient: QueryClient;
118
}) {
119
  const [value, setValue] = useState<string>(initialValue);
39✔
120

121
  // Subscribe to changes in the workspace system prompt value in the query cache
122
  useEffect(() => {
39✔
123
    const queryCache = queryClient.getQueryCache();
30✔
124
    const unsubscribe = queryCache.subscribe((event) => {
30✔
125
      if (
136✔
126
        event.type === "updated" &&
188✔
127
        event.action.type === "success" &&
128
        isGetWorkspaceCustomInstructionsQuery(
129
          event.query.options.queryKey,
130
          options,
131
        )
132
      ) {
133
        const prompt: string | null = getCustomInstructionsFromEvent(event);
5✔
134
        if (prompt === value || prompt === null) return;
5✔
135

136
        setValue(prompt);
3✔
137
      }
138
    });
139

140
    return () => {
30✔
141
      return unsubscribe();
30✔
142
    };
143
  }, [options, queryClient, value]);
144

145
  return { value, setValue };
39✔
146
}
147

148
type PromptPresetPickerProps = {
149
  onActivate: (text: string) => void;
150
};
151

152
function PromptPresetPicker({ onActivate }: PromptPresetPickerProps) {
153
  const [query, setQuery] = useState<string>("");
×
154

155
  const fuse = new Fuse(systemPrompts, {
×
156
    keys: ["name", "text"],
157
  });
158

159
  const handleActivate = useCallback(
×
160
    (prompt: string) => {
161
      onActivate(prompt);
×
162
    },
163
    [onActivate],
164
  );
165

166
  return (
167
    <>
168
      <DialogHeader>
169
        <div className="w-1/3">
170
          <DialogTitle>Choose a prompt template</DialogTitle>
171
        </div>
172
        <div className="w-1/3">
173
          <SearchField
174
            className="w-full max-w-96"
175
            value={query}
176
            onChange={setQuery}
177
          >
178
            <FieldGroup>
179
              <Input icon={<Search />} placeholder="Type to search" autoFocus />
180
              {query.length > 0 ? <SearchFieldClearButton /> : null}
×
181
            </FieldGroup>
182
          </SearchField>
183
        </div>
184
        <div className="w-1/3 flex justify-end">
185
          <DialogCloseButton />
186
        </div>
187
      </DialogHeader>
188
      <DialogContent>
189
        <div className="grid grid-flow-row grid-cols-1 lg:grid-cols-2 xl:grid-cols-3  gap-4 overflow-auto justify-around ">
190
          {fuse.search(query.length > 0 ? query : " ").map(({ item }) => {
×
191
            return (
192
              <Card className=" flex flex-col">
193
                <h2 className="font-bold p-2 flex gap-2 items-center">
194
                  <Bot className="size-4" />
195
                  <div className="truncate">{item.name}</div>
196
                </h2>
197
                <pre className="h-72 overflow-hidden text-wrap text-sm bg-gray-50 p-2 overflow-y-auto">
198
                  {item.text}
199
                </pre>
200
                <div className="flex gap-4 justify-between p-2">
201
                  <div className="h-full items-center">
202
                    <div className="flex h-full items-center max-w-52 text-clip">
203
                      {item.contributors.map((contributor) => (
204
                        <Link
205
                          className="font-bold text-sm no-underline text-secondary flex gap-1 items-center hover:bg-gray-200 h-full px-2 rounded-md"
206
                          target="_blank"
207
                          href={`https://github.com/${contributor}/`}
208
                        >
209
                          <img
210
                            className="size-6 rounded-full"
211
                            src={`https://github.com/${contributor}.png?size=24`}
212
                          />
213
                          <span className="truncate">{contributor}</span>
214
                        </Link>
215
                      ))}
216
                    </div>
217
                  </div>
218
                  <Button
219
                    isIcon
220
                    slot="close"
221
                    variant="secondary"
222
                    onPress={() => {
UNCOV
223
                      handleActivate(item.text);
×
224
                    }}
225
                  >
226
                    <Download />
227
                  </Button>
228
                </div>
229
              </Card>
230
            );
231
          })}
232
        </div>
233
      </DialogContent>
234
    </>
235
  );
236
}
237

238
export function WorkspaceCustomInstructions({
239
  className,
240
  workspaceName,
241
  isArchived,
242
}: {
243
  className?: string;
244
  workspaceName: string;
245
  isArchived: boolean | undefined;
246
}) {
247
  const context = useContext(DarkModeContext);
39✔
248
  const theme: Theme = inferDarkMode(context);
39✔
249

250
  const options: V1GetWorkspaceCustomInstructionsData &
251
    Omit<V1SetWorkspaceCustomInstructionsData, "body"> = useMemo(
39✔
252
    () => ({
6✔
253
      path: { workspace_name: workspaceName },
254
    }),
255
    [workspaceName],
256
  );
257

258
  const queryClient = useQueryClient();
39✔
259

260
  const {
261
    data: customInstructionsResponse,
262
    isPending: isCustomInstructionsPending,
263
  } = useQueryGetWorkspaceCustomInstructions(options);
39✔
264
  const { mutateAsync, isPending: isMutationPending } =
265
    useMutationSetWorkspaceCustomInstructions(options);
39✔
266

267
  const { setValue, value } = useCustomInstructionsValue({
39✔
268
    initialValue: customInstructionsResponse?.prompt ?? "",
51✔
269
    options,
270
    queryClient,
271
  });
272

273
  const handleSubmit = useCallback(
39✔
274
    (value: string) => {
275
      mutateAsync(
1✔
276
        { ...options, body: { prompt: value } },
277
        {
278
          onSuccess: () => {
279
            queryClient.invalidateQueries({
1✔
280
              queryKey: v1GetWorkspaceCustomInstructionsQueryKey(options),
281
              refetchType: "all",
282
            });
283
          },
284
        },
285
      );
286
    },
287
    [mutateAsync, options, queryClient],
288
  );
289

290
  return (
291
    <Card className={twMerge(className, "shrink-0")}>
292
      <CardBody>
293
        <Text className="text-primary">Custom instructions</Text>
294
        <Text className="text-secondary mb-4">
295
          Pass custom instructions to your LLM to augment its behavior, and save
296
          time & tokens.
297
        </Text>
298
        <div className="border border-gray-200 rounded overflow-hidden">
299
          {isCustomInstructionsPending ? (
300
            <EditorLoadingUI />
12✔
301
          ) : (
302
            <Editor
303
              options={{
304
                minimap: { enabled: false },
305
                readOnly: isArchived,
306
              }}
307
              value={value}
308
              onChange={(v) => setValue(v ?? "")}
21!
309
              height="20rem"
310
              defaultLanguage="Markdown"
311
              theme={theme}
312
              className={twMerge("bg-base", isArchived ? "opacity-25" : "")}
27!
313
            />
314
          )}
315
        </div>
316
      </CardBody>
317
      <CardFooter className="justify-end gap-2">
318
        <DialogTrigger>
319
          <Button>Use a preset</Button>
320
          <DialogModalOverlay isDismissable>
321
            <DialogModal isDismissable>
322
              <Dialog
323
                width="lg"
324
                className="flex flex-col p-4 gap-4 "
325
                style={{ maxWidth: "min(calc(100vw - 64px), 1200px)" }}
326
              >
327
                <PromptPresetPicker
328
                  onActivate={(prompt: string) => {
UNCOV
329
                    setValue(prompt);
×
330
                  }}
331
                />
332
              </Dialog>
333
            </DialogModal>
334
          </DialogModalOverlay>
335
        </DialogTrigger>
336
        <Button
337
          isPending={isMutationPending}
338
          isDisabled={Boolean(isArchived ?? isCustomInstructionsPending)}
51✔
339
          onPress={() => handleSubmit(value)}
1✔
340
        >
341
          Save
342
        </Button>
343
      </CardFooter>
344
    </Card>
345
  );
346
}
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