@hienlh/ppm 0.11.17 → 0.12.0
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/CHANGELOG.md +22 -0
- package/dist/web/assets/{ai-settings-section-L6XAmZEP.js → ai-settings-section-BHdBBJtS.js} +1 -1
- package/dist/web/assets/{audio-preview-VMboGrIH.js → audio-preview-D4AxF10w.js} +1 -1
- package/dist/web/assets/chat-tab-Bq2hmJ-B.js +12 -0
- package/dist/web/assets/code-editor-CMcDjype.js +8 -0
- package/dist/web/assets/{conflict-editor-943WUefe.js → conflict-editor-Br-ugFiK.js} +1 -1
- package/dist/web/assets/{csv-preview-BEBJD4a_.js → csv-preview-HMSavgBb.js} +1 -1
- package/dist/web/assets/{database-viewer-BV0Ebp0z.js → database-viewer-DxP0GmQK.js} +2 -2
- package/dist/web/assets/{diff-viewer-B3gAWXgA.js → diff-viewer-oEyE9UwV.js} +1 -1
- package/dist/web/assets/dist-D7KGU7Vl.js +1 -0
- package/dist/web/assets/extension-webview-CVqfQGjg.js +3 -0
- package/dist/web/assets/{image-preview-BEiYtg6_.js → image-preview-CY3sVd25.js} +1 -1
- package/dist/web/assets/index-BDRoldC9.js +23 -0
- package/dist/web/assets/index-CDSox8V2.css +2 -0
- package/dist/web/assets/{input-ClhO__YM.js → input-Dk49gO8E.js} +1 -1
- package/dist/web/assets/{markdown-renderer-t1ZBKbXZ.js → markdown-renderer-DwqWhkri.js} +1 -1
- package/dist/web/assets/{pdf-preview-CjfQxXE5.js → pdf-preview-Cl95qWE_.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-BZmfg410.js → port-forwarding-tab-iJ3MAjXa.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CSTO0jc2.js → postgres-viewer-Do_w0Cji.js} +2 -2
- package/dist/web/assets/{scroll-area-DW7L4Gnc.js → scroll-area-BEllam7_.js} +1 -1
- package/dist/web/assets/settings-tab-DyBeLmUh.js +1 -0
- package/dist/web/assets/{sqlite-viewer-D0oWgepE.js → sqlite-viewer-oZkGJfW2.js} +1 -1
- package/dist/web/assets/{terminal-tab-WBPZXu12.js → terminal-tab-UoDiWvzG.js} +1 -1
- package/dist/web/assets/{vendor-ui-B-T_damt.js → vendor-ui-B-89Uj8i.js} +1 -1
- package/dist/web/assets/{video-preview-BcMa4tim.js → video-preview-3MbkDYcA.js} +1 -1
- package/dist/web/index.html +7 -7
- package/dist/web/sw.js +1 -1
- package/docs/project-changelog.md +56 -0
- package/docs/system-architecture.md +10 -1
- package/package.json +1 -1
- package/src/server/routes/chat.ts +57 -2
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/tag-routes.ts +93 -0
- package/src/services/db.service.ts +35 -1
- package/src/services/project.service.ts +2 -0
- package/src/services/supervisor.ts +7 -2
- package/src/services/tag.service.ts +114 -0
- package/src/types/chat.ts +9 -0
- package/src/web/components/chat/chat-history-bar.tsx +106 -7
- package/src/web/components/chat/chat-welcome.tsx +54 -27
- package/src/web/components/chat/session-context-menu.tsx +101 -0
- package/src/web/components/chat/session-picker.tsx +3 -0
- package/src/web/components/chat/tag-filter-chips.tsx +58 -0
- package/src/web/components/extensions/extension-webview.tsx +5 -33
- package/src/web/components/layout/editor-panel.tsx +53 -26
- package/src/web/components/layout/upgrade-banner.tsx +47 -37
- package/src/web/components/settings/tag-settings-section.tsx +167 -0
- package/src/web/hooks/use-extension-ws.ts +7 -2
- package/src/web/styles/globals.css +14 -0
- package/dist/web/assets/chat-tab-DfO2rHO8.js +0 -12
- package/dist/web/assets/code-editor-BU7NX_SZ.js +0 -8
- package/dist/web/assets/dist-C5IgeqrV.js +0 -1
- package/dist/web/assets/extension-webview-C8rdBYLl.js +0 -3
- package/dist/web/assets/index-B0V_IYbX.css +0 -2
- package/dist/web/assets/index-CBsOxcqb.js +0 -23
- package/dist/web/assets/settings-tab-b3AbZg6I.js +0 -1
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { api } from "@/lib/api-client";
|
|
3
3
|
import { toast } from "sonner";
|
|
4
|
-
import { Loader2, ArrowUpCircle, X } from "lucide-react";
|
|
4
|
+
import { Loader2, ArrowUpCircle, X, RefreshCw, CheckCircle2 } from "lucide-react";
|
|
5
5
|
|
|
6
6
|
const POLL_INTERVAL_MS = 60_000;
|
|
7
7
|
const DISMISS_KEY_PREFIX = "ppm-upgrade-dismissed-";
|
|
8
|
-
const RESTART_POLL_MS = 1_500;
|
|
9
|
-
const RESTART_TIMEOUT_MS = 60_000;
|
|
10
8
|
|
|
11
9
|
interface UpgradeStatus {
|
|
12
10
|
currentVersion: string;
|
|
@@ -21,28 +19,13 @@ interface UpgradeResult {
|
|
|
21
19
|
message?: string;
|
|
22
20
|
}
|
|
23
21
|
|
|
24
|
-
/**
|
|
25
|
-
async function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
while (Date.now() - start < RESTART_TIMEOUT_MS) {
|
|
30
|
-
await new Promise((r) => setTimeout(r, RESTART_POLL_MS));
|
|
31
|
-
try {
|
|
32
|
-
const res = await fetch("/api/health", { cache: "no-store" });
|
|
33
|
-
if (res.ok && serverWentDown) {
|
|
34
|
-
if ("caches" in window) {
|
|
35
|
-
const keys = await caches.keys();
|
|
36
|
-
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
37
|
-
}
|
|
38
|
-
window.location.reload();
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
serverWentDown = true;
|
|
43
|
-
}
|
|
22
|
+
/** Clear browser/SW caches and reload the page */
|
|
23
|
+
async function clearCachesAndReload() {
|
|
24
|
+
if ("caches" in window) {
|
|
25
|
+
const keys = await caches.keys();
|
|
26
|
+
await Promise.all(keys.map((k) => caches.delete(k)));
|
|
44
27
|
}
|
|
45
|
-
|
|
28
|
+
window.location.reload();
|
|
46
29
|
}
|
|
47
30
|
|
|
48
31
|
interface UpgradeBannerProps {
|
|
@@ -52,6 +35,7 @@ interface UpgradeBannerProps {
|
|
|
52
35
|
export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
53
36
|
const [availableVersion, setAvailableVersion] = useState<string | null>(null);
|
|
54
37
|
const [upgrading, setUpgrading] = useState(false);
|
|
38
|
+
const [upgradeComplete, setUpgradeComplete] = useState(false);
|
|
55
39
|
const [dismissed, setDismissed] = useState(false);
|
|
56
40
|
|
|
57
41
|
// Poll for upgrade status
|
|
@@ -86,12 +70,9 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
86
70
|
const data = await api.post<UpgradeResult>("/api/upgrade/apply");
|
|
87
71
|
|
|
88
72
|
if (data.restart) {
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
toast.warning("Upgrade installed but server hasn't restarted. Try refreshing manually.");
|
|
93
|
-
setUpgrading(false);
|
|
94
|
-
}
|
|
73
|
+
// Upgrade installed, server will restart — ask user to reload
|
|
74
|
+
setUpgrading(false);
|
|
75
|
+
setUpgradeComplete(true);
|
|
95
76
|
} else {
|
|
96
77
|
// No supervisor — manual restart needed
|
|
97
78
|
toast.info(data.message || "Upgrade installed. Restart PPM manually.");
|
|
@@ -102,10 +83,21 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
102
83
|
setDismissed(true);
|
|
103
84
|
}
|
|
104
85
|
} catch (e) {
|
|
105
|
-
|
|
106
|
-
|
|
86
|
+
// If fetch failed with a network error, the server likely died mid-response
|
|
87
|
+
// after the install succeeded (supervisor killed the server before response flushed).
|
|
88
|
+
// Show reload prompt instead of a confusing error.
|
|
89
|
+
const isNetworkError = e instanceof TypeError
|
|
90
|
+
|| (e as Error).message?.includes("fetch")
|
|
91
|
+
|| (e as Error).message?.includes("network");
|
|
92
|
+
if (isNetworkError) {
|
|
93
|
+
setUpgrading(false);
|
|
94
|
+
setUpgradeComplete(true);
|
|
95
|
+
} else {
|
|
96
|
+
toast.error(`Upgrade failed: ${(e as Error).message}`);
|
|
97
|
+
setUpgrading(false);
|
|
98
|
+
}
|
|
107
99
|
}
|
|
108
|
-
}, []);
|
|
100
|
+
}, [availableVersion]);
|
|
109
101
|
|
|
110
102
|
const handleDismiss = useCallback(() => {
|
|
111
103
|
if (availableVersion) {
|
|
@@ -114,7 +106,7 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
114
106
|
setDismissed(true);
|
|
115
107
|
}, [availableVersion]);
|
|
116
108
|
|
|
117
|
-
const visible = !!availableVersion && !dismissed;
|
|
109
|
+
const visible = (!!availableVersion && !dismissed) || upgradeComplete;
|
|
118
110
|
|
|
119
111
|
useEffect(() => {
|
|
120
112
|
onVisibilityChange?.(visible);
|
|
@@ -123,12 +115,30 @@ export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
|
123
115
|
if (!visible) return null;
|
|
124
116
|
|
|
125
117
|
return (
|
|
126
|
-
<div className=
|
|
127
|
-
|
|
118
|
+
<div className={`w-full text-white px-3 py-1 flex items-center justify-between gap-2 z-50 text-sm shrink-0 ${
|
|
119
|
+
upgradeComplete ? "bg-green-600 dark:bg-green-700" : "bg-blue-600 dark:bg-blue-700"
|
|
120
|
+
}`}>
|
|
121
|
+
{upgradeComplete ? (
|
|
122
|
+
<>
|
|
123
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
124
|
+
<CheckCircle2 className="size-4 shrink-0" />
|
|
125
|
+
<span className="truncate">
|
|
126
|
+
Upgrade to v{availableVersion} installed! Reload to apply.
|
|
127
|
+
</span>
|
|
128
|
+
</div>
|
|
129
|
+
<button
|
|
130
|
+
onClick={clearCachesAndReload}
|
|
131
|
+
className="bg-white text-green-600 font-medium rounded-full px-3 py-0.5 text-xs min-h-[28px] min-w-[28px] flex items-center gap-1.5 justify-center hover:bg-green-50 active:bg-green-100 transition-colors shrink-0"
|
|
132
|
+
>
|
|
133
|
+
<RefreshCw className="size-3" />
|
|
134
|
+
Reload
|
|
135
|
+
</button>
|
|
136
|
+
</>
|
|
137
|
+
) : upgrading ? (
|
|
128
138
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
129
139
|
<Loader2 className="size-4 animate-spin shrink-0" />
|
|
130
140
|
<span className="truncate">
|
|
131
|
-
Upgrading to v{availableVersion}...
|
|
141
|
+
Upgrading to v{availableVersion}...
|
|
132
142
|
</span>
|
|
133
143
|
</div>
|
|
134
144
|
) : (
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { Plus, Trash2, Pencil, Check, X, RotateCcw } from "lucide-react";
|
|
3
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
4
|
+
import type { ProjectTag } from "../../../types/chat";
|
|
5
|
+
|
|
6
|
+
interface TagSettingsSectionProps {
|
|
7
|
+
projectName: string;
|
|
8
|
+
onTagsChanged?: () => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TagSettingsSection({ projectName, onTagsChanged }: TagSettingsSectionProps) {
|
|
12
|
+
const [tags, setTags] = useState<ProjectTag[]>([]);
|
|
13
|
+
const [defaultTagId, setDefaultTagId] = useState<number | null>(null);
|
|
14
|
+
const [loading, setLoading] = useState(true);
|
|
15
|
+
const [editingId, setEditingId] = useState<number | null>(null);
|
|
16
|
+
const [editName, setEditName] = useState("");
|
|
17
|
+
const [editColor, setEditColor] = useState("");
|
|
18
|
+
const [newName, setNewName] = useState("");
|
|
19
|
+
const [newColor, setNewColor] = useState("#22c55e");
|
|
20
|
+
const [showAdd, setShowAdd] = useState(false);
|
|
21
|
+
|
|
22
|
+
const baseUrl = `${projectUrl(projectName)}/tags`;
|
|
23
|
+
|
|
24
|
+
const loadTags = useCallback(async () => {
|
|
25
|
+
try {
|
|
26
|
+
const data = await api.get<{ tags: ProjectTag[]; defaultTagId: number | null }>(baseUrl);
|
|
27
|
+
setTags(data.tags);
|
|
28
|
+
setDefaultTagId(data.defaultTagId);
|
|
29
|
+
} catch { /* silent */ }
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}, [baseUrl]);
|
|
32
|
+
|
|
33
|
+
useEffect(() => { loadTags(); }, [loadTags]);
|
|
34
|
+
|
|
35
|
+
const handleCreate = async () => {
|
|
36
|
+
if (!newName.trim()) return;
|
|
37
|
+
try {
|
|
38
|
+
await api.post(baseUrl, { name: newName.trim(), color: newColor });
|
|
39
|
+
setNewName("");
|
|
40
|
+
setShowAdd(false);
|
|
41
|
+
loadTags();
|
|
42
|
+
onTagsChanged?.();
|
|
43
|
+
} catch { /* silent */ }
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const handleUpdate = async (id: number) => {
|
|
47
|
+
try {
|
|
48
|
+
await api.patch(`${baseUrl}/${id}`, { name: editName.trim() || undefined, color: editColor || undefined });
|
|
49
|
+
setEditingId(null);
|
|
50
|
+
loadTags();
|
|
51
|
+
onTagsChanged?.();
|
|
52
|
+
} catch { /* silent */ }
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const handleDelete = async (id: number, name: string) => {
|
|
56
|
+
if (!window.confirm(`Delete tag "${name}"? Sessions with this tag will become untagged.`)) return;
|
|
57
|
+
try {
|
|
58
|
+
await api.del(`${baseUrl}/${id}`);
|
|
59
|
+
loadTags();
|
|
60
|
+
onTagsChanged?.();
|
|
61
|
+
} catch { /* silent */ }
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const handleSetDefault = async (tagId: number) => {
|
|
65
|
+
const newId = tagId === defaultTagId ? null : tagId;
|
|
66
|
+
try {
|
|
67
|
+
await api.patch(`${baseUrl}/default-tag`, { tagId: newId });
|
|
68
|
+
setDefaultTagId(newId);
|
|
69
|
+
} catch { /* silent */ }
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleReset = async () => {
|
|
73
|
+
try {
|
|
74
|
+
await api.post(`${baseUrl}/reset`, {});
|
|
75
|
+
loadTags();
|
|
76
|
+
onTagsChanged?.();
|
|
77
|
+
} catch { /* silent */ }
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
if (loading) return <p className="text-[11px] text-muted-foreground animate-pulse">Loading tags...</p>;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className="space-y-2">
|
|
84
|
+
<div className="flex items-center justify-between">
|
|
85
|
+
<h3 className="text-xs font-medium text-muted-foreground">Session Tags</h3>
|
|
86
|
+
<div className="flex items-center gap-1">
|
|
87
|
+
<button onClick={handleReset} className="p-1 rounded text-text-subtle hover:text-text-secondary" title="Reset to defaults">
|
|
88
|
+
<RotateCcw className="size-3" />
|
|
89
|
+
</button>
|
|
90
|
+
<button onClick={() => setShowAdd(!showAdd)} className="p-1 rounded text-primary hover:bg-primary/10" title="Add tag">
|
|
91
|
+
<Plus className="size-3.5" />
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* Add form */}
|
|
97
|
+
{showAdd && (
|
|
98
|
+
<div className="flex items-center gap-1.5 px-1">
|
|
99
|
+
<input type="color" value={newColor} onChange={(e) => setNewColor(e.target.value)} className="size-6 rounded cursor-pointer border-0 p-0" />
|
|
100
|
+
<input
|
|
101
|
+
value={newName}
|
|
102
|
+
onChange={(e) => setNewName(e.target.value)}
|
|
103
|
+
onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); if (e.key === "Escape") setShowAdd(false); }}
|
|
104
|
+
placeholder="Tag name"
|
|
105
|
+
className="flex-1 min-w-0 bg-surface-elevated text-[11px] text-text-primary px-2 py-1 rounded border border-border outline-none focus:border-primary"
|
|
106
|
+
autoFocus
|
|
107
|
+
/>
|
|
108
|
+
<button onClick={handleCreate} className="p-1 text-green-500 hover:text-green-400"><Check className="size-3.5" /></button>
|
|
109
|
+
<button onClick={() => setShowAdd(false)} className="p-1 text-text-subtle hover:text-text-secondary"><X className="size-3.5" /></button>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
|
|
113
|
+
{/* Tag list */}
|
|
114
|
+
<div className="space-y-0.5">
|
|
115
|
+
{tags.map((tag) => (
|
|
116
|
+
<div key={tag.id} className="flex items-center gap-1.5 px-1 py-1 rounded hover:bg-surface-elevated group">
|
|
117
|
+
{editingId === tag.id ? (
|
|
118
|
+
<>
|
|
119
|
+
<input type="color" value={editColor} onChange={(e) => setEditColor(e.target.value)} className="size-5 rounded cursor-pointer border-0 p-0" />
|
|
120
|
+
<input
|
|
121
|
+
value={editName}
|
|
122
|
+
onChange={(e) => setEditName(e.target.value)}
|
|
123
|
+
onKeyDown={(e) => { if (e.key === "Enter") handleUpdate(tag.id); if (e.key === "Escape") setEditingId(null); }}
|
|
124
|
+
className="flex-1 min-w-0 bg-surface-elevated text-[11px] px-1.5 py-0.5 rounded border border-border outline-none focus:border-primary"
|
|
125
|
+
autoFocus
|
|
126
|
+
/>
|
|
127
|
+
<button onClick={() => handleUpdate(tag.id)} className="p-0.5 text-green-500"><Check className="size-3" /></button>
|
|
128
|
+
<button onClick={() => setEditingId(null)} className="p-0.5 text-text-subtle"><X className="size-3" /></button>
|
|
129
|
+
</>
|
|
130
|
+
) : (
|
|
131
|
+
<>
|
|
132
|
+
<span className="size-3 rounded-full shrink-0" style={{ backgroundColor: tag.color }} />
|
|
133
|
+
<span className="flex-1 text-[11px] text-text-primary truncate">{tag.name}</span>
|
|
134
|
+
<button
|
|
135
|
+
onClick={() => handleSetDefault(tag.id)}
|
|
136
|
+
className={`px-1.5 py-0.5 rounded text-[9px] font-medium transition-colors ${
|
|
137
|
+
tag.id === defaultTagId
|
|
138
|
+
? "bg-primary/15 text-primary border border-primary/30"
|
|
139
|
+
: "text-text-subtle border border-transparent can-hover:opacity-0 can-hover:group-hover:opacity-100 hover:bg-surface-elevated hover:border-border"
|
|
140
|
+
}`}
|
|
141
|
+
title={tag.id === defaultTagId ? "Default tag (click to unset)" : "Set as default for new sessions"}
|
|
142
|
+
>
|
|
143
|
+
{tag.id === defaultTagId ? "Default" : "Set default"}
|
|
144
|
+
</button>
|
|
145
|
+
<button
|
|
146
|
+
onClick={() => { setEditingId(tag.id); setEditName(tag.name); setEditColor(tag.color); }}
|
|
147
|
+
className="p-0.5 rounded text-text-subtle hover:text-text-secondary can-hover:opacity-0 can-hover:group-hover:opacity-100"
|
|
148
|
+
>
|
|
149
|
+
<Pencil className="size-3" />
|
|
150
|
+
</button>
|
|
151
|
+
<button
|
|
152
|
+
onClick={() => handleDelete(tag.id, tag.name)}
|
|
153
|
+
className="p-0.5 rounded text-text-subtle hover:text-red-400 can-hover:opacity-0 can-hover:group-hover:opacity-100"
|
|
154
|
+
>
|
|
155
|
+
<Trash2 className="size-3" />
|
|
156
|
+
</button>
|
|
157
|
+
</>
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
))}
|
|
161
|
+
{tags.length === 0 && (
|
|
162
|
+
<p className="text-[11px] text-muted-foreground py-2 text-center">No tags. Click + to create one.</p>
|
|
163
|
+
)}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
);
|
|
167
|
+
}
|
|
@@ -151,10 +151,15 @@ export function useExtensionWs(enabled = true) {
|
|
|
151
151
|
if (t) { existingTabId = t.id; break; }
|
|
152
152
|
}
|
|
153
153
|
if (existingTabId) {
|
|
154
|
-
// Tab already exists — update metadata with new panelId (panel was recreated)
|
|
154
|
+
// Tab already exists — update metadata with new panelId (panel was recreated).
|
|
155
|
+
// Preserve existing metadata (e.g. projectName) since updateTab replaces metadata entirely.
|
|
156
|
+
const existingTab = ps.grid.flat().reduce<Record<string, unknown> | undefined>((acc, pid) => {
|
|
157
|
+
if (acc) return acc;
|
|
158
|
+
return ps.panels[pid]?.tabs.find(tab => tab.id === existingTabId)?.metadata;
|
|
159
|
+
}, undefined);
|
|
155
160
|
useTabStore.getState().updateTab(existingTabId, {
|
|
156
161
|
title: msg.title,
|
|
157
|
-
metadata: { viewType: viewTypeSlug, panelId: msg.panelId, extensionId: msg.extensionId },
|
|
162
|
+
metadata: { ...existingTab, viewType: viewTypeSlug, panelId: msg.panelId, extensionId: msg.extensionId },
|
|
158
163
|
});
|
|
159
164
|
// Focus the existing tab so Cmd+G / command palette switches to it
|
|
160
165
|
useTabStore.getState().setActiveTab(existingTabId);
|
|
@@ -172,6 +172,7 @@ html, body {
|
|
|
172
172
|
|
|
173
173
|
.markdown-content pre {
|
|
174
174
|
overflow-x: auto;
|
|
175
|
+
overflow-y: hidden;
|
|
175
176
|
border-radius: 6px;
|
|
176
177
|
background: var(--color-background);
|
|
177
178
|
padding: 0.5rem;
|
|
@@ -179,6 +180,19 @@ html, body {
|
|
|
179
180
|
font-family: var(--font-mono);
|
|
180
181
|
border: 1px solid var(--color-border);
|
|
181
182
|
margin: 0.5rem 0;
|
|
183
|
+
scrollbar-width: thin;
|
|
184
|
+
scrollbar-color: var(--color-border) transparent;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.markdown-content pre::-webkit-scrollbar {
|
|
188
|
+
height: 4px;
|
|
189
|
+
}
|
|
190
|
+
.markdown-content pre::-webkit-scrollbar-track {
|
|
191
|
+
background: transparent;
|
|
192
|
+
}
|
|
193
|
+
.markdown-content pre::-webkit-scrollbar-thumb {
|
|
194
|
+
background: var(--color-border);
|
|
195
|
+
border-radius: 9999px;
|
|
182
196
|
}
|
|
183
197
|
|
|
184
198
|
.markdown-content code {
|