@hienlh/ppm 0.2.20 → 0.2.21

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 (82) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/CLAUDE.md +18 -1
  3. package/bun.lock +57 -59
  4. package/dist/ppm +0 -0
  5. package/dist/web/assets/chat-tab-C_U7EwM9.js +6 -0
  6. package/dist/web/assets/code-editor-DuarTBEe.js +1 -0
  7. package/dist/web/assets/diff-viewer-sBWBgb7U.js +4 -0
  8. package/dist/web/assets/git-graph-fOKEZiot.js +1 -0
  9. package/dist/web/assets/index-3zt5mBwZ.css +2 -0
  10. package/dist/web/assets/index-CaUQy3Zs.js +21 -0
  11. package/dist/web/assets/input-CTnwfHVN.js +41 -0
  12. package/dist/web/assets/settings-tab-C5aWMqIA.js +1 -0
  13. package/dist/web/assets/{terminal-tab-sGlqTp7k.js → terminal-tab-BEFAYT4S.js} +1 -1
  14. package/dist/web/assets/use-monaco-theme-BxaccPmI.js +11 -0
  15. package/dist/web/index.html +35 -8
  16. package/dist/web/sw.js +1 -1
  17. package/docs/codebase-summary.md +13 -8
  18. package/docs/project-roadmap.md +22 -4
  19. package/docs/system-architecture.md +59 -0
  20. package/package.json +6 -14
  21. package/src/providers/claude-agent-sdk.ts +2 -2
  22. package/src/providers/registry.ts +12 -11
  23. package/src/server/routes/projects.ts +43 -0
  24. package/src/server/routes/settings.ts +42 -8
  25. package/src/server/ws/chat.ts +2 -2
  26. package/src/services/config.service.ts +5 -1
  27. package/src/services/project.service.ts +1 -0
  28. package/src/types/config.ts +37 -0
  29. package/src/types/project.ts +1 -0
  30. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/css.worker.bundle.js +54268 -0
  31. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/editor.worker.bundle.js +14316 -0
  32. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/html.worker.bundle.js +30452 -0
  33. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/json.worker.bundle.js +22095 -0
  34. package/src/web/Users/hienlh/Projects/ppm/dist/web/monacoeditorwork/ts.worker.bundle.js +225957 -0
  35. package/src/web/app.tsx +43 -5
  36. package/src/web/components/chat/chat-history-panel.tsx +106 -0
  37. package/src/web/components/chat/chat-tab.tsx +27 -19
  38. package/src/web/components/editor/code-editor.tsx +78 -197
  39. package/src/web/components/editor/diff-viewer.tsx +59 -176
  40. package/src/web/components/layout/add-project-form.tsx +151 -0
  41. package/src/web/components/layout/command-palette.tsx +3 -1
  42. package/src/web/components/layout/editor-panel.tsx +6 -4
  43. package/src/web/components/layout/mobile-drawer.tsx +48 -180
  44. package/src/web/components/layout/mobile-nav.tsx +89 -6
  45. package/src/web/components/layout/panel-layout.tsx +16 -10
  46. package/src/web/components/layout/project-bar.tsx +329 -0
  47. package/src/web/components/layout/project-bottom-sheet.tsx +345 -0
  48. package/src/web/components/layout/sidebar.tsx +56 -142
  49. package/src/web/components/layout/tab-bar.tsx +1 -6
  50. package/src/web/components/layout/tab-content.tsx +0 -10
  51. package/src/web/components/ui/dialog.tsx +1 -1
  52. package/src/web/lib/project-avatar.ts +45 -0
  53. package/src/web/lib/project-palette.ts +18 -0
  54. package/src/web/lib/use-monaco-theme.ts +29 -0
  55. package/src/web/stores/panel-store.ts +96 -9
  56. package/src/web/stores/project-store.ts +87 -3
  57. package/src/web/stores/settings-store.ts +31 -4
  58. package/src/web/stores/tab-store.ts +0 -2
  59. package/vite.config.ts +6 -2
  60. package/dist/web/assets/arrow-up-from-line-DjfWTP75.js +0 -1
  61. package/dist/web/assets/button-CQ5h5gxS.js +0 -41
  62. package/dist/web/assets/chat-tab-Cfw__7vJ.js +0 -6
  63. package/dist/web/assets/code-editor-D8Pz69sx.js +0 -2
  64. package/dist/web/assets/dialog-BL9i7XEo.js +0 -5
  65. package/dist/web/assets/diff-viewer-CWS5n7ur.js +0 -4
  66. package/dist/web/assets/dist-0XHv8Vwc.js +0 -1
  67. package/dist/web/assets/dist-Ca3N8Xbh.js +0 -46
  68. package/dist/web/assets/git-graph-DwA62J8-.js +0 -1
  69. package/dist/web/assets/git-status-panel-DaB-zzSF.js +0 -1
  70. package/dist/web/assets/index-BYIXPY6U.css +0 -2
  71. package/dist/web/assets/index-DbTCLiox.js +0 -17
  72. package/dist/web/assets/project-list-Z4lhtp6P.js +0 -1
  73. package/dist/web/assets/refresh-cw-S6I91MHO.js +0 -1
  74. package/dist/web/assets/settings-tab-BW6MGcir.js +0 -1
  75. package/dist/web/assets/trash-2-CGlFXde_.js +0 -1
  76. package/dist/web/assets/x-C0Rw5Giw.js +0 -1
  77. /package/dist/web/assets/{api-client-DzH9zCD7.js → api-client-BCjah751.js} +0 -0
  78. /package/dist/web/assets/{columns-2-DsiY76NQ.js → columns-2-DFQ3yid7.js} +0 -0
  79. /package/dist/web/assets/{copy-D_Q54D-v.js → copy-B-kLwqzg.js} +0 -0
  80. /package/dist/web/assets/{external-link-C6Y-D528.js → external-link-Dim3NH6h.js} +0 -0
  81. /package/dist/web/assets/{marked.esm-Cv8mjgnt.js → marked.esm-DhBtkBa8.js} +0 -0
  82. /package/dist/web/assets/{utils-D6me7KDg.js → utils-B-_GCz7E.js} +0 -0
