@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.
Files changed (52) hide show
  1. package/260415-0932-git-graph-stash-rebase-conflicts/reports/code-reviewer-260415-1020-stash-rebase-conflicts.md +288 -0
  2. package/260415-0932-git-graph-stash-rebase-conflicts/reports/tester-260415-1020-build-check.md +117 -0
  3. package/260415-1150-ext-silent-failure-debugging/reports/code-reviewer-260415-1159-ext-error-reporting-review.md +205 -0
  4. package/260415-1150-ext-silent-failure-debugging/reports/docs-manager-260415-1206-ext-error-reporting.md +99 -0
  5. package/260415-1150-ext-silent-failure-debugging/reports/tester-260415-1159-extension-error-reporting.md +174 -0
  6. package/CHANGELOG.md +19 -0
  7. package/dist/web/assets/{chat-tab-BEEd-Km4.js → chat-tab-R4gKsnxD.js} +1 -1
  8. package/dist/web/assets/{code-editor-Ij4p30cr.js → code-editor-Br0vzTOy.js} +2 -2
  9. package/dist/web/assets/conflict-editor-BPgCjnNz.js +19 -0
  10. package/dist/web/assets/{csv-preview-CwQnOa3E.js → csv-preview-BZRICDP0.js} +1 -1
  11. package/dist/web/assets/{database-viewer-C1UHSgft.js → database-viewer-DaUoQ-oR.js} +1 -1
  12. package/dist/web/assets/{diff-viewer-CVx5naBA.js → diff-viewer-BzvK3gAE.js} +1 -1
  13. package/dist/web/assets/extension-webview-CGepEw-b.js +3 -0
  14. package/dist/web/assets/{index-OqgGFmh8.js → index-CKsEzQ4f.js} +4 -4
  15. package/dist/web/assets/index-Chf0otez.css +2 -0
  16. package/dist/web/assets/keybindings-store-D5zgHod8.js +1 -0
  17. package/dist/web/assets/{markdown-renderer-CRy8xw2B.js → markdown-renderer-DSYnGywb.js} +1 -1
  18. package/dist/web/assets/{port-forwarding-tab-Biua8ov5.js → port-forwarding-tab-vmqDKmk2.js} +1 -1
  19. package/dist/web/assets/{postgres-viewer-BcVjCAl4.js → postgres-viewer-0lIAosrr.js} +1 -1
  20. package/dist/web/assets/{settings-tab-C9X-N8hE.js → settings-tab-CMnv1fce.js} +1 -1
  21. package/dist/web/assets/{sql-query-editor-BFvRvJn0.js → sql-query-editor-Bc2hAwqT.js} +1 -1
  22. package/dist/web/assets/{sqlite-viewer-CPfvwFl4.js → sqlite-viewer-B60MS2Dy.js} +1 -1
  23. package/dist/web/assets/{terminal-tab-mWwk_weB.js → terminal-tab-CCJoLstH.js} +1 -1
  24. package/dist/web/assets/{use-monaco-theme-CPaeSMAA.js → use-monaco-theme-BJK48EmK.js} +1 -1
  25. package/dist/web/index.html +2 -2
  26. package/dist/web/sw.js +1 -1
  27. package/docs/codebase-summary.md +39 -6
  28. package/docs/project-changelog.md +86 -25
  29. package/docs/project-roadmap.md +3 -2
  30. package/docs/system-architecture.md +44 -1
  31. package/package.json +1 -1
  32. package/packages/ext-git-graph/src/extension.ts +126 -5
  33. package/packages/ext-git-graph/src/types.ts +13 -2
  34. package/packages/ext-git-graph/src/webview-html.ts +249 -31
  35. package/src/server/ws/extensions.ts +28 -2
  36. package/src/services/extension-host-worker.ts +6 -1
  37. package/src/services/extension.service.ts +17 -3
  38. package/src/types/extension-messages.ts +1 -1
  39. package/src/web/components/editor/conflict-editor.tsx +368 -0
  40. package/src/web/components/extensions/extension-webview.tsx +45 -3
  41. package/src/web/components/layout/editor-panel.tsx +1 -0
  42. package/src/web/components/layout/mobile-nav.tsx +1 -0
  43. package/src/web/components/layout/tab-bar.tsx +1 -0
  44. package/src/web/components/layout/tab-content.tsx +5 -0
  45. package/src/web/hooks/use-extension-ws.ts +8 -0
  46. package/src/web/stores/extension-store.ts +8 -0
  47. package/src/web/stores/panel-utils.ts +2 -0
  48. package/src/web/stores/tab-store.ts +2 -1
  49. package/dist/web/assets/extension-webview-CHVVpV34.js +0 -3
  50. package/dist/web/assets/index-vA7juDri.css +0 -2
  51. package/dist/web/assets/keybindings-store-BQxgPV5o.js +0 -1
  52. /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, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
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-2 text-sm text-text-subtle">
189
+ <div className="flex flex-col items-center justify-center h-full gap-3 text-sm text-text-subtle">
159
190
  {timedOut ? (
160
- <span>Extension failed to load webview panel</span>
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; }
@@ -40,6 +40,7 @@ const TAB_ICONS: Record<TabType, React.ElementType> = {
40
40
  ports: Globe,
41
41
  extension: Puzzle,
42
42
  "extension-webview": Puzzle,
43
+ "conflict-editor": FileDiff,
43
44
  };
44
45
 
45
46
  interface TabBarProps {
@@ -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":
@@ -13,7 +13,8 @@ export type TabType =
13
13
  | "settings"
14
14
  | "ports"
15
15
  | "extension"
16
- | "extension-webview";
16
+ | "extension-webview"
17
+ | "conflict-editor";
17
18
 
18
19
  export interface Tab {
19
20
  id: string;
@@ -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};