@ripla/godd-mcp 1.0.4-canary.21 → 1.0.4-canary.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
-
import { render, screen, waitFor } from "@testing-library/react";
|
|
2
|
+
import { render, screen, waitFor, fireEvent } from "@testing-library/react";
|
|
3
3
|
|
|
4
4
|
vi.mock("@/lib/api", () => ({
|
|
5
5
|
listIssuesApi: vi.fn(),
|
|
6
|
+
listIssueLabelsApi: vi.fn(),
|
|
6
7
|
getErrorMessage: vi.fn((e: unknown) => String(e)),
|
|
7
8
|
isAbortError: vi.fn(() => false),
|
|
8
9
|
}));
|
|
9
10
|
|
|
10
|
-
import { listIssuesApi } from "@/lib/api";
|
|
11
|
+
import { listIssuesApi, listIssueLabelsApi } from "@/lib/api";
|
|
11
12
|
import IssueList from "./IssueList";
|
|
12
13
|
|
|
13
14
|
const mockListIssues = vi.mocked(listIssuesApi);
|
|
15
|
+
const mockListLabels = vi.mocked(listIssueLabelsApi);
|
|
14
16
|
|
|
15
17
|
const MOCK_ISSUES = [
|
|
16
18
|
{
|
|
@@ -40,6 +42,9 @@ const MOCK_ISSUES = [
|
|
|
40
42
|
describe("IssueList", () => {
|
|
41
43
|
beforeEach(() => {
|
|
42
44
|
vi.clearAllMocks();
|
|
45
|
+
mockListLabels.mockResolvedValue({
|
|
46
|
+
labels: [{ name: "bug", color: "d73a4a" }],
|
|
47
|
+
});
|
|
43
48
|
});
|
|
44
49
|
|
|
45
50
|
it("Issueリストを取得して表示する", async () => {
|
|
@@ -90,4 +95,24 @@ describe("IssueList", () => {
|
|
|
90
95
|
|
|
91
96
|
expect(mockListIssues).toHaveBeenCalledTimes(1);
|
|
92
97
|
});
|
|
98
|
+
|
|
99
|
+
it("ラベルフィルタ変更時に labels クエリ付きで listIssuesApi を呼ぶ", async () => {
|
|
100
|
+
mockListIssues.mockResolvedValue({ issues: MOCK_ISSUES });
|
|
101
|
+
|
|
102
|
+
render(<IssueList onSelect={vi.fn()} selectedIssue={null} />);
|
|
103
|
+
|
|
104
|
+
await waitFor(() => {
|
|
105
|
+
expect(screen.getByLabelText("ラベルでフィルタ")).toBeTruthy();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
fireEvent.change(screen.getByLabelText("ラベルでフィルタ"), {
|
|
109
|
+
target: { value: "bug" },
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await waitFor(() => {
|
|
113
|
+
expect(mockListIssues).toHaveBeenCalledWith(
|
|
114
|
+
expect.objectContaining({ labels: "bug" }),
|
|
115
|
+
);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
93
118
|
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
listIssuesApi,
|
|
4
|
+
listIssueLabelsApi,
|
|
4
5
|
getErrorMessage,
|
|
5
6
|
isAbortError,
|
|
6
7
|
} from "@/lib/api";
|
|
7
|
-
import type { IssueSummary } from "@/lib/api";
|
|
8
|
-
import { CircleDot, CircleCheck, MessageSquare, RefreshCw } from "lucide-react";
|
|
8
|
+
import type { IssueLabel, IssueSummary } from "@/lib/api";
|
|
9
|
+
import { CircleDot, CircleCheck, MessageSquare, RefreshCw, Tag } from "lucide-react";
|
|
9
10
|
import { InlineError } from "@/components/Skeleton";
|
|
10
11
|
|
|
11
12
|
type IssueListProps = {
|
|
@@ -19,9 +20,25 @@ export default function IssueList({ onSelect, selectedIssue, reloadTrigger }: Is
|
|
|
19
20
|
const [loading, setLoading] = useState(true);
|
|
20
21
|
const [error, setError] = useState<string | null>(null);
|
|
21
22
|
const [stateFilter, setStateFilter] = useState<"open" | "closed">("open");
|
|
23
|
+
const [labelFilter, setLabelFilter] = useState<string>("");
|
|
24
|
+
const [labels, setLabels] = useState<IssueLabel[]>([]);
|
|
22
25
|
const [reloadKey, setReloadKey] = useState(0);
|
|
23
26
|
const requestSeq = useRef(0);
|
|
24
27
|
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const ac = new AbortController();
|
|
30
|
+
listIssueLabelsApi()
|
|
31
|
+
.then((data) => {
|
|
32
|
+
if (ac.signal.aborted) return;
|
|
33
|
+
setLabels([...data.labels].sort((a, b) => a.name.localeCompare(b.name)));
|
|
34
|
+
})
|
|
35
|
+
.catch(() => {
|
|
36
|
+
if (ac.signal.aborted) return;
|
|
37
|
+
setLabels([]);
|
|
38
|
+
});
|
|
39
|
+
return () => ac.abort();
|
|
40
|
+
}, [reloadTrigger]);
|
|
41
|
+
|
|
25
42
|
const requestReload = () => {
|
|
26
43
|
requestSeq.current += 1;
|
|
27
44
|
setLoading(true);
|
|
@@ -37,11 +54,24 @@ export default function IssueList({ onSelect, selectedIssue, reloadTrigger }: Is
|
|
|
37
54
|
setStateFilter(nextState);
|
|
38
55
|
};
|
|
39
56
|
|
|
57
|
+
const changeLabelFilter = (nextLabel: string) => {
|
|
58
|
+
if (nextLabel === labelFilter) return;
|
|
59
|
+
requestSeq.current += 1;
|
|
60
|
+
setLoading(true);
|
|
61
|
+
setError(null);
|
|
62
|
+
setLabelFilter(nextLabel);
|
|
63
|
+
};
|
|
64
|
+
|
|
40
65
|
useEffect(() => {
|
|
41
66
|
const ac = new AbortController();
|
|
42
67
|
const requestId = requestSeq.current + 1;
|
|
43
68
|
requestSeq.current = requestId;
|
|
44
|
-
listIssuesApi({
|
|
69
|
+
listIssuesApi({
|
|
70
|
+
state: stateFilter,
|
|
71
|
+
labels: labelFilter || undefined,
|
|
72
|
+
per_page: 50,
|
|
73
|
+
signal: ac.signal,
|
|
74
|
+
})
|
|
45
75
|
.then((data) => {
|
|
46
76
|
if (ac.signal.aborted || requestSeq.current !== requestId) return;
|
|
47
77
|
setIssues([...data.issues].sort((a, b) => b.number - a.number));
|
|
@@ -56,7 +86,7 @@ export default function IssueList({ onSelect, selectedIssue, reloadTrigger }: Is
|
|
|
56
86
|
}
|
|
57
87
|
});
|
|
58
88
|
return () => ac.abort();
|
|
59
|
-
}, [stateFilter, reloadKey, reloadTrigger]);
|
|
89
|
+
}, [stateFilter, labelFilter, reloadKey, reloadTrigger]);
|
|
60
90
|
|
|
61
91
|
return (
|
|
62
92
|
<div className="flex flex-col h-full">
|
|
@@ -98,6 +128,28 @@ export default function IssueList({ onSelect, selectedIssue, reloadTrigger }: Is
|
|
|
98
128
|
</button>
|
|
99
129
|
</div>
|
|
100
130
|
|
|
131
|
+
{labels.length > 0 && (
|
|
132
|
+
<div className="px-2 pt-2">
|
|
133
|
+
<label className="flex items-center gap-1.5 text-[10px] text-gray-500 mb-1">
|
|
134
|
+
<Tag className="w-3 h-3" />
|
|
135
|
+
ラベル
|
|
136
|
+
</label>
|
|
137
|
+
<select
|
|
138
|
+
value={labelFilter}
|
|
139
|
+
onChange={(e) => changeLabelFilter(e.target.value)}
|
|
140
|
+
className="w-full px-2 py-1 rounded text-xs bg-gray-900 border border-gray-600 text-gray-200 outline-none focus:border-blue-500"
|
|
141
|
+
aria-label="ラベルでフィルタ"
|
|
142
|
+
>
|
|
143
|
+
<option value="">すべて</option>
|
|
144
|
+
{labels.map((label) => (
|
|
145
|
+
<option key={label.name} value={label.name}>
|
|
146
|
+
{label.name}
|
|
147
|
+
</option>
|
|
148
|
+
))}
|
|
149
|
+
</select>
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
101
153
|
{/* Issue list */}
|
|
102
154
|
<div className="flex-1 overflow-y-auto p-2">
|
|
103
155
|
{loading && issues.length === 0 && (
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
buildMainPageSearchParams,
|
|
4
|
+
parseIssueSearchParam,
|
|
5
|
+
issueSelectionToSearchParam,
|
|
6
|
+
searchParamsToString,
|
|
7
|
+
} from "./issue-url-sync";
|
|
8
|
+
|
|
9
|
+
describe("parseIssueSearchParam", () => {
|
|
10
|
+
it("parses positive issue numbers", () => {
|
|
11
|
+
expect(parseIssueSearchParam("794")).toBe(794);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("parses new issue sentinel", () => {
|
|
15
|
+
expect(parseIssueSearchParam("new")).toBe("new");
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns null for invalid values", () => {
|
|
19
|
+
expect(parseIssueSearchParam(null)).toBeNull();
|
|
20
|
+
expect(parseIssueSearchParam("")).toBeNull();
|
|
21
|
+
expect(parseIssueSearchParam("0")).toBeNull();
|
|
22
|
+
expect(parseIssueSearchParam("-1")).toBeNull();
|
|
23
|
+
expect(parseIssueSearchParam("abc")).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("issueSelectionToSearchParam", () => {
|
|
28
|
+
it("serializes issue selections", () => {
|
|
29
|
+
expect(issueSelectionToSearchParam(123)).toBe("123");
|
|
30
|
+
expect(issueSelectionToSearchParam("new")).toBe("new");
|
|
31
|
+
expect(issueSelectionToSearchParam(null)).toBeNull();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("buildMainPageSearchParams", () => {
|
|
36
|
+
it("builds issue-only deep link", () => {
|
|
37
|
+
const params = buildMainPageSearchParams({ issue: 794 });
|
|
38
|
+
expect(searchParamsToString(params)).toBe("?issue=794");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("builds new-issue deep link", () => {
|
|
42
|
+
const params = buildMainPageSearchParams({ issue: "new" });
|
|
43
|
+
expect(searchParamsToString(params)).toBe("?issue=new");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("prefers file over issue when both provided", () => {
|
|
47
|
+
const params = buildMainPageSearchParams({
|
|
48
|
+
file: "docs/README.md",
|
|
49
|
+
issue: 794,
|
|
50
|
+
});
|
|
51
|
+
expect(params.get("file")).toBe("docs/README.md");
|
|
52
|
+
expect(params.get("issue")).toBe("794");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("preserves unrelated query params", () => {
|
|
56
|
+
const preserve = new URLSearchParams("x=1&file=old&issue=1");
|
|
57
|
+
const params = buildMainPageSearchParams({
|
|
58
|
+
issue: 42,
|
|
59
|
+
preserve,
|
|
60
|
+
});
|
|
61
|
+
expect(params.get("x")).toBe("1");
|
|
62
|
+
expect(params.get("issue")).toBe("42");
|
|
63
|
+
expect(params.has("file")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("clears issue and file when neither is set", () => {
|
|
67
|
+
const preserve = new URLSearchParams("file=a&issue=1&x=1");
|
|
68
|
+
const params = buildMainPageSearchParams({ preserve });
|
|
69
|
+
expect(params.get("x")).toBe("1");
|
|
70
|
+
expect(params.has("file")).toBe(false);
|
|
71
|
+
expect(params.has("issue")).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type IssueUrlSelection = number | "new" | null;
|
|
2
|
+
|
|
3
|
+
export function parseIssueSearchParam(value: string | null): IssueUrlSelection {
|
|
4
|
+
if (!value) return null;
|
|
5
|
+
if (value === "new") return "new";
|
|
6
|
+
const parsed = Number(value);
|
|
7
|
+
if (Number.isInteger(parsed) && parsed > 0) return parsed;
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function issueSelectionToSearchParam(selection: IssueUrlSelection): string | null {
|
|
12
|
+
if (selection === null) return null;
|
|
13
|
+
if (selection === "new") return "new";
|
|
14
|
+
return String(selection);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildMainPageSearchParams(options: {
|
|
18
|
+
file?: string | null;
|
|
19
|
+
issue?: IssueUrlSelection;
|
|
20
|
+
preserve?: URLSearchParams;
|
|
21
|
+
}): URLSearchParams {
|
|
22
|
+
const params = new URLSearchParams();
|
|
23
|
+
if (options.preserve) {
|
|
24
|
+
for (const [key, value] of options.preserve) {
|
|
25
|
+
if (key !== "file" && key !== "issue") {
|
|
26
|
+
params.append(key, value);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (options.file) {
|
|
31
|
+
params.set("file", options.file);
|
|
32
|
+
}
|
|
33
|
+
const issueValue = issueSelectionToSearchParam(options.issue ?? null);
|
|
34
|
+
if (issueValue) {
|
|
35
|
+
params.set("issue", issueValue);
|
|
36
|
+
}
|
|
37
|
+
return params;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function searchParamsToString(params: URLSearchParams): string {
|
|
41
|
+
const qs = params.toString();
|
|
42
|
+
return qs ? `?${qs}` : "";
|
|
43
|
+
}
|
|
@@ -42,6 +42,11 @@ import { insertChildEntry, moveEntry, removeEntry } from "@/lib/tree-utils";
|
|
|
42
42
|
import { createdFileToContent, createdFileToTreeEntry } from "@/lib/created-file";
|
|
43
43
|
import { DEFAULT_BASE_REF, hasContentDiff, resolveBranchLabels, resolveOriginalContent } from "@/lib/branch-diff";
|
|
44
44
|
import { clearDraft, loadDraft, saveDraft } from "@/lib/draft-storage";
|
|
45
|
+
import {
|
|
46
|
+
buildMainPageSearchParams,
|
|
47
|
+
parseIssueSearchParam,
|
|
48
|
+
type IssueUrlSelection,
|
|
49
|
+
} from "@/lib/issue-url-sync";
|
|
45
50
|
|
|
46
51
|
const MarkdownEditor = lazy(() => import("@/components/MarkdownEditor"));
|
|
47
52
|
const SpreadsheetEditor = lazy(() => import("@/components/SpreadsheetEditor"));
|
|
@@ -74,6 +79,13 @@ export default function MainPage() {
|
|
|
74
79
|
const [selectedFile, setSelectedFile] = useState<string | null>(
|
|
75
80
|
() => searchParams.get("file"),
|
|
76
81
|
);
|
|
82
|
+
const [sidebarTab, setSidebarTab] = useState<SidebarTab>(() => {
|
|
83
|
+
if (searchParams.get("file")) return "files";
|
|
84
|
+
return parseIssueSearchParam(searchParams.get("issue")) !== null ? "issues" : "files";
|
|
85
|
+
});
|
|
86
|
+
const [selectedIssue, setSelectedIssue] = useState<number | "new" | null>(() =>
|
|
87
|
+
searchParams.get("file") ? null : parseIssueSearchParam(searchParams.get("issue")),
|
|
88
|
+
);
|
|
77
89
|
const [fileContent, setFileContent] = useState<FileContent | null>(null);
|
|
78
90
|
const [baseFileContent, setBaseFileContent] = useState<FileContent | null>(null);
|
|
79
91
|
const [baseFileError, setBaseFileError] = useState<string | null>(null);
|
|
@@ -190,7 +202,17 @@ export default function MainPage() {
|
|
|
190
202
|
});
|
|
191
203
|
}, [showToast]);
|
|
192
204
|
|
|
193
|
-
const
|
|
205
|
+
const initialDeepLinkLoaded = useRef(false);
|
|
206
|
+
|
|
207
|
+
const syncIssueToUrl = useCallback(
|
|
208
|
+
(issue: IssueUrlSelection) => {
|
|
209
|
+
setSearchParams(
|
|
210
|
+
buildMainPageSearchParams({ issue, preserve: searchParams }),
|
|
211
|
+
{ replace: true },
|
|
212
|
+
);
|
|
213
|
+
},
|
|
214
|
+
[searchParams, setSearchParams],
|
|
215
|
+
);
|
|
194
216
|
|
|
195
217
|
useEffect(() => {
|
|
196
218
|
loadTree(viewingRef ?? undefined);
|
|
@@ -297,10 +319,14 @@ export default function MainPage() {
|
|
|
297
319
|
autoSave.reset();
|
|
298
320
|
pendingStylesRef.current = null;
|
|
299
321
|
setSelectedFile(path);
|
|
322
|
+
setSelectedIssue(null);
|
|
300
323
|
setBaseFileContent(null);
|
|
301
324
|
setBaseFileError(null);
|
|
302
325
|
setActiveDir(path.substring(0, path.lastIndexOf("/")) || "docs");
|
|
303
|
-
setSearchParams(
|
|
326
|
+
setSearchParams(
|
|
327
|
+
buildMainPageSearchParams({ file: path, preserve: searchParams }),
|
|
328
|
+
{ replace: true },
|
|
329
|
+
);
|
|
304
330
|
setFileLoading(true);
|
|
305
331
|
setEditing(false);
|
|
306
332
|
setShowOriginal(false);
|
|
@@ -343,17 +369,45 @@ export default function MainPage() {
|
|
|
343
369
|
.finally(() => {
|
|
344
370
|
if (!controller.signal.aborted) setFileLoading(false);
|
|
345
371
|
});
|
|
346
|
-
}, [autoSave, editSession.session?.branchName, showToast, setSearchParams, viewingRef]);
|
|
372
|
+
}, [autoSave, editSession.session?.branchName, showToast, setSearchParams, searchParams, viewingRef]);
|
|
347
373
|
|
|
348
374
|
useEffect(() => {
|
|
349
|
-
if (
|
|
375
|
+
if (initialDeepLinkLoaded.current) return;
|
|
350
376
|
if (apiState !== "ready" && apiState !== "mock") return;
|
|
377
|
+
|
|
351
378
|
const fileParam = searchParams.get("file");
|
|
379
|
+
const issueParam = parseIssueSearchParam(searchParams.get("issue"));
|
|
380
|
+
|
|
352
381
|
if (fileParam && !fileContent) {
|
|
353
|
-
|
|
382
|
+
initialDeepLinkLoaded.current = true;
|
|
354
383
|
handleFileSelect(fileParam);
|
|
384
|
+
return;
|
|
355
385
|
}
|
|
356
|
-
|
|
386
|
+
|
|
387
|
+
if (issueParam !== null && selectedIssue === null && !selectedFile) {
|
|
388
|
+
initialDeepLinkLoaded.current = true;
|
|
389
|
+
setSidebarTab("issues");
|
|
390
|
+
setSelectedIssue(issueParam);
|
|
391
|
+
}
|
|
392
|
+
}, [apiState, searchParams, fileContent, selectedIssue, selectedFile, handleFileSelect]);
|
|
393
|
+
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
if (searchParams.get("file")) return;
|
|
396
|
+
|
|
397
|
+
const issueParam = parseIssueSearchParam(searchParams.get("issue"));
|
|
398
|
+
if (issueParam !== null) {
|
|
399
|
+
setSidebarTab("issues");
|
|
400
|
+
setSelectedIssue((current) => (current === issueParam ? current : issueParam));
|
|
401
|
+
setSelectedFile(null);
|
|
402
|
+
setFileContent(null);
|
|
403
|
+
setEditing(false);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (!searchParams.get("issue")) {
|
|
408
|
+
setSelectedIssue((current) => (current === null ? current : null));
|
|
409
|
+
}
|
|
410
|
+
}, [searchParams]);
|
|
357
411
|
|
|
358
412
|
const handleSave = useCallback(
|
|
359
413
|
async (newContent: string, styles?: FileStyles | null) => {
|
|
@@ -427,7 +481,10 @@ export default function MainPage() {
|
|
|
427
481
|
setBaseFileContent(createdFileToContent(created));
|
|
428
482
|
setBaseFileError(null);
|
|
429
483
|
setActiveDir(parentPath);
|
|
430
|
-
setSearchParams(
|
|
484
|
+
setSearchParams(
|
|
485
|
+
buildMainPageSearchParams({ file: created.path, preserve: searchParams }),
|
|
486
|
+
{ replace: true },
|
|
487
|
+
);
|
|
431
488
|
setFileLoading(false);
|
|
432
489
|
setEditing(false);
|
|
433
490
|
setShowOriginal(false);
|
|
@@ -441,7 +498,7 @@ export default function MainPage() {
|
|
|
441
498
|
showToast(`作成に失敗しました: ${getErrorMessage(e)}`, "error");
|
|
442
499
|
}
|
|
443
500
|
},
|
|
444
|
-
[dialog, showToast, autoSave, setSearchParams, loadTree],
|
|
501
|
+
[dialog, showToast, autoSave, setSearchParams, searchParams, loadTree],
|
|
445
502
|
);
|
|
446
503
|
|
|
447
504
|
const handleCreateFolder = useCallback(
|
|
@@ -620,8 +677,6 @@ export default function MainPage() {
|
|
|
620
677
|
const comments = useComments(selectedFile);
|
|
621
678
|
|
|
622
679
|
const [sidebarOpen, setSidebarOpen] = useState(false);
|
|
623
|
-
const [sidebarTab, setSidebarTab] = useState<SidebarTab>("files");
|
|
624
|
-
const [selectedIssue, setSelectedIssue] = useState<number | "new" | null>(null);
|
|
625
680
|
const [issueListReloadTrigger, setIssueListReloadTrigger] = useState(0);
|
|
626
681
|
const [newIssueKey, setNewIssueKey] = useState(0);
|
|
627
682
|
|
|
@@ -853,6 +908,7 @@ export default function MainPage() {
|
|
|
853
908
|
setNewIssueKey((k) => k + 1);
|
|
854
909
|
setSelectedFile(null);
|
|
855
910
|
setFileContent(null);
|
|
911
|
+
syncIssueToUrl("new");
|
|
856
912
|
setSidebarOpen(false);
|
|
857
913
|
}}
|
|
858
914
|
className="w-full px-2 py-1.5 rounded flex items-center justify-center gap-1.5 text-xs text-green-300 bg-green-900/20 border border-green-700/40 hover:bg-green-900/40 transition-colors"
|
|
@@ -870,6 +926,7 @@ export default function MainPage() {
|
|
|
870
926
|
setSelectedFile(null);
|
|
871
927
|
setFileContent(null);
|
|
872
928
|
setEditing(false);
|
|
929
|
+
syncIssueToUrl(num);
|
|
873
930
|
setSidebarOpen(false);
|
|
874
931
|
}}
|
|
875
932
|
/>
|
|
@@ -1019,9 +1076,13 @@ export default function MainPage() {
|
|
|
1019
1076
|
issueNumber={selectedIssue}
|
|
1020
1077
|
onCreated={(num) => {
|
|
1021
1078
|
setSelectedIssue(num);
|
|
1079
|
+
syncIssueToUrl(num);
|
|
1022
1080
|
setIssueListReloadTrigger((k) => k + 1);
|
|
1023
1081
|
}}
|
|
1024
|
-
onClose={() =>
|
|
1082
|
+
onClose={() => {
|
|
1083
|
+
setSelectedIssue(null);
|
|
1084
|
+
syncIssueToUrl(null);
|
|
1085
|
+
}}
|
|
1025
1086
|
/>
|
|
1026
1087
|
</Suspense>
|
|
1027
1088
|
)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ripla/godd-mcp",
|
|
3
|
-
"version": "1.0.4-canary.
|
|
3
|
+
"version": "1.0.4-canary.23",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "GoDD MCP Server - AI-powered development workflow tools via Model Context Protocol (slash commands support)",
|
|
6
6
|
"main": "dist/index.js",
|