package/src/web/app.tsx CHANGED
@@ -3,8 +3,10 @@ import { Toaster } from "@/components/ui/sonner";
3
3
  import { TooltipProvider } from "@/components/ui/tooltip";
4
4
  import { PanelLayout } from "@/components/layout/panel-layout";
5
5
  import { Sidebar } from "@/components/layout/sidebar";
6
+ import { ProjectBar } from "@/components/layout/project-bar";
6
7
  import { MobileNav } from "@/components/layout/mobile-nav";
7
8
  import { MobileDrawer } from "@/components/layout/mobile-drawer";
9
+ import { ProjectBottomSheet } from "@/components/layout/project-bottom-sheet";
8
10
  import { LoginScreen } from "@/components/auth/login-screen";
9
11
  import { useProjectStore } from "@/stores/project-store";
10
12
  import { useTabStore } from "@/stores/tab-store";
@@ -17,12 +19,17 @@ import { useUrlSync, parseUrlState } from "@/hooks/use-url-sync";
17
19
  import { useGlobalKeybindings } from "@/hooks/use-global-keybindings";
18
20
  import { useHealthCheck } from "@/hooks/use-health-check";
19
21
  import { CommandPalette } from "@/components/layout/command-palette";
22
+ import { cn } from "@/lib/utils";
20
23
 
21
24
  type AuthState = "checking" | "authenticated" | "unauthenticated";
22
25
 
23
26
  export function App() {
24
27
  const [authState, setAuthState] = useState<AuthState>("checking");
25
28
  const [drawerOpen, setDrawerOpen] = useState(false);
29
+ const [projectSheetOpen, setProjectSheetOpen] = useState(false);
30
+ const [mountedProjects, setMountedProjects] = useState<Set<string>>(
31
+ () => new Set(["__global__"]),
32
+ );
26
33
  const theme = useSettingsStore((s) => s.theme);
27
34
  const fetchProjects = useProjectStore((s) => s.fetchProjects);
28
35
  const fetchServerInfo = useSettingsStore((s) => s.fetchServerInfo);
@@ -113,6 +120,15 @@ export function App() {
113
120
  useTabStore.getState().switchProject(projectName);
114
121
  }, [activeProject?.name]);
115
122
 
