@hienlh/ppm 0.11.18 → 0.12.1
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-CZ7uVdJ1.js → audio-preview-DBOZOGw7.js} +1 -1
- package/dist/web/assets/chat-tab-BrdZcg-D.js +12 -0
- package/dist/web/assets/code-editor-BjB4rgPf.js +8 -0
- package/dist/web/assets/{conflict-editor-BsNC4CIp.js → conflict-editor-BnXrp55a.js} +1 -1
- package/dist/web/assets/{csv-preview-BEBJD4a_.js → csv-preview-HMSavgBb.js} +1 -1
- package/dist/web/assets/{database-viewer-dYaf9tXG.js → database-viewer-D39etQBx.js} +2 -2
- package/dist/web/assets/{diff-viewer-BlQsizFF.js → diff-viewer-NVkEMuQW.js} +1 -1
- package/dist/web/assets/dist-D7KGU7Vl.js +1 -0
- package/dist/web/assets/extension-webview-AjxJYBif.js +3 -0
- package/dist/web/assets/{image-preview-eH8PNO1D.js → image-preview-BDhESiBw.js} +1 -1
- package/dist/web/assets/index-BeD-Flot.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-CyDOVFYo.js → markdown-renderer-CRA2pyBy.js} +1 -1
- package/dist/web/assets/{pdf-preview-BOJHsWt2.js → pdf-preview-BbA8cq4C.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-BxuO01Zb.js → port-forwarding-tab-D-Ka0Xhw.js} +1 -1
- package/dist/web/assets/{postgres-viewer-cLp_WazA.js → postgres-viewer-B4YwHwTt.js} +2 -2
- package/dist/web/assets/{scroll-area-DW7L4Gnc.js → scroll-area-BEllam7_.js} +1 -1
- package/dist/web/assets/settings-tab-ycdscbD8.js +1 -0
- package/dist/web/assets/{sqlite-viewer-BgEY7Fml.js → sqlite-viewer-CBR5cebG.js} +1 -1
- package/dist/web/assets/{terminal-tab-B6meTG0M.js → terminal-tab-DATStOUT.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-D2gRujKO.js → video-preview-B-DptaDM.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/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/draggable-tab.tsx +12 -0
- package/src/web/components/layout/editor-panel.tsx +53 -26
- package/src/web/components/layout/tab-bar.tsx +66 -0
- 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-D0edjxcO.js +0 -12
- package/dist/web/assets/code-editor-CHWEBvuT.js +0 -8
- package/dist/web/assets/dist-C5IgeqrV.js +0 -1
- package/dist/web/assets/extension-webview-DXjdd0U6.js +0 -3
- package/dist/web/assets/index-BVQikaUv.js +0 -23
- package/dist/web/assets/index-C99i-AFP.css +0 -2
- package/dist/web/assets/settings-tab-BQpEkd-V.js +0 -1
|
@@ -4,6 +4,8 @@ import { usePanelStore } from "@/stores/panel-store";
|
|
|
4
4
|
import { useProjectStore } from "@/stores/project-store";
|
|
5
5
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
6
6
|
import { api, projectUrl } from "@/lib/api-client";
|
|
7
|
+
import { useProjectTags, TagChipBar } from "@/components/chat/tag-filter-chips";
|
|
8
|
+
import { SessionContextMenu } from "@/components/chat/session-context-menu";
|
|
7
9
|
import type { SessionInfo } from "../../../types/chat";
|
|
8
10
|
import { TabBar } from "./tab-bar";
|
|
9
11
|
import { SplitDropOverlay } from "./split-drop-overlay";
|
|
@@ -111,6 +113,8 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
111
113
|
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
112
114
|
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
113
115
|
const [showAll, setShowAll] = useState(false);
|
|
116
|
+
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
|
|
117
|
+
const { projectTags, tagCounts, loadTags } = useProjectTags(activeProject?.name);
|
|
114
118
|
|
|
115
119
|
const loadSessions = useCallback(async () => {
|
|
116
120
|
if (!activeProject?.name) return;
|
|
@@ -176,41 +180,58 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
176
180
|
);
|
|
177
181
|
}
|
|
178
182
|
|
|
179
|
-
const
|
|
180
|
-
|
|
183
|
+
const handleTagChanged = useCallback((sid: string, tag: { id: number; name: string; color: string } | null) => {
|
|
184
|
+
setSessions((prev) => prev.map((s) => s.id === sid ? { ...s, tag } : s));
|
|
185
|
+
loadTags();
|
|
186
|
+
}, [loadTags]);
|
|
187
|
+
|
|
188
|
+
const filtered = selectedTagId !== null ? sessions.filter((s) => s.tag?.id === selectedTagId) : sessions;
|
|
189
|
+
const pinnedSessions = filtered.filter((s) => s.pinned);
|
|
190
|
+
const allRecentSessions = filtered.filter((s) => !s.pinned);
|
|
181
191
|
const recentSessions = showAll ? allRecentSessions : allRecentSessions.slice(0, MAX_RECENT_SESSIONS);
|
|
182
192
|
const hasMore = allRecentSessions.length > MAX_RECENT_SESSIONS;
|
|
183
193
|
|
|
184
194
|
function renderSessionRow(session: SessionInfo) {
|
|
185
195
|
return (
|
|
186
|
-
<
|
|
196
|
+
<SessionContextMenu
|
|
187
197
|
key={session.id}
|
|
188
|
-
|
|
189
|
-
|
|
198
|
+
session={session}
|
|
199
|
+
projectName={activeProject!.name}
|
|
200
|
+
projectTags={projectTags}
|
|
201
|
+
onTogglePin={togglePin}
|
|
202
|
+
onTagChanged={handleTagChanged}
|
|
190
203
|
>
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
</span>
|
|
195
|
-
{session.updatedAt && (
|
|
196
|
-
<span className="text-[10px] text-text-subtle shrink-0">
|
|
197
|
-
{formatRelativeDate(session.updatedAt)}
|
|
198
|
-
</span>
|
|
199
|
-
)}
|
|
200
|
-
<span
|
|
201
|
-
role="button"
|
|
202
|
-
tabIndex={0}
|
|
203
|
-
onClick={(e) => togglePin(e, session)}
|
|
204
|
-
className={`p-1 rounded transition-colors shrink-0 ${
|
|
205
|
-
session.pinned
|
|
206
|
-
? "text-primary hover:text-primary/70"
|
|
207
|
-
: "text-text-subtle can-hover:opacity-0 can-hover:group-hover:opacity-100 hover:text-text-primary"
|
|
208
|
-
}`}
|
|
209
|
-
aria-label={session.pinned ? "Unpin session" : "Pin session"}
|
|
204
|
+
<button
|
|
205
|
+
onClick={() => openSession(session)}
|
|
206
|
+
className="group flex items-center gap-2.5 w-full px-3 py-2.5 text-left hover:bg-surface-elevated active:bg-surface-elevated transition-colors border-b border-border/50 last:border-0"
|
|
210
207
|
>
|
|
211
|
-
|
|
208
|
+
<MessageSquare className="size-3.5 shrink-0 text-text-subtle" />
|
|
209
|
+
{session.tag && (
|
|
210
|
+
<span className="size-2 rounded-full shrink-0" style={{ backgroundColor: session.tag.color }} title={session.tag.name} />
|
|
211
|
+
)}
|
|
212
|
+
<span className="flex-1 min-w-0 text-xs font-medium truncate text-text-primary">
|
|
213
|
+
{session.title || "Untitled"}
|
|
214
|
+
</span>
|
|
215
|
+
{session.updatedAt && (
|
|
216
|
+
<span className="text-[10px] text-text-subtle shrink-0">
|
|
217
|
+
{formatRelativeDate(session.updatedAt)}
|
|
218
|
+
</span>
|
|
219
|
+
)}
|
|
220
|
+
<span
|
|
221
|
+
role="button"
|
|
222
|
+
tabIndex={0}
|
|
223
|
+
onClick={(e) => togglePin(e, session)}
|
|
224
|
+
className={`p-1 rounded transition-colors shrink-0 ${
|
|
225
|
+
session.pinned
|
|
226
|
+
? "text-primary hover:text-primary/70"
|
|
227
|
+
: "text-text-subtle can-hover:opacity-0 can-hover:group-hover:opacity-100 hover:text-text-primary"
|
|
228
|
+
}`}
|
|
229
|
+
aria-label={session.pinned ? "Unpin session" : "Pin session"}
|
|
230
|
+
>
|
|
231
|
+
{session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
212
232
|
</span>
|
|
213
|
-
|
|
233
|
+
</button>
|
|
234
|
+
</SessionContextMenu>
|
|
214
235
|
);
|
|
215
236
|
}
|
|
216
237
|
|
|
@@ -234,6 +255,12 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
234
255
|
})}
|
|
235
256
|
</div>
|
|
236
257
|
|
|
258
|
+
{activeProject && !loadingSessions && sessions.length > 0 && (
|
|
259
|
+
<div className="w-full max-w-sm">
|
|
260
|
+
<TagChipBar projectTags={projectTags} tagCounts={tagCounts} totalCount={sessions.length} selectedTagId={selectedTagId} onSelect={setSelectedTagId} />
|
|
261
|
+
</div>
|
|
262
|
+
)}
|
|
263
|
+
|
|
237
264
|
{activeProject && !loadingSessions && pinnedSessions.length > 0 && (
|
|
238
265
|
<div className="flex flex-col gap-2 w-full max-w-sm">
|
|
239
266
|
<p className="text-xs text-text-subtle text-center">Pinned</p>
|
|
@@ -20,6 +20,12 @@ import { useTabDrag } from "@/hooks/use-tab-drag";
|
|
|
20
20
|
import { useTouchTabDrag, wasTouchDragRecent } from "@/hooks/use-touch-tab-drag";
|
|
21
21
|
import { openCommandPalette } from "@/hooks/use-global-keybindings";
|
|
22
22
|
import { api, projectUrl } from "@/lib/api-client";
|
|
23
|
+
import { useProjectTags } from "@/components/chat/tag-filter-chips";
|
|
24
|
+
import {
|
|
25
|
+
ContextMenuSub, ContextMenuSubTrigger, ContextMenuSubContent,
|
|
26
|
+
ContextMenuItem, ContextMenuSeparator,
|
|
27
|
+
} from "@/components/ui/context-menu";
|
|
28
|
+
import { Tag, Check } from "lucide-react";
|
|
23
29
|
import { useNotificationStore, notificationColor } from "@/stores/notification-store";
|
|
24
30
|
import { useStreamingStore } from "@/stores/streaming-store";
|
|
25
31
|
import { useTabOverflow, getHiddenUnreadDirection } from "@/hooks/use-tab-overflow";
|
|
@@ -64,6 +70,37 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
64
70
|
useTabDrag(effectivePanelId);
|
|
65
71
|
const { handleTouchStart, handleTouchMove, handleTouchEnd } = useTouchTabDrag(effectivePanelId);
|
|
66
72
|
|
|
73
|
+
const { projectTags, loadTags } = useProjectTags(activeProject?.name);
|
|
74
|
+
const [sessionTagMap, setSessionTagMap] = useState<Record<string, { id: number; name: string; color: string }>>({});
|
|
75
|
+
|
|
76
|
+
// Fetch session tags for open chat tabs
|
|
77
|
+
const chatSessionIds = tabs.filter((t) => t.type === "chat" && t.metadata?.sessionId).map((t) => t.metadata!.sessionId as string);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
if (!activeProject?.name || chatSessionIds.length === 0) return;
|
|
80
|
+
api.get<{ sessions: { id: string; tag?: { id: number; name: string; color: string } | null }[] }>(
|
|
81
|
+
`${projectUrl(activeProject.name)}/chat/sessions?limit=50`,
|
|
82
|
+
).then((data) => {
|
|
83
|
+
const map: Record<string, { id: number; name: string; color: string }> = {};
|
|
84
|
+
for (const s of data.sessions) { if (s.tag) map[s.id] = s.tag; }
|
|
85
|
+
setSessionTagMap(map);
|
|
86
|
+
}).catch(() => {});
|
|
87
|
+
}, [activeProject?.name, chatSessionIds.join(",")]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
88
|
+
|
|
89
|
+
const assignTagToSession = useCallback(async (sessionId: string, tagId: number | null) => {
|
|
90
|
+
if (!activeProject?.name) return;
|
|
91
|
+
try {
|
|
92
|
+
if (tagId !== null) {
|
|
93
|
+
await api.patch(`${projectUrl(activeProject.name)}/chat/sessions/${sessionId}/tag`, { tagId });
|
|
94
|
+
const tag = projectTags.find((t) => t.id === tagId);
|
|
95
|
+
if (tag) setSessionTagMap((prev) => ({ ...prev, [sessionId]: { id: tag.id, name: tag.name, color: tag.color } }));
|
|
96
|
+
} else {
|
|
97
|
+
await api.del(`${projectUrl(activeProject.name)}/chat/sessions/${sessionId}/tag`);
|
|
98
|
+
setSessionTagMap((prev) => { const n = { ...prev }; delete n[sessionId]; return n; });
|
|
99
|
+
}
|
|
100
|
+
loadTags();
|
|
101
|
+
} catch { /* silent */ }
|
|
102
|
+
}, [activeProject?.name, projectTags, loadTags]);
|
|
103
|
+
|
|
67
104
|
const notifications = useNotificationStore((s) => s.notifications);
|
|
68
105
|
const streamingSessions = useStreamingStore((s) => s.sessions);
|
|
69
106
|
const { canScrollLeft, canScrollRight, scrollLeft: doScrollLeft, scrollRight: doScrollRight } =
|
|
@@ -219,6 +256,35 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
219
256
|
}}
|
|
220
257
|
onRename={tab.type === "chat" ? (title) => handleRenameTab(tab, title) : undefined}
|
|
221
258
|
onContextAction={(action) => handleTabContextAction(tab, action)}
|
|
259
|
+
tagColor={sessionId ? sessionTagMap[sessionId]?.color : undefined}
|
|
260
|
+
extraMenuContent={sessionId && projectTags.length > 0 ? (
|
|
261
|
+
<>
|
|
262
|
+
<ContextMenuSub>
|
|
263
|
+
<ContextMenuSubTrigger>
|
|
264
|
+
<Tag className="size-3.5 mr-2" />
|
|
265
|
+
Set Tag
|
|
266
|
+
</ContextMenuSubTrigger>
|
|
267
|
+
<ContextMenuSubContent>
|
|
268
|
+
{projectTags.map((pt) => (
|
|
269
|
+
<ContextMenuItem key={pt.id} onClick={() => assignTagToSession(sessionId, pt.id)}>
|
|
270
|
+
<span className="size-2.5 rounded-full mr-2 shrink-0" style={{ backgroundColor: pt.color }} />
|
|
271
|
+
{pt.name}
|
|
272
|
+
{sessionTagMap[sessionId]?.id === pt.id && <Check className="size-3 ml-auto" />}
|
|
273
|
+
</ContextMenuItem>
|
|
274
|
+
))}
|
|
275
|
+
{sessionTagMap[sessionId] && (
|
|
276
|
+
<>
|
|
277
|
+
<ContextMenuSeparator />
|
|
278
|
+
<ContextMenuItem onClick={() => assignTagToSession(sessionId, null)}>
|
|
279
|
+
Remove tag
|
|
280
|
+
</ContextMenuItem>
|
|
281
|
+
</>
|
|
282
|
+
)}
|
|
283
|
+
</ContextMenuSubContent>
|
|
284
|
+
</ContextMenuSub>
|
|
285
|
+
<ContextMenuSeparator />
|
|
286
|
+
</>
|
|
287
|
+
) : undefined}
|
|
222
288
|
/>
|
|
223
289
|
);
|
|
224
290
|
})}
|
|
@@ -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 {
|