Skip to content

Commit f034e19

Browse files
committed
fix overflow issues and replace pull request dropdown with matching UI from dotcom
1 parent d564d98 commit f034e19

File tree

5 files changed

+191
-71
lines changed

5 files changed

+191
-71
lines changed

pkg/github/pullrequests.go

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -538,23 +538,25 @@ func CreatePullRequest(t translations.TranslationHelperFunc) inventory.ServerToo
538538
return utils.NewToolResultError(err.Error()), nil, nil
539539
}
540540

541-
// When insiders mode is enabled, show UI - the host will detect the UI metadata and display the form
542-
if deps.GetFlags().InsidersMode {
541+
// Check if required params for PR creation are provided
542+
title, _ := OptionalParam[string](args, "title")
543+
head, _ := OptionalParam[string](args, "head")
544+
base, _ := OptionalParam[string](args, "base")
545+
546+
// When insiders mode is enabled and required params are missing, show UI
547+
if deps.GetFlags().InsidersMode && (title == "" || head == "" || base == "") {
543548
return utils.NewToolResultText(fmt.Sprintf("Ready to create a pull request in %s/%s. The interactive form will be displayed.", owner, repo)), nil, nil
544549
}
545550

546-
// When not using UI, title/head/base are required
547-
title, err := RequiredParam[string](args, "title")
548-
if err != nil {
549-
return utils.NewToolResultError(err.Error()), nil, nil
551+
// When creating PR, title/head/base are required
552+
if title == "" {
553+
return utils.NewToolResultError("title is required"), nil, nil
550554
}
551-
head, err := RequiredParam[string](args, "head")
552-
if err != nil {
553-
return utils.NewToolResultError(err.Error()), nil, nil
555+
if head == "" {
556+
return utils.NewToolResultError("head is required"), nil, nil
554557
}
555-
base, err := RequiredParam[string](args, "base")
556-
if err != nil {
557-
return utils.NewToolResultError(err.Error()), nil, nil
558+
if base == "" {
559+
return utils.NewToolResultError("base is required"), nil, nil
558560
}
559561

560562
body, err := OptionalParam[string](args, "body")

ui/src/apps/issue-write/App.tsx

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -259,9 +259,9 @@ function CreateIssueApp() {
259259
console.log("Repository search result:", result);
260260
if (result && !result.isError && result.content) {
261261
const textContent = result.content.find(
262-
(c: { type: string }) => c.type === "text"
262+
(c) => c.type === "text"
263263
);
264-
if (textContent?.text) {
264+
if (textContent && textContent.type === "text" && textContent.text) {
265265
const data = JSON.parse(textContent.text);
266266
console.log("Parsed repository data:", data);
267267
const repos = (data.repositories || data.items || []).map(
@@ -457,9 +457,9 @@ function CreateIssueApp() {
457457

458458
if (result && !result.isError && result.content) {
459459
const textContent = result.content.find(
460-
(c: { type: string }) => c.type === "text"
460+
(c) => c.type === "text"
461461
);
462-
if (textContent?.text) {
462+
if (textContent && textContent.type === "text" && textContent.text) {
463463
const issueData = JSON.parse(textContent.text);
464464
console.log("Loaded issue data:", issueData);
465465

@@ -754,15 +754,18 @@ function CreateIssueApp() {
754754
borderBottomWidth={1}
755755
borderBottomStyle="solid"
756756
borderBottomColor="border.default"
757+
sx={{ minWidth: 0, overflow: "hidden" }}
757758
>
758-
<ActionMenu>
759-
<ActionMenu.Button
760-
size="small"
761-
leadingVisual={selectedRepo?.isPrivate ? LockIcon : RepoIcon}
762-
>
763-
{selectedRepo ? selectedRepo.fullName : "Select repository"}
764-
</ActionMenu.Button>
765-
<ActionMenu.Overlay width="large">
759+
<Box sx={{ minWidth: 0, maxWidth: "100%" }}>
760+
<ActionMenu>
761+
<ActionMenu.Button
762+
size="small"
763+
leadingVisual={selectedRepo?.isPrivate ? LockIcon : RepoIcon}
764+
sx={{ maxWidth: "100%", overflow: "hidden", "& > span:last-child": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }}
765+
>
766+
{selectedRepo ? selectedRepo.fullName : "Select repository"}
767+
</ActionMenu.Button>
768+
<ActionMenu.Overlay width="medium">
766769
<ActionList selectionVariant="single">
767770
<Box px={3} py={2}>
768771
<TextInput
@@ -828,6 +831,7 @@ function CreateIssueApp() {
828831
</ActionList>
829832
</ActionMenu.Overlay>
830833
</ActionMenu>
834+
</Box>
831835
</Box>
832836

833837
{/* Error banner */}
@@ -867,7 +871,7 @@ function CreateIssueApp() {
867871
</Box>
868872

869873
{/* Metadata section */}
870-
<Box display="flex" gap={1} mb={3} sx={{ flexWrap: "nowrap", overflow: "hidden" }}>
874+
<Box display="flex" gap={3} mb={3} sx={{ flexWrap: "wrap" }}>
871875
{/* Labels dropdown */}
872876
<ActionMenu>
873877
<ActionMenu.Button size="small" leadingVisual={TagIcon}>

ui/src/apps/pr-write/App.tsx

Lines changed: 81 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@ import {
1212
ActionMenu,
1313
ActionList,
1414
Checkbox,
15+
ButtonGroup,
1516
} from "@primer/react";
1617
import {
1718
GitPullRequestIcon,
1819
CheckCircleIcon,
1920
RepoIcon,
2021
LockIcon,
2122
GitBranchIcon,
23+
TriangleDownIcon,
2224
} from "@primer/octicons-react";
2325
import { AppProvider } from "../../components/AppProvider";
2426
import { useMcpApp } from "../../hooks/useMcpApp";
@@ -199,8 +201,8 @@ function CreatePRApp() {
199201
try {
200202
const result = await callTool("search_repositories", { query: repoFilter, perPage: 10 });
201203
if (result && !result.isError && result.content) {
202-
const textContent = result.content.find((c: { type: string }) => c.type === "text");
203-
if (textContent?.text) {
204+
const textContent = result.content.find((c) => c.type === "text");
205+
if (textContent && textContent.type === "text" && textContent.text) {
204206
const data = JSON.parse(textContent.text);
205207
const repos = (data.repositories || data.items || []).map(
206208
(r: { id?: number; owner?: { login?: string } | string; name?: string; full_name?: string; private?: boolean }) => ({
@@ -291,11 +293,12 @@ function CreatePRApp() {
291293
});
292294

293295
if (result.isError) {
294-
const errorText = result.content?.find((c: { type: string }) => c.type === "text");
295-
setError(errorText?.text || "Failed to create pull request");
296+
const errorText = result.content?.find((c) => c.type === "text");
297+
const errorMessage = errorText && errorText.type === "text" ? errorText.text : "Failed to create pull request";
298+
setError(errorMessage);
296299
} else {
297-
const textContent = result.content?.find((c: { type: string }) => c.type === "text");
298-
if (textContent?.text) {
300+
const textContent = result.content?.find((c) => c.type === "text");
301+
if (textContent && textContent.type === "text" && textContent.text) {
299302
const prData = JSON.parse(textContent.text);
300303
setSuccessPR(prData);
301304
}
@@ -353,15 +356,18 @@ function CreatePRApp() {
353356
borderBottomWidth={1}
354357
borderBottomStyle="solid"
355358
borderBottomColor="border.default"
359+
sx={{ minWidth: 0, overflow: "hidden" }}
356360
>
357-
<ActionMenu>
358-
<ActionMenu.Button
359-
size="small"
360-
leadingVisual={selectedRepo?.isPrivate ? LockIcon : RepoIcon}
361-
>
362-
{selectedRepo ? selectedRepo.fullName : "Select repository"}
363-
</ActionMenu.Button>
364-
<ActionMenu.Overlay width="large">
361+
<Box sx={{ minWidth: 0, maxWidth: "100%" }}>
362+
<ActionMenu>
363+
<ActionMenu.Button
364+
size="small"
365+
leadingVisual={selectedRepo?.isPrivate ? LockIcon : RepoIcon}
366+
sx={{ maxWidth: "100%", overflow: "hidden", "& > span:last-child": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }}
367+
>
368+
{selectedRepo ? selectedRepo.fullName : "Select repository"}
369+
</ActionMenu.Button>
370+
<ActionMenu.Overlay width="medium">
365371
<ActionList selectionVariant="single">
366372
<Box px={3} py={2}>
367373
<TextInput
@@ -412,14 +418,15 @@ function CreatePRApp() {
412418
</ActionList>
413419
</ActionMenu.Overlay>
414420
</ActionMenu>
421+
</Box>
415422
</Box>
416423

417424
{/* Branch selectors */}
418-
<Box display="flex" gap={3} mb={3} alignItems="flex-end">
419-
<Box flex={1}>
425+
<Box display="flex" gap={2} mb={3} alignItems="flex-end" sx={{ minWidth: 0, flexWrap: "wrap" }}>
426+
<Box sx={{ flex: "1 1 120px", minWidth: 0 }}>
420427
<Text sx={{ fontSize: 0, color: "fg.muted", mb: 1, display: "block" }}>base</Text>
421428
<ActionMenu>
422-
<ActionMenu.Button size="small" leadingVisual={GitBranchIcon} sx={{ width: "100%" }}>
429+
<ActionMenu.Button size="small" leadingVisual={GitBranchIcon} sx={{ width: "100%", "& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }}>
423430
{baseBranch || "Select base"}
424431
</ActionMenu.Button>
425432
<ActionMenu.Overlay width="medium">
@@ -455,12 +462,12 @@ function CreatePRApp() {
455462
</ActionMenu>
456463
</Box>
457464

458-
<Text sx={{ color: "fg.muted", pb: 1, px: 1 }}></Text>
465+
<Text sx={{ color: "fg.muted", pb: 1, px: 1, flexShrink: 0 }}></Text>
459466

460-
<Box flex={1}>
467+
<Box sx={{ flex: "1 1 120px", minWidth: 0 }}>
461468
<Text sx={{ fontSize: 0, color: "fg.muted", mb: 1, display: "block" }}>compare</Text>
462469
<ActionMenu>
463-
<ActionMenu.Button size="small" leadingVisual={GitBranchIcon} sx={{ width: "100%" }}>
470+
<ActionMenu.Button size="small" leadingVisual={GitBranchIcon} sx={{ width: "100%", "& > span": { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }}>
464471
{headBranch || "Select head"}
465472
</ActionMenu.Button>
466473
<ActionMenu.Overlay width="medium">
@@ -519,33 +526,62 @@ function CreatePRApp() {
519526
<MarkdownEditor value={body} onChange={setBody} placeholder="Add a description..." />
520527
</Box>
521528

522-
{/* Options */}
523-
<Box mb={3} display="flex" gap={4}>
524-
<FormControl>
525-
<Checkbox checked={isDraft} onChange={(e) => setIsDraft(e.target.checked)} />
526-
<FormControl.Label sx={{ fontWeight: "normal", ml: 1 }}>Create as draft</FormControl.Label>
527-
</FormControl>
528-
<FormControl>
529+
{/* Options and Submit */}
530+
<Box display="flex" justifyContent="space-between" alignItems="flex-end" flexWrap="wrap" gap={3}>
531+
<Box as="label" display="flex" alignItems="center" sx={{ cursor: "pointer", gap: 2 }}>
529532
<Checkbox checked={maintainerCanModify} onChange={(e) => setMaintainerCanModify(e.target.checked)} />
530-
<FormControl.Label sx={{ fontWeight: "normal", ml: 1 }}>Allow maintainer edits</FormControl.Label>
531-
</FormControl>
532-
</Box>
533+
<Text sx={{ fontSize: 1, color: "fg.muted" }}>Allow maintainer edits</Text>
534+
</Box>
533535

534-
{/* Submit button */}
535-
<Box display="flex" justifyContent="flex-end" gap={2}>
536-
<Button
537-
variant="primary"
538-
onClick={handleSubmit}
539-
disabled={isSubmitting || !owner || !repo || !baseBranch || !headBranch}
540-
>
541-
{isSubmitting ? (
542-
<><Spinner size="small" sx={{ mr: 1 }} />Creating...</>
543-
) : isDraft ? (
544-
"Create draft pull request"
545-
) : (
546-
"Create pull request"
547-
)}
548-
</Button>
536+
<ButtonGroup>
537+
<Button
538+
variant="primary"
539+
onClick={handleSubmit}
540+
disabled={isSubmitting || !owner || !repo || !baseBranch || !headBranch}
541+
>
542+
{isSubmitting ? (
543+
<><Spinner size="small" sx={{ mr: 1 }} />Creating...</>
544+
) : isDraft ? (
545+
"Draft pull request"
546+
) : (
547+
"Create pull request"
548+
)}
549+
</Button>
550+
<ActionMenu>
551+
<ActionMenu.Anchor>
552+
<Button
553+
variant="primary"
554+
disabled={isSubmitting || !owner || !repo || !baseBranch || !headBranch}
555+
sx={{ px: 2 }}
556+
aria-label="Select pull request type"
557+
>
558+
<TriangleDownIcon />
559+
</Button>
560+
</ActionMenu.Anchor>
561+
<ActionMenu.Overlay width="medium">
562+
<ActionList selectionVariant="single">
563+
<ActionList.Item selected={!isDraft} onSelect={() => setIsDraft(false)}>
564+
<ActionList.LeadingVisual>
565+
<GitPullRequestIcon />
566+
</ActionList.LeadingVisual>
567+
Create pull request
568+
<ActionList.Description variant="block">
569+
Open a pull request that is ready for review
570+
</ActionList.Description>
571+
</ActionList.Item>
572+
<ActionList.Item selected={isDraft} onSelect={() => setIsDraft(true)}>
573+
<ActionList.LeadingVisual>
574+
<GitPullRequestIcon />
575+
</ActionList.LeadingVisual>
576+
Create draft pull request
577+
<ActionList.Description variant="block">
578+
Cannot be merged until marked ready for review
579+
</ActionList.Description>
580+
</ActionList.Item>
581+
</ActionList>
582+
</ActionMenu.Overlay>
583+
</ActionMenu>
584+
</ButtonGroup>
549585
</Box>
550586
</Box>
551587
</AppProvider>

ui/src/components/MarkdownEditor.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,79 @@ export function MarkdownEditor({
9494
}
9595
}, [value]);
9696

97+
// Handle Enter key for list continuation
98+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
99+
if (e.key !== "Enter" || e.shiftKey) return;
100+
101+
const textarea = textareaRef.current;
102+
if (!textarea) return;
103+
104+
const { selectionStart, value: currentValue } = textarea;
105+
106+
// Get the current line
107+
const beforeCursor = currentValue.substring(0, selectionStart);
108+
const lastNewline = beforeCursor.lastIndexOf("\n");
109+
const currentLine = beforeCursor.substring(lastNewline + 1);
110+
111+
// Match different list patterns
112+
const unorderedMatch = currentLine.match(/^(\s*)([-*])\s/);
113+
const orderedMatch = currentLine.match(/^(\s*)(\d+)\.\s/);
114+
const taskMatch = currentLine.match(/^(\s*)([-*])\s\[[ x]\]\s/);
115+
116+
let prefix = "";
117+
let isEmpty = false;
118+
119+
if (taskMatch) {
120+
const indent = taskMatch[1];
121+
const marker = taskMatch[2];
122+
// Check if the line only has the list marker with no content
123+
isEmpty = currentLine.trim() === `${marker} [ ]` || currentLine.trim() === `${marker} [x]`;
124+
prefix = `${indent}${marker} [ ] `;
125+
} else if (orderedMatch) {
126+
const indent = orderedMatch[1];
127+
const num = parseInt(orderedMatch[2], 10);
128+
// Check if the line only has the list marker
129+
isEmpty = currentLine.trim() === `${num}.`;
130+
prefix = `${indent}${num + 1}. `;
131+
} else if (unorderedMatch) {
132+
const indent = unorderedMatch[1];
133+
const marker = unorderedMatch[2];
134+
// Check if the line only has the list marker
135+
isEmpty = currentLine.trim() === marker;
136+
prefix = `${indent}${marker} `;
137+
}
138+
139+
if (prefix) {
140+
e.preventDefault();
141+
142+
if (isEmpty) {
143+
// If just the list marker, remove it and exit list
144+
const newValue = currentValue.substring(0, lastNewline + 1) + currentValue.substring(selectionStart);
145+
onChange(newValue);
146+
// Set cursor position after React updates
147+
requestAnimationFrame(() => {
148+
if (textarea) {
149+
textarea.selectionStart = textarea.selectionEnd = lastNewline + 1;
150+
textarea.focus();
151+
}
152+
});
153+
} else {
154+
// Continue the list on the next line
155+
const afterCursor = currentValue.substring(selectionStart);
156+
const newValue = beforeCursor + "\n" + prefix + afterCursor;
157+
onChange(newValue);
158+
// Set cursor position after the prefix
159+
const newCursorPos = selectionStart + 1 + prefix.length;
160+
requestAnimationFrame(() => {
161+
if (textarea) {
162+
textarea.selectionStart = textarea.selectionEnd = newCursorPos;
163+
textarea.focus();
164+
}
165+
});
166+
}
167+
}
168+
};
169+
97170
return (
98171
<Box
99172
borderWidth={1}
@@ -248,6 +321,7 @@ export function MarkdownEditor({
248321
id={textareaId}
249322
defaultValue={value}
250323
onChange={(e) => onChange(e.target.value)}
324+
onKeyDown={handleKeyDown}
251325
placeholder={placeholder}
252326
style={{
253327
width: "100%",
@@ -310,8 +384,11 @@ export function MarkdownEditor({
310384
borderLeft: "4px solid",
311385
borderColor: "border.default",
312386
pl: 3,
387+
ml: 0,
388+
mr: 0,
313389
my: 2,
314390
color: "fg.muted",
391+
bg: "canvas.subtle",
315392
},
316393
"& a": {
317394
color: "accent.fg",

0 commit comments

Comments
 (0)