123
+ // Keep-alive: mount workspace on first visit, never unmount
124
+ useEffect(() => {
125
+ const projectName = activeProject?.name ?? "__global__";
126
+ setMountedProjects((prev) => {
127
+ if (prev.has(projectName)) return prev;
128
+ return new Set([...prev, projectName]);
129
+ });
130
+ }, [activeProject?.name]);
131
+
116
132
  // On initial auth with no project selected, ensure a tab set exists
117
133
  useEffect(() => {
118
134
  if (authState === "authenticated" && !activeProject) {
@@ -138,22 +154,38 @@ export function App() {
138
154
  return <LoginScreen onSuccess={handleLoginSuccess} />;
139
155
  }
140
156
 
157
+ const activeProjectName = activeProject?.name ?? "__global__";
158
+
141
159
  return (
142
160
  <TooltipProvider>
143
161
  <div className="h-dvh flex flex-col bg-background text-foreground overflow-hidden">
144
162
  {/* Main layout */}
145
163
  <div className="flex flex-1 overflow-hidden">
164
+ {/* Desktop project bar (far left, non-collapsible) */}
165
+ <ProjectBar />
166
+
146
167
  {/* Desktop sidebar */}
147
168
  <Sidebar />
148
169
 
149
- {/* Content area */}
150
- <main className="flex-1 overflow-hidden pb-12 md:pb-0">
151
- <PanelLayout />
152
- </main>
170
+ {/* Content area — keep-alive per project */}
171
+ {[...mountedProjects].map((projectName) => (
172
+ <div
173
+ key={projectName}
174
+ className={cn(
175
+ "flex-1 overflow-hidden pb-12 md:pb-0",
176
+ activeProjectName !== projectName && "hidden",
177
+ )}
178
+ >
179
+ <PanelLayout projectName={projectName} />
180
+ </div>
181
+ ))}
153
182
  </div>
154
183
 
155
184
  {/* Mobile bottom nav */}
156
- <MobileNav onMenuPress={() => setDrawerOpen(true)} />
185
+ <MobileNav
186
+ onMenuPress={() => setDrawerOpen(true)}
187
+ onProjectsPress={() => setProjectSheetOpen(true)}
188
+ />
157
189
 
158
190
  {/* Mobile drawer overlay */}
159
191
  <MobileDrawer
@@ -161,6 +193,12 @@ export function App() {
161
193
  onClose={() => setDrawerOpen(false)}
162
194
  />
163
195
 
196
+ {/* Mobile project bottom sheet */}
197
+ <ProjectBottomSheet
198
+ isOpen={projectSheetOpen}
199
+ onClose={() => setProjectSheetOpen(false)}
200
+ />
201
+
164
202
  {/* Command palette (Shift+Shift) */}
165
203
  <CommandPalette open={paletteOpen} onClose={closePalette} />
166
204
 
@@ -0,0 +1,106 @@
1
+ import { useEffect, useState, useCallback } from "react";
2
+ import { MessageSquare, Loader2, RefreshCw } from "lucide-react";
3
+ import { api, projectUrl } from "@/lib/api-client";
4
+ import { useTabStore } from "@/stores/tab-store";
5
+ import type { SessionInfo } from "../../../types/chat";
6
+
7
+ interface ChatHistoryPanelProps {
8
+ projectName?: string;
9
+ }
10
+
11
+ function formatDate(iso: string): string {
12
+ try {
13
+ return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" });
14
+ } catch {
15
+ return "";
16
+ }
17
+ }
18
+
19
+ export function ChatHistoryPanel({ projectName }: ChatHistoryPanelProps) {
20
+ const [sessions, setSessions] = useState<SessionInfo[]>([]);
21
+ const [loading, setLoading] = useState(false);
22
+ const [error, setError] = useState<string | null>(null);
23
+ const openTab = useTabStore((s) => s.openTab);
24
+
25
+ const load = useCallback(async () => {
26
+ if (!projectName) return;
27
+ setLoading(true);
28
+ setError(null);
29
+ try {
30
+ const data = await api.get<SessionInfo[]>(`${projectUrl(projectName)}/chat/sessions`);
31
+ setSessions(data);
32
+ } catch (e) {
33
+ setError(e instanceof Error ? e.message : "Failed to load sessions");
34
+ } finally {
35
+ setLoading(false);
36
+ }
37
+ }, [projectName]);
38
+
39
+ useEffect(() => { load(); }, [load]);
40
+
41
+ if (!projectName) {
42
+ return (
43
+ <div className="flex items-center justify-center h-32 text-xs text-text-subtle px-4 text-center">
44
+ Select a project to view chat history
45
+ </div>
46
+ );
47
+ }
48
+
49
+ if (loading) {
50
+ return (
51
+ <div className="flex items-center justify-center h-24">
52
+ <Loader2 className="size-4 animate-spin text-text-subtle" />
53
+ </div>
54
+ );
55
+ }
56
+
57
+ if (error) {
58
+ return (
59
+ <div className="flex flex-col items-center gap-2 p-4 text-xs text-text-subtle">
60
+ <span>{error}</span>
61
+ <button onClick={load} className="flex items-center gap-1 hover:text-text-secondary transition-colors">
62
+ <RefreshCw className="size-3" /> Retry
63
+ </button>
64
+ </div>
65
+ );
66
+ }
67
+
68
+ if (sessions.length === 0) {
69
+ return (
70
+ <div className="flex flex-col items-center justify-center h-32 gap-2 text-xs text-text-subtle">
71
+ <MessageSquare className="size-5" />
72
+ <span>No chat sessions yet</span>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ function openSession(session: SessionInfo) {
78
+ openTab({
79
+ type: "chat",
80
+ title: session.title || "Chat",
81
+ projectId: projectName ?? null,
82
+ metadata: { projectName, sessionId: session.id },
83
+ closable: true,
84
+ });
85
+ }
86
+
87
+ return (
88
+ <div className="flex flex-col">
89
+ {sessions.map((session) => (
90
+ <button
91
+ key={session.id}
92
+ onClick={() => openSession(session)}
93
+ className="flex items-start gap-2 px-3 py-2 text-left hover:bg-surface-elevated transition-colors border-b border-border/50 last:border-0"
94
+ >
95
+ <MessageSquare className="size-3.5 shrink-0 mt-0.5 text-text-subtle" />
96
+ <div className="flex-1 min-w-0">
97
+ <p className="text-xs font-medium truncate text-text-primary">{session.title || "Untitled"}</p>
98
+ {session.updatedAt && (
99
+ <p className="text-[10px] text-text-subtle">{formatDate(session.updatedAt)}</p>
100
+ )}
101
+ </div>
102
+ </button>
103
+ ))}
104
+ </div>
105
+ );
106
+ }
@@ -4,12 +4,11 @@ import { api, projectUrl } from "@/lib/api-client";
4
4
  import { useChat } from "@/hooks/use-chat";
5
5
  import { useUsage } from "@/hooks/use-usage";
6
6
  import { useTabStore } from "@/stores/tab-store";
7
- import { useProjectStore } from "@/stores/project-store";
8
7
  import { useSettingsStore } from "@/stores/settings-store";
9
8
  import { buildBugReport, openGithubIssue, copyToClipboard } from "@/lib/report-bug";
10
9
  import { MessageList } from "./message-list";
11
10
  import { MessageInput, type ChatAttachment } from "./message-input";
12
- import { SessionPicker } from "./session-picker";
11
+ import { Bot } from "lucide-react";
13
12
  import { SlashCommandPicker, type SlashItem } from "./slash-command-picker";
14
13
  import { FilePicker } from "./file-picker";
15
14
  import { UsageBadge, UsageDetailPanel } from "./usage-badge";
@@ -53,13 +52,25 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
53
52
  const [externalFiles, setExternalFiles] = useState<File[] | null>(null);
54
53
  const dragCounterRef = useRef(0);
55
54
 
56
- const activeProject = useProjectStore((s) => s.activeProject);
55
+ // Use tab's own project, not global activeProject (keep-alive: hidden tabs must not react to switches)
56
+ const projectName = (metadata?.projectName as string) ?? "";
57
57
  const updateTab = useTabStore((s) => s.updateTab);
58
58
  const version = useSettingsStore((s) => s.version);
59
59
 
60
+ // Fetch AI model name
61
+ const [modelName, setModelName] = useState<string>("");
62
+ useEffect(() => {
63
+ api.get<{ default_provider: string; providers: Record<string, { model?: string }> }>("/api/settings/ai")
64
+ .then((ai) => {
65
+ const provider = ai.providers[ai.default_provider];
66
+ setModelName(provider?.model ?? ai.default_provider);
67
+ })
68
+ .catch(() => {});
69
+ }, []);
70
+
60
71
  // Usage runs independently — auto-refreshes on interval
61
72
  const { usageInfo, usageLoading, lastUpdatedAt, refreshUsage, mergeUsage } =
62
- useUsage(activeProject?.name ?? "", providerId);
73
+ useUsage(projectName, providerId);
63
74
 
64
75
  // Persist sessionId and providerId to tab metadata so reload restores the session
65
76
  useEffect(() => {
@@ -80,18 +91,17 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
80
91
  reconnect,
81
92
  refetchMessages,
82
93
  isConnected,
83
- } = useChat(sessionId, providerId, activeProject?.name ?? "", { onUsageEvent: mergeUsage });
94
+ } = useChat(sessionId, providerId, projectName, { onUsageEvent: mergeUsage });
84
95
 
85
96
  const handleNewSession = useCallback(() => {
86
- const projectName = activeProject?.name ?? null;
87
97
  useTabStore.getState().openTab({
88
98
  type: "chat",
89
99
  title: "AI Chat",
90
100
  metadata: { projectName },
91
- projectId: projectName,
101
+ projectId: projectName || null,
92
102
  closable: true,
93
103
  });
94
- }, [activeProject?.name]);
104
+ }, [projectName]);
95
105
 
96
106
  const handleSelectSession = useCallback((session: SessionInfo) => {
97
107
  setSessionId(session.id);
@@ -127,7 +137,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
127
137
 
128
138
  if (!sessionId) {
129
139
  try {
130
- const pName = activeProject?.name ?? (metadata?.project as string) ?? "";
140
+ const pName = projectName;
131
141
  const session = await api.post<Session>(`${projectUrl(pName)}/chat/sessions`, {
132
142
  providerId,
133
143
  title: content.slice(0, 50),
@@ -145,7 +155,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
145
155
  }
146
156
  sendMessage(fullContent);
147
157
  },
148
- [sessionId, providerId, metadata?.project, sendMessage, buildMessageWithAttachments, activeProject?.name],
158
+ [sessionId, providerId, projectName, sendMessage, buildMessageWithAttachments],
149
159
  );
150
160
 
151
161
  // --- Slash picker handlers ---
@@ -243,19 +253,17 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
243
253
  pendingApproval={pendingApproval}
244
254
  onApprovalResponse={respondToApproval}
245
255
  isStreaming={isStreaming}
246
- projectName={activeProject?.name}
256
+ projectName={projectName}
247
257
  />
248
258
 
249
259
  {/* Bottom toolbar */}
250
260
  <div className="border-t border-border bg-background shrink-0">
251
261
  {/* Session bar */}
252
262
  <div className="flex items-center justify-between px-3 py-1.5 border-b border-border/50">
253
- <SessionPicker
254
- currentSessionId={sessionId}
255
- onSelectSession={handleSelectSession}
256
- onNewSession={handleNewSession}
257
- projectName={activeProject?.name}
258
- />
263
+ <div className="flex items-center gap-1.5 text-xs text-text-secondary px-1">
264
+ <Bot className="size-3.5" />
265
+ <span className="truncate max-w-[180px]">{modelName || "AI"}</span>
266
+ </div>
259
267
  <div className="flex items-center gap-2">
260
268
  <UsageBadge
261
269
  usage={usageInfo}
@@ -265,7 +273,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
265
273
  {sessionId && (
266
274
  <button
267
275
  onClick={async () => {
268
- const text = await buildBugReport(version, { sessionId, projectName: activeProject?.name });
276
+ const text = await buildBugReport(version, { sessionId, projectName: projectName });
269
277
  setBugReportText(text);
270
278
  setCopied(false);
271
279
  }}
@@ -323,7 +331,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
323
331
  onSend={handleSend}
324
332
  isStreaming={isStreaming}
325
333
  onCancel={cancelStreaming}
326
- projectName={activeProject?.name}
334
+ projectName={projectName}
327
335
  onSlashStateChange={handleSlashStateChange}
328
336
  onSlashItemsLoaded={setSlashItems}
329
337
  slashSelected={slashSelected}