@hienlh/ppm 0.9.86 → 0.9.88
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.
- package/260415-0932-git-graph-stash-rebase-conflicts/reports/code-reviewer-260415-1020-stash-rebase-conflicts.md +288 -0
- package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md +117 -0
- package/260415-1150-ext-silent-failure-debugging/reports/code-reviewer-260415-1159-ext-error-reporting-review.md +205 -0
- package/260415-1150-ext-silent-failure-debugging/reports/docs-manager-260415-1206-ext-error-reporting.md +99 -0
- package/260415-1150-ext-silent-failure-debugging/reports/tester-260415-1159-extension-error-reporting.md +174 -0
- package/CHANGELOG.md +19 -0
- package/dist/web/assets/{chat-tab-BEEd-Km4.js → chat-tab-R4gKsnxD.js} +1 -1
- package/dist/web/assets/{code-editor-Ij4p30cr.js → code-editor-Br0vzTOy.js} +2 -2
- package/dist/web/assets/conflict-editor-BPgCjnNz.js +19 -0
- package/dist/web/assets/{csv-preview-CwQnOa3E.js → csv-preview-BZRICDP0.js} +1 -1
- package/dist/web/assets/{database-viewer-C1UHSgft.js → database-viewer-DaUoQ-oR.js} +1 -1
- package/dist/web/assets/{diff-viewer-CVx5naBA.js → diff-viewer-BzvK3gAE.js} +1 -1
- package/dist/web/assets/extension-webview-CGepEw-b.js +3 -0
- package/dist/web/assets/{index-OqgGFmh8.js → index-CKsEzQ4f.js} +4 -4
- package/dist/web/assets/index-Chf0otez.css +2 -0
- package/dist/web/assets/keybindings-store-D5zgHod8.js +1 -0
- package/dist/web/assets/{markdown-renderer-CRy8xw2B.js → markdown-renderer-DSYnGywb.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-Biua8ov5.js → port-forwarding-tab-vmqDKmk2.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BcVjCAl4.js → postgres-viewer-0lIAosrr.js} +1 -1
- package/dist/web/assets/{settings-tab-C9X-N8hE.js → settings-tab-CMnv1fce.js} +1 -1
- package/dist/web/assets/{sql-query-editor-BFvRvJn0.js → sql-query-editor-Bc2hAwqT.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-CPfvwFl4.js → sqlite-viewer-B60MS2Dy.js} +1 -1
- package/dist/web/assets/{terminal-tab-mWwk_weB.js → terminal-tab-CCJoLstH.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-CPaeSMAA.js → use-monaco-theme-BJK48EmK.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/docs/codebase-summary.md +39 -6
- package/docs/project-changelog.md +86 -25
- package/docs/project-roadmap.md +3 -2
- package/docs/system-architecture.md +44 -1
- package/package.json +1 -1
- package/packages/ext-git-graph/src/extension.ts +126 -5
- package/packages/ext-git-graph/src/types.ts +13 -2
- package/packages/ext-git-graph/src/webview-html.ts +249 -31
- package/src/server/ws/extensions.ts +28 -2
- package/src/services/extension-host-worker.ts +6 -1
- package/src/services/extension.service.ts +17 -3
- package/src/types/extension-messages.ts +1 -1
- package/src/web/components/editor/conflict-editor.tsx +368 -0
- package/src/web/components/extensions/extension-webview.tsx +45 -3
- package/src/web/components/layout/editor-panel.tsx +1 -0
- package/src/web/components/layout/mobile-nav.tsx +1 -0
- package/src/web/components/layout/tab-bar.tsx +1 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- package/src/web/hooks/use-extension-ws.ts +8 -0
- package/src/web/stores/extension-store.ts +8 -0
- package/src/web/stores/panel-utils.ts +2 -0
- package/src/web/stores/tab-store.ts +2 -1
- package/dist/web/assets/extension-webview-CHVVpV34.js +0 -3
- package/dist/web/assets/index-vA7juDri.css +0 -2
- package/dist/web/assets/keybindings-store-BQxgPV5o.js +0 -1
- /package/dist/web/assets/{lib-CeBVkQ-7.js → lib-DSLzfeW0.js} +0 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import { useEffect, useState, useRef, useCallback } from "react";
|
|
2
|
+
import Editor, { type OnMount } from "@monaco-editor/react";
|
|
3
|
+
import type * as MonacoType from "monaco-editor";
|
|
4
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
5
|
+
import { useSettingsStore } from "@/stores/settings-store";
|
|
6
|
+
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
7
|
+
import { Loader2 } from "lucide-react";
|
|
8
|
+
|
|
9
|
+
function getMonacoLanguage(filename: string): string {
|
|
10
|
+
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
11
|
+
const map: Record<string, string> = {
|
|
12
|
+
js: "javascript", jsx: "javascript",
|
|
13
|
+
ts: "typescript", tsx: "typescript",
|
|
14
|
+
py: "python", html: "html",
|
|
15
|
+
css: "css", scss: "scss",
|
|
16
|
+
json: "json", md: "markdown", mdx: "markdown",
|
|
17
|
+
yaml: "yaml", yml: "yaml",
|
|
18
|
+
sh: "shell", bash: "shell",
|
|
19
|
+
go: "go", rs: "rust", java: "java",
|
|
20
|
+
rb: "ruby", php: "php", swift: "swift",
|
|
21
|
+
sql: "sql", xml: "xml", toml: "toml",
|
|
22
|
+
};
|
|
23
|
+
return map[ext] ?? "plaintext";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ConflictRegion {
|
|
27
|
+
id: number;
|
|
28
|
+
startLine: number; // 1-indexed, line of <<<<<<< marker
|
|
29
|
+
separatorLine: number; // line of =======
|
|
30
|
+
endLine: number; // line of >>>>>>> marker
|
|
31
|
+
currentContent: string;
|
|
32
|
+
incomingContent: string;
|
|
33
|
+
currentLabel: string;
|
|
34
|
+
incomingLabel: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseConflicts(content: string): ConflictRegion[] {
|
|
38
|
+
const lines = content.split("\n");
|
|
39
|
+
const regions: ConflictRegion[] = [];
|
|
40
|
+
let i = 0;
|
|
41
|
+
let id = 0;
|
|
42
|
+
|
|
43
|
+
while (i < lines.length) {
|
|
44
|
+
const line = lines[i]!;
|
|
45
|
+
if (line.startsWith("<<<<<<<")) {
|
|
46
|
+
const startLine = i;
|
|
47
|
+
const currentLabel = line.substring(7).trim();
|
|
48
|
+
const currentLines: string[] = [];
|
|
49
|
+
i++;
|
|
50
|
+
|
|
51
|
+
while (i < lines.length && !lines[i]!.startsWith("=======")) {
|
|
52
|
+
currentLines.push(lines[i]!);
|
|
53
|
+
i++;
|
|
54
|
+
}
|
|
55
|
+
if (i >= lines.length) break;
|
|
56
|
+
|
|
57
|
+
const separatorLine = i;
|
|
58
|
+
const incomingLines: string[] = [];
|
|
59
|
+
i++;
|
|
60
|
+
|
|
61
|
+
while (i < lines.length && !lines[i]!.startsWith(">>>>>>>")) {
|
|
62
|
+
incomingLines.push(lines[i]!);
|
|
63
|
+
i++;
|
|
64
|
+
}
|
|
65
|
+
if (i >= lines.length) break;
|
|
66
|
+
|
|
67
|
+
const incomingLabel = lines[i]!.substring(7).trim();
|
|
68
|
+
|
|
69
|
+
regions.push({
|
|
70
|
+
id: id++,
|
|
71
|
+
startLine: startLine + 1,
|
|
72
|
+
separatorLine: separatorLine + 1,
|
|
73
|
+
endLine: i + 1,
|
|
74
|
+
currentContent: currentLines.join("\n"),
|
|
75
|
+
incomingContent: incomingLines.join("\n"),
|
|
76
|
+
currentLabel,
|
|
77
|
+
incomingLabel,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
return regions;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface ConflictEditorProps {
|
|
86
|
+
metadata?: Record<string, unknown>;
|
|
87
|
+
tabId?: string;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function ConflictEditor({ metadata }: ConflictEditorProps) {
|
|
91
|
+
const filePath = metadata?.filePath as string | undefined;
|
|
92
|
+
const projectName = metadata?.projectName as string | undefined;
|
|
93
|
+
|
|
94
|
+
const [content, setContent] = useState<string | null>(null);
|
|
95
|
+
const [loading, setLoading] = useState(true);
|
|
96
|
+
const [error, setError] = useState<string | null>(null);
|
|
97
|
+
const [conflictCount, setConflictCount] = useState(0);
|
|
98
|
+
const editorRef = useRef<MonacoType.editor.IStandaloneCodeEditor | null>(null);
|
|
99
|
+
const monacoRef = useRef<typeof MonacoType | null>(null);
|
|
100
|
+
const widgetsRef = useRef<MonacoType.editor.IContentWidget[]>([]);
|
|
101
|
+
const decorationsRef = useRef<MonacoType.editor.IEditorDecorationsCollection | null>(null);
|
|
102
|
+
|
|
103
|
+
const { wordWrap } = useSettingsStore();
|
|
104
|
+
const monacoTheme = useMonacoTheme();
|
|
105
|
+
|
|
106
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
107
|
+
const [containerHeight, setContainerHeight] = useState<number | undefined>();
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const el = containerRef.current;
|
|
111
|
+
if (!el) return;
|
|
112
|
+
const ro = new ResizeObserver(([entry]) => {
|
|
113
|
+
if (entry) setContainerHeight(Math.floor(entry.contentRect.height));
|
|
114
|
+
});
|
|
115
|
+
ro.observe(el);
|
|
116
|
+
return () => ro.disconnect();
|
|
117
|
+
}, []);
|
|
118
|
+
|
|
119
|
+
// Load file content
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
if (!filePath || !projectName) return;
|
|
122
|
+
setLoading(true);
|
|
123
|
+
api
|
|
124
|
+
.get<{ content: string }>(`${projectUrl(projectName)}/files/read?path=${encodeURIComponent(filePath)}`)
|
|
125
|
+
.then((data) => {
|
|
126
|
+
setContent(data.content);
|
|
127
|
+
setLoading(false);
|
|
128
|
+
})
|
|
129
|
+
.catch((e: Error) => {
|
|
130
|
+
setError(e.message || "Failed to load file");
|
|
131
|
+
setLoading(false);
|
|
132
|
+
});
|
|
133
|
+
}, [filePath, projectName]);
|
|
134
|
+
|
|
135
|
+
const refreshConflicts = useCallback(() => {
|
|
136
|
+
const editor = editorRef.current;
|
|
137
|
+
const monaco = monacoRef.current;
|
|
138
|
+
if (!editor || !monaco) return;
|
|
139
|
+
|
|
140
|
+
const value = editor.getModel()?.getValue() || "";
|
|
141
|
+
const regions = parseConflicts(value);
|
|
142
|
+
setConflictCount(regions.length);
|
|
143
|
+
|
|
144
|
+
// Clear old widgets
|
|
145
|
+
for (const w of widgetsRef.current) {
|
|
146
|
+
editor.removeContentWidget(w);
|
|
147
|
+
}
|
|
148
|
+
widgetsRef.current = [];
|
|
149
|
+
|
|
150
|
+
// Clear old decorations
|
|
151
|
+
if (decorationsRef.current) {
|
|
152
|
+
decorationsRef.current.clear();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (regions.length === 0) return;
|
|
156
|
+
|
|
157
|
+
// Apply decorations
|
|
158
|
+
const decos: MonacoType.editor.IModelDeltaDecoration[] = [];
|
|
159
|
+
for (const region of regions) {
|
|
160
|
+
// Marker lines
|
|
161
|
+
decos.push({
|
|
162
|
+
range: new monaco.Range(region.startLine, 1, region.startLine, 1),
|
|
163
|
+
options: { isWholeLine: true, className: "conflict-marker-line", glyphMarginClassName: "conflict-glyph-current" },
|
|
164
|
+
});
|
|
165
|
+
decos.push({
|
|
166
|
+
range: new monaco.Range(region.separatorLine, 1, region.separatorLine, 1),
|
|
167
|
+
options: { isWholeLine: true, className: "conflict-marker-line" },
|
|
168
|
+
});
|
|
169
|
+
decos.push({
|
|
170
|
+
range: new monaco.Range(region.endLine, 1, region.endLine, 1),
|
|
171
|
+
options: { isWholeLine: true, className: "conflict-marker-line", glyphMarginClassName: "conflict-glyph-incoming" },
|
|
172
|
+
});
|
|
173
|
+
// Current content (green)
|
|
174
|
+
if (region.separatorLine - region.startLine > 1) {
|
|
175
|
+
decos.push({
|
|
176
|
+
range: new monaco.Range(region.startLine + 1, 1, region.separatorLine - 1, 1),
|
|
177
|
+
options: { isWholeLine: true, className: "conflict-current-content" },
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// Incoming content (blue)
|
|
181
|
+
if (region.endLine - region.separatorLine > 1) {
|
|
182
|
+
decos.push({
|
|
183
|
+
range: new monaco.Range(region.separatorLine + 1, 1, region.endLine - 1, 1),
|
|
184
|
+
options: { isWholeLine: true, className: "conflict-incoming-content" },
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
decorationsRef.current = editor.createDecorationsCollection(decos);
|
|
190
|
+
|
|
191
|
+
// Add accept widgets above each conflict
|
|
192
|
+
for (const region of regions) {
|
|
193
|
+
const widgetId = `conflict-widget-${region.id}`;
|
|
194
|
+
|
|
195
|
+
const domNode = document.createElement("div");
|
|
196
|
+
domNode.className = "conflict-actions";
|
|
197
|
+
domNode.innerHTML =
|
|
198
|
+
`<span class="conflict-label">Current Change (${escHtml(region.currentLabel || "HEAD")})</span>` +
|
|
199
|
+
`<button class="conflict-btn conflict-btn-current" data-action="current">Accept Current</button>` +
|
|
200
|
+
`<button class="conflict-btn conflict-btn-incoming" data-action="incoming">Accept Incoming</button>` +
|
|
201
|
+
`<button class="conflict-btn conflict-btn-both" data-action="both">Accept Both</button>`;
|
|
202
|
+
|
|
203
|
+
domNode.addEventListener("click", (e) => {
|
|
204
|
+
const btn = (e.target as HTMLElement).closest("[data-action]");
|
|
205
|
+
if (!btn) return;
|
|
206
|
+
const action = btn.getAttribute("data-action") as "current" | "incoming" | "both";
|
|
207
|
+
acceptConflict(region.id, action);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const widget: MonacoType.editor.IContentWidget = {
|
|
211
|
+
getId: () => widgetId,
|
|
212
|
+
getDomNode: () => domNode,
|
|
213
|
+
getPosition: () => ({
|
|
214
|
+
position: { lineNumber: region.startLine, column: 1 },
|
|
215
|
+
preference: [monaco.editor.ContentWidgetPositionPreference.ABOVE],
|
|
216
|
+
}),
|
|
217
|
+
};
|
|
218
|
+
editor.addContentWidget(widget);
|
|
219
|
+
widgetsRef.current.push(widget);
|
|
220
|
+
}
|
|
221
|
+
}, []);
|
|
222
|
+
|
|
223
|
+
const acceptConflict = useCallback(
|
|
224
|
+
(regionId: number, action: "current" | "incoming" | "both") => {
|
|
225
|
+
const editor = editorRef.current;
|
|
226
|
+
const monaco = monacoRef.current;
|
|
227
|
+
if (!editor || !monaco) return;
|
|
228
|
+
|
|
229
|
+
const model = editor.getModel();
|
|
230
|
+
if (!model) return;
|
|
231
|
+
|
|
232
|
+
const value = model.getValue();
|
|
233
|
+
const regions = parseConflicts(value);
|
|
234
|
+
const region = regions.find((r) => r.id === regionId);
|
|
235
|
+
if (!region) return;
|
|
236
|
+
|
|
237
|
+
let replacement: string;
|
|
238
|
+
switch (action) {
|
|
239
|
+
case "current":
|
|
240
|
+
replacement = region.currentContent;
|
|
241
|
+
break;
|
|
242
|
+
case "incoming":
|
|
243
|
+
replacement = region.incomingContent;
|
|
244
|
+
break;
|
|
245
|
+
case "both":
|
|
246
|
+
replacement = region.currentContent + "\n" + region.incomingContent;
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const range = new monaco.Range(region.startLine, 1, region.endLine + 1, 1);
|
|
251
|
+
model.pushEditOperations(
|
|
252
|
+
[],
|
|
253
|
+
[{ range, text: replacement + "\n" }],
|
|
254
|
+
() => null,
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Save and refresh
|
|
258
|
+
saveFile(model.getValue());
|
|
259
|
+
// Small delay to let the model update before refreshing decorations
|
|
260
|
+
setTimeout(() => refreshConflicts(), 50);
|
|
261
|
+
},
|
|
262
|
+
[refreshConflicts],
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const saveFile = useCallback(
|
|
266
|
+
async (newContent: string) => {
|
|
267
|
+
if (!filePath || !projectName) return;
|
|
268
|
+
try {
|
|
269
|
+
await api.put<{ written: boolean }>(`${projectUrl(projectName)}/files/write`, {
|
|
270
|
+
path: filePath,
|
|
271
|
+
content: newContent,
|
|
272
|
+
});
|
|
273
|
+
} catch (e) {
|
|
274
|
+
console.error("[conflict-editor] save failed:", e);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
[filePath, projectName],
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
const handleMount: OnMount = (editor, monaco) => {
|
|
281
|
+
editorRef.current = editor;
|
|
282
|
+
monacoRef.current = monaco;
|
|
283
|
+
|
|
284
|
+
// Inject conflict editor styles (idempotent)
|
|
285
|
+
const doc = editor.getDomNode()?.ownerDocument ?? document;
|
|
286
|
+
if (!doc.getElementById("conflict-editor-styles")) {
|
|
287
|
+
const styleEl = doc.createElement("style");
|
|
288
|
+
styleEl.id = "conflict-editor-styles";
|
|
289
|
+
styleEl.textContent = `
|
|
290
|
+
.conflict-current-content { background: rgba(34, 197, 94, 0.1) !important; }
|
|
291
|
+
.conflict-incoming-content { background: rgba(59, 130, 246, 0.1) !important; }
|
|
292
|
+
.conflict-marker-line { background: rgba(100, 100, 100, 0.15) !important; font-style: italic; }
|
|
293
|
+
.conflict-glyph-current { background: #22c55e !important; }
|
|
294
|
+
.conflict-glyph-incoming { background: #3b82f6 !important; }
|
|
295
|
+
.conflict-actions { display: flex; gap: 8px; align-items: center; padding: 2px 0; font-size: 12px; font-family: system-ui; }
|
|
296
|
+
.conflict-label { color: #22c55e; font-weight: 600; margin-right: 8px; }
|
|
297
|
+
.conflict-btn { padding: 1px 8px; border-radius: 3px; border: none; cursor: pointer; font-size: 11px; opacity: 0.9; }
|
|
298
|
+
.conflict-btn:hover { opacity: 1; }
|
|
299
|
+
.conflict-btn-current { color: #22c55e; background: rgba(34, 197, 94, 0.15); }
|
|
300
|
+
.conflict-btn-incoming { color: #3b82f6; background: rgba(59, 130, 246, 0.15); }
|
|
301
|
+
.conflict-btn-both { color: #a855f7; background: rgba(168, 85, 247, 0.15); }
|
|
302
|
+
`;
|
|
303
|
+
doc.head?.appendChild(styleEl);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
refreshConflicts();
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const fileName = filePath?.split(/[\\/]/).pop() ?? "unknown";
|
|
310
|
+
const language = getMonacoLanguage(fileName);
|
|
311
|
+
|
|
312
|
+
if (loading) {
|
|
313
|
+
return (
|
|
314
|
+
<div className="flex items-center justify-center h-full">
|
|
315
|
+
<Loader2 className="size-6 animate-spin text-primary" />
|
|
316
|
+
</div>
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (error) {
|
|
321
|
+
return (
|
|
322
|
+
<div className="flex items-center justify-center h-full text-destructive">
|
|
323
|
+
{error}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return (
|
|
329
|
+
<div className="h-full w-full flex flex-col">
|
|
330
|
+
<div className="flex items-center gap-2 px-3 py-1.5 text-xs border-b border-border bg-muted/50 flex-shrink-0">
|
|
331
|
+
<span className="font-medium">{fileName}</span>
|
|
332
|
+
<span className="text-muted-foreground">—</span>
|
|
333
|
+
{conflictCount > 0 ? (
|
|
334
|
+
<span className="text-destructive font-medium">
|
|
335
|
+
{conflictCount} conflict{conflictCount !== 1 ? "s" : ""} remaining
|
|
336
|
+
</span>
|
|
337
|
+
) : (
|
|
338
|
+
<span className="text-green-500 font-medium">All conflicts resolved</span>
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
<div ref={containerRef} className="flex-1 min-h-0">
|
|
342
|
+
{content !== null && containerHeight && (
|
|
343
|
+
<Editor
|
|
344
|
+
height={containerHeight}
|
|
345
|
+
language={language}
|
|
346
|
+
value={content}
|
|
347
|
+
onMount={handleMount}
|
|
348
|
+
theme={monacoTheme}
|
|
349
|
+
options={{
|
|
350
|
+
fontSize: 13,
|
|
351
|
+
fontFamily: "Menlo, Monaco, Consolas, monospace",
|
|
352
|
+
wordWrap: wordWrap ? "on" : "off",
|
|
353
|
+
glyphMargin: true,
|
|
354
|
+
readOnly: false,
|
|
355
|
+
automaticLayout: true,
|
|
356
|
+
scrollBeyondLastLine: false,
|
|
357
|
+
minimap: { enabled: false },
|
|
358
|
+
}}
|
|
359
|
+
/>
|
|
360
|
+
)}
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function escHtml(s: string): string {
|
|
367
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
368
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useEffect, useState } from "react";
|
|
1
|
+
import { useRef, useEffect, useState, useCallback } from "react";
|
|
2
2
|
import { useExtensionStore } from "@/stores/extension-store";
|
|
3
3
|
import { useTabStore } from "@/stores/tab-store";
|
|
4
4
|
import { Loader2 } from "lucide-react";
|
|
@@ -118,6 +118,37 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
|
|
|
118
118
|
})();
|
|
119
119
|
}, [panel, viewType, currentProject]);
|
|
120
120
|
|
|
121
|
+
// Check activation errors for this extension
|
|
122
|
+
const extensionId = metadata?.extensionId as string | undefined;
|
|
123
|
+
const activationError = useExtensionStore((s) => {
|
|
124
|
+
// Direct match by extensionId (most reliable)
|
|
125
|
+
if (extensionId && s.activationErrors[extensionId]) return s.activationErrors[extensionId];
|
|
126
|
+
// Fallback: check by viewType prefix (e.g. "ext-git-graph" for viewType "git-graph")
|
|
127
|
+
if (!viewType) return undefined;
|
|
128
|
+
for (const [extId, error] of Object.entries(s.activationErrors)) {
|
|
129
|
+
if (extId === `ext-${viewType}`) return error;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Retry handler — re-dispatches the command
|
|
135
|
+
const handleRetry = useCallback(() => {
|
|
136
|
+
setTimedOut(false);
|
|
137
|
+
if (!viewType) return;
|
|
138
|
+
const command = viewType.includes(".") ? viewType : `${viewType}.view`;
|
|
139
|
+
(async () => {
|
|
140
|
+
try {
|
|
141
|
+
const res = await fetch("/api/projects");
|
|
142
|
+
const json = await res.json() as { ok: boolean; data?: { name: string; path: string }[] };
|
|
143
|
+
const match = json.data?.find((p) => p.name === projectName);
|
|
144
|
+
const args = match ? [match.path] : [];
|
|
145
|
+
window.dispatchEvent(new CustomEvent("ext:command:execute", {
|
|
146
|
+
detail: { command, args },
|
|
147
|
+
}));
|
|
148
|
+
} catch {}
|
|
149
|
+
})();
|
|
150
|
+
}, [viewType, projectName]);
|
|
151
|
+
|
|
121
152
|
// Timeout: if panel doesn't appear within 10s, show error
|
|
122
153
|
useEffect(() => {
|
|
123
154
|
if (panel) { setTimedOut(false); return; }
|
|
@@ -155,9 +186,20 @@ export function ExtensionWebview({ metadata }: ExtensionWebviewProps) {
|
|
|
155
186
|
// Loading state — waiting for extension to create the panel
|
|
156
187
|
if (!panel) {
|
|
157
188
|
return (
|
|
158
|
-
<div className="flex flex-col items-center justify-center h-full gap-
|
|
189
|
+
<div className="flex flex-col items-center justify-center h-full gap-3 text-sm text-text-subtle">
|
|
159
190
|
{timedOut ? (
|
|
160
|
-
|
|
191
|
+
<>
|
|
192
|
+
<span className="text-destructive font-medium">Extension failed to load</span>
|
|
193
|
+
{activationError && (
|
|
194
|
+
<span className="text-xs text-muted-foreground max-w-md text-center">{activationError}</span>
|
|
195
|
+
)}
|
|
196
|
+
<button
|
|
197
|
+
onClick={handleRetry}
|
|
198
|
+
className="text-xs text-primary hover:underline"
|
|
199
|
+
>
|
|
200
|
+
Retry
|
|
201
|
+
</button>
|
|
202
|
+
</>
|
|
161
203
|
) : (
|
|
162
204
|
<>
|
|
163
205
|
<Loader2 className="size-5 animate-spin" />
|
|
@@ -27,6 +27,7 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
27
27
|
ports: lazy(() => import("@/components/ports/port-forwarding-tab").then((m) => ({ default: m.PortForwardingTab }))),
|
|
28
28
|
extension: lazy(() => import("@/components/extensions/extension-webview").then((m) => ({ default: m.ExtensionWebview }))),
|
|
29
29
|
"extension-webview": lazy(() => import("@/components/extensions/extension-webview").then((m) => ({ default: m.ExtensionWebview }))),
|
|
30
|
+
"conflict-editor": lazy(() => import("@/components/editor/conflict-editor").then((m) => ({ default: m.ConflictEditor }))),
|
|
30
31
|
};
|
|
31
32
|
|
|
32
33
|
interface EditorPanelProps {
|
|
@@ -30,6 +30,7 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
|
|
|
30
30
|
"git-diff": FileDiff, settings: Settings, ports: Globe,
|
|
31
31
|
extension: Puzzle,
|
|
32
32
|
"extension-webview": Puzzle,
|
|
33
|
+
"conflict-editor": FileDiff,
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
interface MobileNavProps { onMenuPress: () => void; onProjectsPress: () => void; }
|
|
@@ -58,6 +58,11 @@ const TAB_COMPONENTS: Record<TabType, React.LazyExoticComponent<React.ComponentT
|
|
|
58
58
|
default: m.ExtensionWebview,
|
|
59
59
|
})),
|
|
60
60
|
),
|
|
61
|
+
"conflict-editor": lazy(() =>
|
|
62
|
+
import("@/components/editor/conflict-editor").then((m) => ({
|
|
63
|
+
default: m.ConflictEditor,
|
|
64
|
+
})),
|
|
65
|
+
),
|
|
61
66
|
};
|
|
62
67
|
|
|
63
68
|
function LoadingFallback() {
|
|
@@ -40,6 +40,14 @@ export function useExtensionWs(enabled = true) {
|
|
|
40
40
|
switch (msg.type) {
|
|
41
41
|
case "contributions:update":
|
|
42
42
|
store.setContributions(msg.contributions);
|
|
43
|
+
if (msg.activationErrors) {
|
|
44
|
+
const prev = store.activationErrors;
|
|
45
|
+
store.setActivationErrors(msg.activationErrors);
|
|
46
|
+
// Only toast NEW errors (avoid spam on repeated contributions:update)
|
|
47
|
+
for (const [extId, error] of Object.entries(msg.activationErrors)) {
|
|
48
|
+
if (!prev[extId]) toast.error(`Extension "${extId}" failed to activate: ${error}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
43
51
|
break;
|
|
44
52
|
|
|
45
53
|
case "statusbar:update":
|
|
@@ -87,6 +87,10 @@ interface ExtensionStore {
|
|
|
87
87
|
contributions: ExtensionContributes | null;
|
|
88
88
|
setContributions: (c: ExtensionContributes) => void;
|
|
89
89
|
|
|
90
|
+
// Activation errors (sent from server)
|
|
91
|
+
activationErrors: Record<string, string>;
|
|
92
|
+
setActivationErrors: (errors: Record<string, string>) => void;
|
|
93
|
+
|
|
90
94
|
// QuickPick modal
|
|
91
95
|
quickPick: QuickPickState | null;
|
|
92
96
|
showQuickPick: (items: QuickPickItemUI[], options?: QuickPickState["options"]) => Promise<QuickPickItemUI[] | undefined>;
|
|
@@ -154,6 +158,10 @@ export const useExtensionStore = create<ExtensionStore>((set, get) => ({
|
|
|
154
158
|
contributions: null,
|
|
155
159
|
setContributions: (c) => set({ contributions: c }),
|
|
156
160
|
|
|
161
|
+
// --- Activation errors ---
|
|
162
|
+
activationErrors: {},
|
|
163
|
+
setActivationErrors: (errors) => set({ activationErrors: errors }),
|
|
164
|
+
|
|
157
165
|
// --- QuickPick ---
|
|
158
166
|
quickPick: null,
|
|
159
167
|
showQuickPick: (items, options = {}) => {
|
|
@@ -109,6 +109,8 @@ export function deriveTabId(type: TabType, metadata?: Record<string, unknown>):
|
|
|
109
109
|
}
|
|
110
110
|
case "git-diff":
|
|
111
111
|
return `git-diff:${metadata?.filePath ?? "unknown"}`;
|
|
112
|
+
case "conflict-editor":
|
|
113
|
+
return `conflict-editor:${metadata?.filePath ?? "unknown"}`;
|
|
112
114
|
case "settings":
|
|
113
115
|
return "settings";
|
|
114
116
|
case "ports":
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import{o as e}from"./chunk-CFjPhJqf.js";import{t}from"./react-nm2Ru1Pt.js";import{t as n}from"./jsx-runtime-BRW_vwa9.js";import{B as r,E as i,v as a}from"./index-OqgGFmh8.js";var o=e(t(),1),s=n(),c=`<script>
|
|
2
|
-
function acquireVsCodeApi(){return{postMessage:function(m){window.parent.postMessage(m,"*")},getState:function(){try{return JSON.parse(sessionStorage.getItem("vscode-state")||"null")}catch{return null}},setState:function(s){sessionStorage.setItem("vscode-state",JSON.stringify(s));return s}}}
|
|
3
|
-
<\/script>`;function l(e){if(!e)return e;let t=e.indexOf(`<head>`);return t===-1?c+e:e.slice(0,t+6)+c+e.slice(t+6)}function u({metadata:e}){let t=e?.panelId,n=e?.viewType,c=i(e=>e.currentProject),u=e?.projectName||c||void 0,[d,f]=(0,o.useState)(!1),p=a(e=>{if(t&&e.webviewPanels[t])return e.webviewPanels[t];if(n){let t=n.includes(`.`)?n:`${n}.view`;return Object.values(e.webviewPanels).find(e=>e.viewType===n||e.viewType===t)}}),m=p?.id??t,h=(0,o.useRef)(null),g=l(p?.html??``);(0,o.useEffect)(()=>{if(p||!n)return;let e=n.includes(`.`)?n:`${n}.view`,t=!1,r=null;async function i(){if(r)return r;if(!u)return[];try{let e=(await(await fetch(`/api/projects`)).json()).data?.find(e=>e.name===u);r=e?[e.path]:[]}catch{r=[]}return r}async function a(){let n=await i();t||window.dispatchEvent(new CustomEvent(`ext:command:execute`,{detail:{command:e,args:n}}))}let o=setTimeout(()=>{t||a()},500),s=setInterval(()=>{t||a()},2e3);return()=>{t=!0,clearTimeout(o),clearInterval(s)}},[p,n,u]);let _=(0,o.useRef)(c);return(0,o.useEffect)(()=>{if(!p||!n||!c||c===_.current){_.current=c;return}_.current=c,(async()=>{try{let e=(await(await fetch(`/api/projects`)).json()).data?.find(e=>e.name===c);if(e){let t=n.includes(`.`)?n:`${n}.view`;window.dispatchEvent(new CustomEvent(`ext:command:execute`,{detail:{command:t,args:[e.path]}}))}}catch{}})()},[p,n,c]),(0,o.useEffect)(()=>{if(p){f(!1);return}let e=setTimeout(()=>f(!0),1e4);return()=>clearTimeout(e)},[p]),(0,o.useEffect)(()=>{if(!m)return;let e=e=>{h.current&&e.source===h.current.contentWindow&&window.dispatchEvent(new CustomEvent(`ext:webview:send`,{detail:{panelId:m,message:e.data}}))};return window.addEventListener(`message`,e),()=>window.removeEventListener(`message`,e)},[m]),(0,o.useEffect)(()=>{if(!m)return;let e=e=>{let{panelId:t,message:n}=e.detail;t===m&&h.current?.contentWindow?.postMessage(n,`*`)};return window.addEventListener(`ext:webview:message`,e),()=>window.removeEventListener(`ext:webview:message`,e)},[m]),p?(0,s.jsx)(`div`,{className:`h-full w-full relative`,children:(0,s.jsx)(`iframe`,{ref:h,srcDoc:g,sandbox:`allow-scripts`,className:`w-full h-full border-0 bg-white dark:bg-zinc-900`,title:p.title})}):(0,s.jsx)(`div`,{className:`flex flex-col items-center justify-center h-full gap-2 text-sm text-text-subtle`,children:d?(0,s.jsx)(`span`,{children:`Extension failed to load webview panel`}):(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(r,{className:`size-5 animate-spin`}),(0,s.jsx)(`span`,{children:`Loading extension...`})]})})}export{u as ExtensionWebview};
|