@hienlh/ppm 0.13.12 → 0.13.13
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/.opencode/.env.example +98 -0
- package/.opencode/skills/ads-management/scripts/.env.example +13 -0
- package/.opencode/skills/ai-multimodal/.env.example +230 -0
- package/.opencode/skills/cip-design/.env.example +6 -0
- package/.opencode/skills/devops/.env.example +76 -0
- package/.opencode/skills/docs-seeker/.env.example +15 -0
- package/.opencode/skills/elevenlabs/.env.example +3 -0
- package/.opencode/skills/marketing-dashboard/.env.example +15 -0
- package/.opencode/skills/marketing-dashboard/app/.env.example +2 -0
- package/.opencode/skills/marketing-dashboard/server/.env.example +2 -0
- package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +70 -0
- package/.opencode/skills/mcp-management/scripts/dist/cli.js +160 -0
- package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +183 -0
- package/.opencode/skills/payment-integration/scripts/.env.example +20 -0
- package/.opencode/skills/sequential-thinking/.env.example +8 -0
- package/CHANGELOG.md +5 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +30 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-settings-section-ysK_Eixc.js → ai-settings-section-DR5BueEL.js} +1 -1
- package/dist/web/assets/{api-settings-D0_eiIYv.js → api-settings-DowGyuVy.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-7JKY4P1L.js +1 -0
- package/dist/web/assets/{audio-preview-BjoIjXlf.js → audio-preview-YOG6Biao.js} +1 -1
- package/dist/web/assets/chat-tab-DbdDJuLu.js +12 -0
- package/dist/web/assets/code-editor-C4nuAsy6.js +8 -0
- package/dist/web/assets/{conflict-editor-WxMZDucw.js → conflict-editor-DnGfriL5.js} +1 -1
- package/dist/web/assets/{csv-preview-7TsYBQI6.js → csv-preview-Bo-N3GHl.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-BjjuE4-G.js → data-grid-overlay-editor-DqcDQ9st.js} +1 -1
- package/dist/web/assets/{database-viewer-BRW8CMzC.js → database-viewer-AodppoTs.js} +1 -1
- package/dist/web/assets/diff-viewer-DykLUwna.js +4 -0
- package/dist/web/assets/{esm-zjerHxpO.js → esm-Dvc8oJly.js} +1 -1
- package/dist/web/assets/{extension-webview-DgfgR787.js → extension-webview-Bck7QuaB.js} +1 -1
- package/dist/web/assets/{file-store-BrbCNyLm.js → file-store-4BpOJthN.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Daf9rhiF.js +1 -0
- package/dist/web/assets/{glide-data-grid-DIvkBUKj.js → glide-data-grid-BVt0mwcA.js} +7 -7
- package/dist/web/assets/{image-preview-BlBVP277.js → image-preview-DaSmrIvY.js} +1 -1
- package/dist/web/assets/index-CSK33ACc.css +2 -0
- package/dist/web/assets/index-gZKF1YKy.js +27 -0
- package/dist/web/assets/info-3K5VOQVL-gn0pjNiT.js +1 -0
- package/dist/web/assets/{input-ozrR2DAV.js → input-4ll___Gh.js} +1 -1
- package/dist/web/assets/keybindings-store-DBKLTPrk.js +1 -0
- package/dist/web/assets/{markdown-renderer-CJMJ5Qq0.js → markdown-renderer-B1me_hz2.js} +3 -3
- package/dist/web/assets/{number-overlay-editor-BoRxunFN.js → number-overlay-editor-XTjjEXtk.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-Csaeizjc.js +1 -0
- package/dist/web/assets/{pdf-preview-D0JDPYYs.js → pdf-preview-Dci7TIL1.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-DatkjxTH.js +1 -0
- package/dist/web/assets/port-forwarding-tab-BeM40G-J.js +1 -0
- package/dist/web/assets/{postgres-viewer-DkVKzTKJ.js → postgres-viewer-CGVBOwA9.js} +3 -3
- package/dist/web/assets/radar-KQ55EAFF-BnGB20hR.js +1 -0
- package/dist/web/assets/{scroll-area-7H-Q_k8c.js → scroll-area-iv39O3VN.js} +1 -1
- package/dist/web/assets/search-tM8K5zWU.js +1 -0
- package/dist/web/assets/{settings-store-Dvk8Lvwm.js → settings-store-D2MtC9tm.js} +2 -2
- package/dist/web/assets/settings-tab-CYS8VfNl.js +1 -0
- package/dist/web/assets/{sql-query-editor-BVn40O0T.js → sql-query-editor-DstPySPF.js} +1 -1
- package/dist/web/assets/sqlite-viewer-SUGEk_G1.js +1 -0
- package/dist/web/assets/{tab-store-0rGchMXr.js → tab-store-Dow2Ztto.js} +1 -1
- package/dist/web/assets/terminal-tab-CJvjF79J.js +1 -0
- package/dist/web/assets/treemap-KZPCXAKY-CgEYv38e.js +1 -0
- package/dist/web/assets/{use-blob-url-e9uTXjv5.js → use-blob-url-DGY5qKiT.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-SyJzNaNN.js → use-monaco-theme-CugUkORI.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-DsfY6y4f.js → vendor-mermaid-CPtQ2zua.js} +3 -3
- package/dist/web/assets/{video-preview-BEuZs1dG.js → video-preview-gJSKmPQr.js} +1 -1
- package/dist/web/index.html +16 -15
- package/dist/web/sw.js +1 -1
- package/docs/system-architecture.md +1 -0
- package/package.json +1 -1
- package/src/cli/commands/cloud.ts +53 -0
- package/src/cli/commands/db-cmd.ts +56 -0
- package/src/index.ts +0 -0
- package/src/providers/claude-agent-sdk.ts +12 -15
- package/src/services/cloud.service.ts +55 -0
- package/src/services/sqlite.service.ts +9 -0
- package/src/web/app.tsx +7 -0
- package/src/web/components/chat/chat-welcome.tsx +7 -140
- package/src/web/components/chat/session-list-panel.tsx +188 -0
- package/src/web/components/editor/diff-viewer.tsx +25 -26
- package/src/web/components/layout/editor-panel.tsx +10 -158
- package/bun.lock +0 -2135
- package/bunfig.toml +0 -2
- package/dist/web/assets/architecture-PBZL5I3N-DVlAZGlv.js +0 -1
- package/dist/web/assets/chat-tab-Cq8xYO7K.js +0 -12
- package/dist/web/assets/code-editor-DZ1e_sz0.js +0 -8
- package/dist/web/assets/diff-viewer-BOTb0dkG.js +0 -4
- package/dist/web/assets/gitGraph-HDMCJU4V-b3n-Tgk6.js +0 -1
- package/dist/web/assets/index-COOnLKGB.css +0 -2
- package/dist/web/assets/index-zP-OjEml.js +0 -27
- package/dist/web/assets/info-3K5VOQVL-fJy9dGkV.js +0 -1
- package/dist/web/assets/keybindings-store-Djjc6tPj.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-DMi06dVb.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BECm43s6.js +0 -1
- package/dist/web/assets/port-forwarding-tab-DfaV6GPS.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-BwqCptkx.js +0 -1
- package/dist/web/assets/settings-tab-BFbe6ybw.js +0 -1
- package/dist/web/assets/sqlite-viewer-8oWf4JCB.js +0 -1
- package/dist/web/assets/terminal-tab-D_C7amDZ.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-Da7U3Olf.js +0 -1
- /package/dist/web/assets/{api-client-Dvzcc_EO.js → api-client-DIhJ5qVW.js} +0 -0
- /package/dist/web/assets/{data-grid-types-BTQHYBUh.js → data-grid-types-DqqspyVw.js} +0 -0
- /package/dist/web/assets/{dist-0kPgRaVx.js → dist-D1SZxtVS.js} +0 -0
- /package/dist/web/assets/{dist-DGSkE2Ml.js → dist-_jZs3YZC.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-Baz81y5z.js → file-exclamation-point-BwzaQ50n.js} +0 -0
- /package/dist/web/assets/{katex-BuytEdO1.js → katex-DzXRfQ_m.js} +0 -0
- /package/dist/web/assets/{lib-DQHnkzGy.js → lib-Dub8DlCJ.js} +0 -0
- /package/dist/web/assets/{react-GqWghJ-L.js → react-DMIOAtcX.js} +0 -0
- /package/dist/web/assets/{refresh-cw-LlbZDJpO.js → refresh-cw-BjrAbUJe.js} +0 -0
- /package/dist/web/assets/{sparkles-fWUT5Vzq.js → sparkles-CulWHe4c.js} +0 -0
- /package/dist/web/assets/{table-tf7pRkME.js → table-BzjWcs87.js} +0 -0
- /package/dist/web/assets/{text-wrap-BV-R4Vvy.js → text-wrap-DJz9Bgpa.js} +0 -0
- /package/dist/web/assets/{utils-CTg5uAYR.js → utils-CQux7CsO.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-CU2c3f0A.js → vendor-xterm-Dyfw49hJ.js} +0 -0
- /package/dist/web/assets/{x-CG-_0yIW.js → x-BPReZWnP.js} +0 -0
|
@@ -1,119 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { api, projectUrl } from "@/lib/api-client";
|
|
4
|
-
import { formatRelativeDate } from "@/lib/format-date";
|
|
5
|
-
import { useProjectTags, TagChipBar } from "./tag-filter-chips";
|
|
6
|
-
import { SessionContextMenu } from "./session-context-menu";
|
|
1
|
+
import { Bot } from "lucide-react";
|
|
2
|
+
import { SessionListPanel } from "./session-list-panel";
|
|
7
3
|
import type { SessionInfo } from "../../../types/chat";
|
|
8
4
|
|
|
9
|
-
const MAX_RECENT_SESSIONS = 5;
|
|
10
|
-
const FETCH_SESSIONS_LIMIT = 20;
|
|
11
|
-
|
|
12
5
|
interface ChatWelcomeProps {
|
|
13
6
|
projectName: string;
|
|
14
7
|
onSelectSession: (session: SessionInfo) => void;
|
|
15
8
|
}
|
|
16
9
|
|
|
17
10
|
export function ChatWelcome({ projectName, onSelectSession }: ChatWelcomeProps) {
|
|
18
|
-
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
19
|
-
const [loading, setLoading] = useState(false);
|
|
20
|
-
const [showAll, setShowAll] = useState(false);
|
|
21
|
-
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
|
|
22
|
-
const { projectTags, tagCounts, loadTags } = useProjectTags(projectName);
|
|
23
|
-
|
|
24
|
-
const loadSessions = useCallback(async () => {
|
|
25
|
-
if (!projectName) return;
|
|
26
|
-
setLoading(true);
|
|
27
|
-
try {
|
|
28
|
-
const data = await api.get<{ sessions: SessionInfo[]; hasMore: boolean }>(`${projectUrl(projectName)}/chat/sessions?limit=${FETCH_SESSIONS_LIMIT}`);
|
|
29
|
-
setSessions(data.sessions.slice(0, FETCH_SESSIONS_LIMIT));
|
|
30
|
-
} catch {
|
|
31
|
-
// silently ignore
|
|
32
|
-
} finally {
|
|
33
|
-
setLoading(false);
|
|
34
|
-
}
|
|
35
|
-
}, [projectName]);
|
|
36
|
-
|
|
37
|
-
useEffect(() => { loadSessions(); }, [loadSessions]);
|
|
38
|
-
|
|
39
|
-
const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
|
|
40
|
-
e.stopPropagation();
|
|
41
|
-
if (!projectName) return;
|
|
42
|
-
const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
|
|
43
|
-
try {
|
|
44
|
-
if (session.pinned) {
|
|
45
|
-
await api.del(url);
|
|
46
|
-
} else {
|
|
47
|
-
await api.put(url);
|
|
48
|
-
}
|
|
49
|
-
setSessions((prev) => {
|
|
50
|
-
const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
|
|
51
|
-
return updated.sort((a, b) => {
|
|
52
|
-
if (a.pinned && !b.pinned) return -1;
|
|
53
|
-
if (!a.pinned && b.pinned) return 1;
|
|
54
|
-
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
} catch {
|
|
58
|
-
// silently ignore
|
|
59
|
-
}
|
|
60
|
-
}, [projectName]);
|
|
61
|
-
|
|
62
|
-
const handleTagChanged = useCallback((sid: string, tag: { id: number; name: string; color: string } | null) => {
|
|
63
|
-
setSessions((prev) => prev.map((s) => s.id === sid ? { ...s, tag } : s));
|
|
64
|
-
loadTags();
|
|
65
|
-
}, [loadTags]);
|
|
66
|
-
|
|
67
|
-
const filtered = selectedTagId !== null ? sessions.filter((s) => s.tag?.id === selectedTagId) : sessions;
|
|
68
|
-
const pinnedSessions = filtered.filter((s) => s.pinned);
|
|
69
|
-
const allRecentSessions = filtered.filter((s) => !s.pinned);
|
|
70
|
-
const recentSessions = showAll ? allRecentSessions : allRecentSessions.slice(0, MAX_RECENT_SESSIONS);
|
|
71
|
-
const hasMore = allRecentSessions.length > MAX_RECENT_SESSIONS;
|
|
72
|
-
|
|
73
|
-
function renderSessionRow(session: SessionInfo) {
|
|
74
|
-
return (
|
|
75
|
-
<SessionContextMenu
|
|
76
|
-
key={session.id}
|
|
77
|
-
session={session}
|
|
78
|
-
projectName={projectName}
|
|
79
|
-
projectTags={projectTags}
|
|
80
|
-
onTogglePin={togglePin}
|
|
81
|
-
onTagChanged={handleTagChanged}
|
|
82
|
-
>
|
|
83
|
-
<button
|
|
84
|
-
onClick={() => onSelectSession(session)}
|
|
85
|
-
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"
|
|
86
|
-
>
|
|
87
|
-
<MessageSquare className="size-3.5 shrink-0 text-text-subtle" />
|
|
88
|
-
{session.tag && (
|
|
89
|
-
<span className="size-2 rounded-full shrink-0" style={{ backgroundColor: session.tag.color }} title={session.tag.name} />
|
|
90
|
-
)}
|
|
91
|
-
<span className="flex-1 min-w-0 text-xs font-medium truncate text-text-primary">
|
|
92
|
-
{session.title || "Untitled"}
|
|
93
|
-
</span>
|
|
94
|
-
{session.updatedAt && (
|
|
95
|
-
<span className="text-[10px] text-text-subtle shrink-0">
|
|
96
|
-
{formatRelativeDate(session.updatedAt)}
|
|
97
|
-
</span>
|
|
98
|
-
)}
|
|
99
|
-
<span
|
|
100
|
-
role="button"
|
|
101
|
-
tabIndex={0}
|
|
102
|
-
onClick={(e) => togglePin(e, session)}
|
|
103
|
-
className={`p-1 rounded transition-colors shrink-0 ${
|
|
104
|
-
session.pinned
|
|
105
|
-
? "text-primary hover:text-primary/70"
|
|
106
|
-
: "text-text-subtle can-hover:opacity-0 can-hover:group-hover:opacity-100 hover:text-text-primary"
|
|
107
|
-
}`}
|
|
108
|
-
aria-label={session.pinned ? "Unpin session" : "Pin session"}
|
|
109
|
-
>
|
|
110
|
-
{session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
111
|
-
</span>
|
|
112
|
-
</button>
|
|
113
|
-
</SessionContextMenu>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
11
|
return (
|
|
118
12
|
<div className="flex flex-col items-center justify-center h-full gap-6 text-text-secondary overflow-y-auto">
|
|
119
13
|
<div className="flex flex-col items-center gap-3">
|
|
@@ -121,38 +15,11 @@ export function ChatWelcome({ projectName, onSelectSession }: ChatWelcomeProps)
|
|
|
121
15
|
<p className="text-sm">Send a message to start a new conversation</p>
|
|
122
16
|
</div>
|
|
123
17
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
{!loading && pinnedSessions.length > 0 && (
|
|
131
|
-
<div className="flex flex-col gap-2 w-full max-w-sm px-4">
|
|
132
|
-
<p className="text-xs text-text-subtle text-center">Pinned</p>
|
|
133
|
-
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
134
|
-
{pinnedSessions.map(renderSessionRow)}
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
)}
|
|
138
|
-
|
|
139
|
-
{!loading && recentSessions.length > 0 && (
|
|
140
|
-
<div className="flex flex-col gap-2 w-full max-w-sm px-4">
|
|
141
|
-
<p className="text-xs text-text-subtle text-center">Recent chats</p>
|
|
142
|
-
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
143
|
-
{recentSessions.map(renderSessionRow)}
|
|
144
|
-
</div>
|
|
145
|
-
{hasMore && (
|
|
146
|
-
<button
|
|
147
|
-
onClick={() => setShowAll(!showAll)}
|
|
148
|
-
className="flex items-center justify-center gap-1 text-[11px] text-text-subtle hover:text-text-primary transition-colors py-1"
|
|
149
|
-
>
|
|
150
|
-
{showAll ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
|
|
151
|
-
{showAll ? "Show less" : `Show more (${allRecentSessions.length - MAX_RECENT_SESSIONS})`}
|
|
152
|
-
</button>
|
|
153
|
-
)}
|
|
154
|
-
</div>
|
|
155
|
-
)}
|
|
18
|
+
<SessionListPanel
|
|
19
|
+
projectName={projectName}
|
|
20
|
+
onSelectSession={onSelectSession}
|
|
21
|
+
className="w-full px-4"
|
|
22
|
+
/>
|
|
156
23
|
</div>
|
|
157
24
|
);
|
|
158
25
|
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { ChevronDown, ChevronUp, MessageSquare, Pin, PinOff, Search, X } from "lucide-react";
|
|
3
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
4
|
+
import { formatRelativeDate } from "@/lib/format-date";
|
|
5
|
+
import { useProjectTags, TagChipBar } from "./tag-filter-chips";
|
|
6
|
+
import { SessionContextMenu } from "./session-context-menu";
|
|
7
|
+
import type { SessionInfo, ProjectTag } from "../../../types/chat";
|
|
8
|
+
|
|
9
|
+
const MAX_RECENT_SESSIONS = 5;
|
|
10
|
+
const FETCH_SESSIONS_LIMIT = 20;
|
|
11
|
+
|
|
12
|
+
interface SessionListPanelProps {
|
|
13
|
+
projectName: string | undefined;
|
|
14
|
+
onSelectSession: (session: SessionInfo) => void;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function SessionListPanel({ projectName, onSelectSession, className }: SessionListPanelProps) {
|
|
19
|
+
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
20
|
+
const [loading, setLoading] = useState(false);
|
|
21
|
+
const [showAll, setShowAll] = useState(false);
|
|
22
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
23
|
+
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
|
|
24
|
+
const { projectTags, tagCounts, loadTags } = useProjectTags(projectName);
|
|
25
|
+
|
|
26
|
+
const loadSessions = useCallback(async () => {
|
|
27
|
+
if (!projectName) return;
|
|
28
|
+
setLoading(true);
|
|
29
|
+
try {
|
|
30
|
+
const data = await api.get<{ sessions: SessionInfo[]; hasMore: boolean }>(`${projectUrl(projectName)}/chat/sessions?limit=${FETCH_SESSIONS_LIMIT}`);
|
|
31
|
+
setSessions(data.sessions.slice(0, FETCH_SESSIONS_LIMIT));
|
|
32
|
+
} catch {
|
|
33
|
+
// silently ignore
|
|
34
|
+
} finally {
|
|
35
|
+
setLoading(false);
|
|
36
|
+
}
|
|
37
|
+
}, [projectName]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => { loadSessions(); }, [loadSessions]);
|
|
40
|
+
|
|
41
|
+
const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
if (!projectName) return;
|
|
44
|
+
const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
|
|
45
|
+
try {
|
|
46
|
+
if (session.pinned) {
|
|
47
|
+
await api.del(url);
|
|
48
|
+
} else {
|
|
49
|
+
await api.put(url);
|
|
50
|
+
}
|
|
51
|
+
setSessions((prev) => {
|
|
52
|
+
const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
|
|
53
|
+
return updated.sort((a, b) => {
|
|
54
|
+
if (a.pinned && !b.pinned) return -1;
|
|
55
|
+
if (!a.pinned && b.pinned) return 1;
|
|
56
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
} catch {
|
|
60
|
+
// silently ignore
|
|
61
|
+
}
|
|
62
|
+
}, [projectName]);
|
|
63
|
+
|
|
64
|
+
const handleTagChanged = useCallback((sid: string, tag: { id: number; name: string; color: string } | null) => {
|
|
65
|
+
setSessions((prev) => prev.map((s) => s.id === sid ? { ...s, tag } : s));
|
|
66
|
+
loadTags();
|
|
67
|
+
}, [loadTags]);
|
|
68
|
+
|
|
69
|
+
const query = searchQuery.toLowerCase().trim();
|
|
70
|
+
const filtered = sessions.filter((s) => {
|
|
71
|
+
if (selectedTagId !== null && s.tag?.id !== selectedTagId) return false;
|
|
72
|
+
if (query && !(s.title || "").toLowerCase().includes(query)) return false;
|
|
73
|
+
return true;
|
|
74
|
+
});
|
|
75
|
+
const pinnedSessions = filtered.filter((s) => s.pinned);
|
|
76
|
+
const allRecentSessions = filtered.filter((s) => !s.pinned);
|
|
77
|
+
const recentSessions = showAll ? allRecentSessions : allRecentSessions.slice(0, MAX_RECENT_SESSIONS);
|
|
78
|
+
const hasMore = allRecentSessions.length > MAX_RECENT_SESSIONS;
|
|
79
|
+
|
|
80
|
+
if (loading || !projectName || sessions.length === 0) return null;
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={className}>
|
|
84
|
+
<div className="relative">
|
|
85
|
+
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-text-subtle pointer-events-none" />
|
|
86
|
+
<input
|
|
87
|
+
type="text"
|
|
88
|
+
value={searchQuery}
|
|
89
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
90
|
+
placeholder="Search chats..."
|
|
91
|
+
className="w-full pl-8 pr-8 py-1.5 text-xs rounded-md border border-border bg-surface text-text-primary placeholder:text-text-subtle focus:outline-none focus:ring-1 focus:ring-primary/50"
|
|
92
|
+
/>
|
|
93
|
+
{searchQuery && (
|
|
94
|
+
<button onClick={() => setSearchQuery("")} className="absolute right-2 top-1/2 -translate-y-1/2 text-text-subtle hover:text-text-primary">
|
|
95
|
+
<X className="size-3.5" />
|
|
96
|
+
</button>
|
|
97
|
+
)}
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
<div className="mt-3">
|
|
101
|
+
<TagChipBar projectTags={projectTags} tagCounts={tagCounts} totalCount={sessions.length} selectedTagId={selectedTagId} onSelect={setSelectedTagId} />
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{pinnedSessions.length > 0 && (
|
|
105
|
+
<div className="flex flex-col gap-2 w-full mt-4">
|
|
106
|
+
<p className="text-xs text-text-subtle text-center">Pinned</p>
|
|
107
|
+
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
108
|
+
{pinnedSessions.map((s) => (
|
|
109
|
+
<SessionRow key={s.id} session={s} projectName={projectName} projectTags={projectTags} onSelect={onSelectSession} onTogglePin={togglePin} onTagChanged={handleTagChanged} />
|
|
110
|
+
))}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
{recentSessions.length > 0 && (
|
|
116
|
+
<div className="flex flex-col gap-2 w-full mt-4">
|
|
117
|
+
<p className="text-xs text-text-subtle text-center">Recent chats</p>
|
|
118
|
+
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
119
|
+
{recentSessions.map((s) => (
|
|
120
|
+
<SessionRow key={s.id} session={s} projectName={projectName} projectTags={projectTags} onSelect={onSelectSession} onTogglePin={togglePin} onTagChanged={handleTagChanged} />
|
|
121
|
+
))}
|
|
122
|
+
</div>
|
|
123
|
+
{hasMore && (
|
|
124
|
+
<button
|
|
125
|
+
onClick={() => setShowAll(!showAll)}
|
|
126
|
+
className="flex items-center justify-center gap-1 text-[11px] text-text-subtle hover:text-text-primary transition-colors py-1"
|
|
127
|
+
>
|
|
128
|
+
{showAll ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
|
|
129
|
+
{showAll ? "Show less" : `Show more (${allRecentSessions.length - MAX_RECENT_SESSIONS})`}
|
|
130
|
+
</button>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
interface SessionRowProps {
|
|
139
|
+
session: SessionInfo;
|
|
140
|
+
projectName: string;
|
|
141
|
+
projectTags: ProjectTag[];
|
|
142
|
+
onSelect: (session: SessionInfo) => void;
|
|
143
|
+
onTogglePin: (e: React.MouseEvent, session: SessionInfo) => void;
|
|
144
|
+
onTagChanged: (sid: string, tag: { id: number; name: string; color: string } | null) => void;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function SessionRow({ session, projectName, projectTags, onSelect, onTogglePin, onTagChanged }: SessionRowProps) {
|
|
148
|
+
return (
|
|
149
|
+
<SessionContextMenu
|
|
150
|
+
session={session}
|
|
151
|
+
projectName={projectName}
|
|
152
|
+
projectTags={projectTags}
|
|
153
|
+
onTogglePin={onTogglePin}
|
|
154
|
+
onTagChanged={onTagChanged}
|
|
155
|
+
>
|
|
156
|
+
<button
|
|
157
|
+
onClick={() => onSelect(session)}
|
|
158
|
+
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"
|
|
159
|
+
>
|
|
160
|
+
<MessageSquare className="size-3.5 shrink-0 text-text-subtle" />
|
|
161
|
+
{session.tag && (
|
|
162
|
+
<span className="size-2 rounded-full shrink-0" style={{ backgroundColor: session.tag.color }} title={session.tag.name} />
|
|
163
|
+
)}
|
|
164
|
+
<span className="flex-1 min-w-0 text-xs font-medium truncate text-text-primary">
|
|
165
|
+
{session.title || "Untitled"}
|
|
166
|
+
</span>
|
|
167
|
+
{session.updatedAt && (
|
|
168
|
+
<span className="text-[10px] text-text-subtle shrink-0">
|
|
169
|
+
{formatRelativeDate(session.updatedAt)}
|
|
170
|
+
</span>
|
|
171
|
+
)}
|
|
172
|
+
<span
|
|
173
|
+
role="button"
|
|
174
|
+
tabIndex={0}
|
|
175
|
+
onClick={(e) => onTogglePin(e, session)}
|
|
176
|
+
className={`p-1 rounded transition-colors shrink-0 ${
|
|
177
|
+
session.pinned
|
|
178
|
+
? "text-primary hover:text-primary/70"
|
|
179
|
+
: "text-text-subtle can-hover:opacity-0 can-hover:group-hover:opacity-100 hover:text-text-primary"
|
|
180
|
+
}`}
|
|
181
|
+
aria-label={session.pinned ? "Unpin session" : "Pin session"}
|
|
182
|
+
>
|
|
183
|
+
{session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
184
|
+
</span>
|
|
185
|
+
</button>
|
|
186
|
+
</SessionContextMenu>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -4,7 +4,7 @@ import { api, projectUrl } from "@/lib/api-client";
|
|
|
4
4
|
import { useShallow } from "zustand/react/shallow";
|
|
5
5
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
6
6
|
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
7
|
-
import { Loader2, FileCode,
|
|
7
|
+
import { Loader2, FileCode, WrapText } from "lucide-react";
|
|
8
8
|
|
|
9
9
|
function getMonacoLanguage(filename: string): string {
|
|
10
10
|
const ext = filename.split(".").pop()?.toLowerCase() ?? "";
|
|
@@ -41,12 +41,13 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
41
41
|
const [fullFileDiff, setFullFileDiff] = useState<{ original: string; modified: string } | null>(null);
|
|
42
42
|
const [loading, setLoading] = useState(!isInline);
|
|
43
43
|
const [error, setError] = useState<string | null>(null);
|
|
44
|
-
const [expandMode, setExpandMode] = useState<"both" | "left" | "right">("both");
|
|
45
44
|
const { wordWrap, toggleWordWrap } = useSettingsStore(useShallow((s) => ({ wordWrap: s.wordWrap, toggleWordWrap: s.toggleWordWrap })));
|
|
46
45
|
const monacoTheme = useMonacoTheme();
|
|
47
46
|
|
|
48
47
|
// Measure container height — Monaco needs explicit pixel height on mobile
|
|
49
48
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
49
|
+
const diffEditorRef = useRef<import("monaco-editor").editor.IStandaloneDiffEditor | null>(null);
|
|
50
|
+
const [editorReady, setEditorReady] = useState(false);
|
|
50
51
|
const [containerHeight, setContainerHeight] = useState<number | undefined>();
|
|
51
52
|
|
|
52
53
|
useEffect(() => {
|
|
@@ -125,7 +126,22 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
125
126
|
|
|
126
127
|
// Force inline on mobile (<768px) since side-by-side is too narrow
|
|
127
128
|
const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
|
|
128
|
-
const renderSideBySide = !isMobile
|
|
129
|
+
const renderSideBySide = !isMobile;
|
|
130
|
+
|
|
131
|
+
// Sync word wrap on both sub-editors.
|
|
132
|
+
// Monaco DiffEditor has a bug: during init when container width is 0,
|
|
133
|
+
// useInlineViewWhenSpaceIsLimited briefly triggers inline mode which sets
|
|
134
|
+
// wordWrapOverride2='off' on the original editor. When side-by-side resumes,
|
|
135
|
+
// wordWrapOverride2 is never cleared, permanently blocking word wrap on the
|
|
136
|
+
// left side. We disable that option and also force wordWrapOverride2 to clear it.
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
const editor = diffEditorRef.current;
|
|
139
|
+
if (!editor) return;
|
|
140
|
+
const val: "on" | "off" = isMobile ? "on" : wordWrap ? "on" : "off";
|
|
141
|
+
editor.updateOptions({ diffWordWrap: val });
|
|
142
|
+
editor.getOriginalEditor().updateOptions({ wordWrapOverride2: val } as any);
|
|
143
|
+
editor.getModifiedEditor().updateOptions({ wordWrapOverride2: val } as any);
|
|
144
|
+
}, [wordWrap, isMobile, editorReady]);
|
|
129
145
|
|
|
130
146
|
if (!projectName && !isInline) {
|
|
131
147
|
return (
|
|
@@ -166,28 +182,6 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
166
182
|
{/* Toolbar */}
|
|
167
183
|
{!isMobile && (
|
|
168
184
|
<div className="flex items-center justify-end gap-0.5 px-2 py-0.5 border-b border-border shrink-0">
|
|
169
|
-
<button type="button"
|
|
170
|
-
onClick={() => setExpandMode(expandMode === "left" ? "both" : "left")}
|
|
171
|
-
className={`p-1 rounded hover:bg-muted transition-colors ${expandMode === "left" ? "bg-muted text-foreground" : ""}`}
|
|
172
|
-
title="Expand original"
|
|
173
|
-
>
|
|
174
|
-
<PanelLeftOpen className="size-3.5" />
|
|
175
|
-
</button>
|
|
176
|
-
<button type="button"
|
|
177
|
-
onClick={() => setExpandMode("both")}
|
|
178
|
-
className={`p-1 rounded hover:bg-muted transition-colors ${expandMode === "both" ? "bg-muted text-foreground" : ""}`}
|
|
179
|
-
title="Side by side"
|
|
180
|
-
>
|
|
181
|
-
<Columns2 className="size-3.5" />
|
|
182
|
-
</button>
|
|
183
|
-
<button type="button"
|
|
184
|
-
onClick={() => setExpandMode(expandMode === "right" ? "both" : "right")}
|
|
185
|
-
className={`p-1 rounded hover:bg-muted transition-colors ${expandMode === "right" ? "bg-muted text-foreground" : ""}`}
|
|
186
|
-
title="Expand modified"
|
|
187
|
-
>
|
|
188
|
-
<PanelRightOpen className="size-3.5" />
|
|
189
|
-
</button>
|
|
190
|
-
<div className="w-px h-3.5 bg-border mx-0.5 shrink-0" />
|
|
191
185
|
<button type="button" onClick={toggleWordWrap} title="Toggle word wrap"
|
|
192
186
|
className={`p-1 rounded hover:bg-muted transition-colors ${wordWrap ? "bg-muted text-foreground" : ""}`}
|
|
193
187
|
>
|
|
@@ -204,11 +198,16 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
204
198
|
original={original}
|
|
205
199
|
modified={modified}
|
|
206
200
|
theme={monacoTheme}
|
|
201
|
+
onMount={(editor) => {
|
|
202
|
+
diffEditorRef.current = editor;
|
|
203
|
+
setEditorReady(true);
|
|
204
|
+
}}
|
|
207
205
|
options={{
|
|
208
206
|
fontSize: isMobile ? 11 : 13,
|
|
209
207
|
fontFamily: "Menlo, Monaco, Consolas, monospace",
|
|
210
|
-
|
|
208
|
+
diffWordWrap: isMobile ? "on" : wordWrap ? "on" : "off",
|
|
211
209
|
renderSideBySide,
|
|
210
|
+
useInlineViewWhenSpaceIsLimited: false,
|
|
212
211
|
readOnly: true,
|
|
213
212
|
automaticLayout: true,
|
|
214
213
|
scrollBeyondLastLine: false,
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import { Suspense, lazy,
|
|
2
|
-
import {
|
|
1
|
+
import { Suspense, lazy, useCallback } from "react";
|
|
2
|
+
import { Loader2, Terminal, MessageSquare, FilePlus } from "lucide-react";
|
|
3
3
|
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
|
-
import {
|
|
7
|
-
import { useProjectTags, TagChipBar } from "@/components/chat/tag-filter-chips";
|
|
8
|
-
import { SessionContextMenu } from "@/components/chat/session-context-menu";
|
|
6
|
+
import { SessionListPanel } from "@/components/chat/session-list-panel";
|
|
9
7
|
import type { SessionInfo } from "../../../types/chat";
|
|
10
8
|
import { TabBar } from "./tab-bar";
|
|
11
9
|
import { SplitDropOverlay } from "./split-drop-overlay";
|
|
@@ -87,72 +85,8 @@ export function EditorPanel({ panelId, projectName }: EditorPanelProps) {
|
|
|
87
85
|
);
|
|
88
86
|
}
|
|
89
87
|
|
|
90
|
-
function formatRelativeDate(iso: string): string {
|
|
91
|
-
try {
|
|
92
|
-
const date = new Date(iso);
|
|
93
|
-
const now = new Date();
|
|
94
|
-
const diffMs = now.getTime() - date.getTime();
|
|
95
|
-
const diffMin = Math.floor(diffMs / 60_000);
|
|
96
|
-
if (diffMin < 1) return "Just now";
|
|
97
|
-
if (diffMin < 60) return `${diffMin}m ago`;
|
|
98
|
-
const diffHr = Math.floor(diffMin / 60);
|
|
99
|
-
if (diffHr < 24) return `${diffHr}h ago`;
|
|
100
|
-
const diffDay = Math.floor(diffHr / 24);
|
|
101
|
-
if (diffDay < 7) return `${diffDay}d ago`;
|
|
102
|
-
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
103
|
-
} catch {
|
|
104
|
-
return "";
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const MAX_RECENT_SESSIONS = 5;
|
|
109
|
-
const FETCH_SESSIONS_LIMIT = 20;
|
|
110
|
-
|
|
111
88
|
function EmptyPanel({ panelId }: { panelId: string }) {
|
|
112
89
|
const activeProject = useProjectStore((s) => s.activeProject);
|
|
113
|
-
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
114
|
-
const [loadingSessions, setLoadingSessions] = useState(false);
|
|
115
|
-
const [showAll, setShowAll] = useState(false);
|
|
116
|
-
const [selectedTagId, setSelectedTagId] = useState<number | null>(null);
|
|
117
|
-
const { projectTags, tagCounts, loadTags } = useProjectTags(activeProject?.name);
|
|
118
|
-
|
|
119
|
-
const loadSessions = useCallback(async () => {
|
|
120
|
-
if (!activeProject?.name) return;
|
|
121
|
-
setLoadingSessions(true);
|
|
122
|
-
try {
|
|
123
|
-
const data = await api.get<{ sessions: SessionInfo[]; hasMore: boolean }>(`${projectUrl(activeProject.name)}/chat/sessions?limit=${FETCH_SESSIONS_LIMIT}`);
|
|
124
|
-
setSessions(data.sessions.slice(0, FETCH_SESSIONS_LIMIT));
|
|
125
|
-
} catch {
|
|
126
|
-
// silently ignore — empty state still functional without sessions
|
|
127
|
-
} finally {
|
|
128
|
-
setLoadingSessions(false);
|
|
129
|
-
}
|
|
130
|
-
}, [activeProject?.name]);
|
|
131
|
-
|
|
132
|
-
useEffect(() => { loadSessions(); }, [loadSessions]);
|
|
133
|
-
|
|
134
|
-
const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
|
|
135
|
-
e.stopPropagation();
|
|
136
|
-
if (!activeProject?.name) return;
|
|
137
|
-
const url = `${projectUrl(activeProject.name)}/chat/sessions/${session.id}/pin`;
|
|
138
|
-
try {
|
|
139
|
-
if (session.pinned) {
|
|
140
|
-
await api.del(url);
|
|
141
|
-
} else {
|
|
142
|
-
await api.put(url);
|
|
143
|
-
}
|
|
144
|
-
setSessions((prev) => {
|
|
145
|
-
const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
|
|
146
|
-
return updated.sort((a, b) => {
|
|
147
|
-
if (a.pinned && !b.pinned) return -1;
|
|
148
|
-
if (!a.pinned && b.pinned) return 1;
|
|
149
|
-
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
150
|
-
});
|
|
151
|
-
});
|
|
152
|
-
} catch {
|
|
153
|
-
// silently ignore
|
|
154
|
-
}
|
|
155
|
-
}, [activeProject?.name]);
|
|
156
90
|
|
|
157
91
|
function openTab(type: TabType) {
|
|
158
92
|
if (type === "editor") {
|
|
@@ -167,7 +101,7 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
167
101
|
);
|
|
168
102
|
}
|
|
169
103
|
|
|
170
|
-
|
|
104
|
+
const openSession = useCallback((session: SessionInfo) => {
|
|
171
105
|
usePanelStore.getState().openTab(
|
|
172
106
|
{
|
|
173
107
|
type: "chat",
|
|
@@ -178,62 +112,7 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
178
112
|
},
|
|
179
113
|
panelId,
|
|
180
114
|
);
|
|
181
|
-
}
|
|
182
|
-
|
|
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);
|
|
191
|
-
const recentSessions = showAll ? allRecentSessions : allRecentSessions.slice(0, MAX_RECENT_SESSIONS);
|
|
192
|
-
const hasMore = allRecentSessions.length > MAX_RECENT_SESSIONS;
|
|
193
|
-
|
|
194
|
-
function renderSessionRow(session: SessionInfo) {
|
|
195
|
-
return (
|
|
196
|
-
<SessionContextMenu
|
|
197
|
-
key={session.id}
|
|
198
|
-
session={session}
|
|
199
|
-
projectName={activeProject!.name}
|
|
200
|
-
projectTags={projectTags}
|
|
201
|
-
onTogglePin={togglePin}
|
|
202
|
-
onTagChanged={handleTagChanged}
|
|
203
|
-
>
|
|
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"
|
|
207
|
-
>
|
|
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" />}
|
|
232
|
-
</span>
|
|
233
|
-
</button>
|
|
234
|
-
</SessionContextMenu>
|
|
235
|
-
);
|
|
236
|
-
}
|
|
115
|
+
}, [activeProject?.name, panelId]);
|
|
237
116
|
|
|
238
117
|
return (
|
|
239
118
|
<div className="flex flex-col h-full overflow-y-auto text-text-secondary">
|
|
@@ -255,38 +134,11 @@ function EmptyPanel({ panelId }: { panelId: string }) {
|
|
|
255
134
|
})}
|
|
256
135
|
</div>
|
|
257
136
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
{activeProject && !loadingSessions && pinnedSessions.length > 0 && (
|
|
265
|
-
<div className="flex flex-col gap-2 w-full max-w-sm">
|
|
266
|
-
<p className="text-xs text-text-subtle text-center">Pinned</p>
|
|
267
|
-
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
268
|
-
{pinnedSessions.map(renderSessionRow)}
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
271
|
-
)}
|
|
272
|
-
|
|
273
|
-
{activeProject && !loadingSessions && recentSessions.length > 0 && (
|
|
274
|
-
<div className="flex flex-col gap-2 w-full max-w-sm">
|
|
275
|
-
<p className="text-xs text-text-subtle text-center">Recent chats</p>
|
|
276
|
-
<div className="w-full rounded-md border border-border bg-surface overflow-hidden">
|
|
277
|
-
{recentSessions.map(renderSessionRow)}
|
|
278
|
-
</div>
|
|
279
|
-
{hasMore && (
|
|
280
|
-
<button
|
|
281
|
-
onClick={() => setShowAll(!showAll)}
|
|
282
|
-
className="flex items-center justify-center gap-1 text-[11px] text-text-subtle hover:text-text-primary transition-colors py-1"
|
|
283
|
-
>
|
|
284
|
-
{showAll ? <ChevronUp className="size-3" /> : <ChevronDown className="size-3" />}
|
|
285
|
-
{showAll ? "Show less" : `Show more (${allRecentSessions.length - MAX_RECENT_SESSIONS})`}
|
|
286
|
-
</button>
|
|
287
|
-
)}
|
|
288
|
-
</div>
|
|
289
|
-
)}
|
|
137
|
+
<SessionListPanel
|
|
138
|
+
projectName={activeProject?.name}
|
|
139
|
+
onSelectSession={openSession}
|
|
140
|
+
className="w-full"
|
|
141
|
+
/>
|
|
290
142
|
</div>
|
|
291
143
|
</div>
|
|
292
144
|
);
|