@hienlh/ppm 0.9.79 → 0.9.80
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 +15 -0
- package/dist/web/assets/chat-tab-CmSLt4tg.js +10 -0
- package/dist/web/assets/{code-editor-kyaXcsZW.js → code-editor-BFe-hnpF.js} +1 -1
- package/dist/web/assets/{database-viewer-DmAux3OF.js → database-viewer-BeY2V5QI.js} +1 -1
- package/dist/web/assets/{diff-viewer-Cikon1YK.js → diff-viewer-D6xzs8PP.js} +1 -1
- package/dist/web/assets/{extension-webview-DVvC7SQ-.js → extension-webview-Cd1XYFXO.js} +1 -1
- package/dist/web/assets/{git-graph-Bon2J1_A.js → git-graph-D2XXpiMQ.js} +1 -1
- package/dist/web/assets/index-BtwsLrdT.css +2 -0
- package/dist/web/assets/index-D6_wwsL_.js +30 -0
- package/dist/web/assets/keybindings-store-C8ryKudw.js +1 -0
- package/dist/web/assets/{markdown-renderer-ttL1fRGG.js → markdown-renderer-xYMhd9cE.js} +1 -1
- package/dist/web/assets/{port-forwarding-tab-Bljq2XEH.js → port-forwarding-tab-B5rj_I66.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CqburCkJ.js → postgres-viewer-DnlqzOnm.js} +1 -1
- package/dist/web/assets/{settings-tab-CQVn8u_D.js → settings-tab-CNZpuPD3.js} +1 -1
- package/dist/web/assets/{sql-query-editor-DP6Kh2R8.js → sql-query-editor-Df2kzbPj.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-CrqzbhyF.js → sqlite-viewer-Cj1G70z4.js} +1 -1
- package/dist/web/assets/{terminal-tab-BmBB838x.js → terminal-tab-Dv9A7Xe2.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-ZmSrfclJ.js → use-monaco-theme-CPfIEo8t.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +33 -99
- package/src/providers/cli-provider-base.ts +1 -1
- package/src/server/routes/chat.ts +26 -22
- package/src/server/ws/chat.ts +11 -17
- package/src/services/config.service.ts +4 -3
- package/src/services/db.service.ts +67 -37
- package/src/services/ppmbot/ppmbot-streamer.ts +0 -6
- package/src/types/chat.ts +0 -1
- package/src/web/components/chat/chat-tab.tsx +11 -8
- package/src/web/components/layout/project-bar.tsx +133 -87
- package/src/web/hooks/use-chat.ts +0 -11
- package/AGENTS.md +0 -80
- package/dist/web/assets/chat-tab-B3gpx-qv.js +0 -10
- package/dist/web/assets/index-B_sM201v.css +0 -2
- package/dist/web/assets/index-Buc4QA5O.js +0 -30
- package/dist/web/assets/keybindings-store-CT_EvCrb.js +0 -1
- package/output/pdf/ppm-app-summary.pdf +0 -80
|
@@ -49,6 +49,9 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
49
49
|
(metadata?.permissionMode as string) ?? undefined,
|
|
50
50
|
);
|
|
51
51
|
|
|
52
|
+
// Pending message to send after WS connects (replaces unreliable setTimeout)
|
|
53
|
+
const pendingSendRef = useRef<{ content: string; permissionMode?: string } | null>(null);
|
|
54
|
+
|
|
52
55
|
// Drag-and-drop state
|
|
53
56
|
const [isDragging, setIsDragging] = useState(false);
|
|
54
57
|
const [externalFiles, setExternalFiles] = useState<File[] | null>(null);
|
|
@@ -92,7 +95,6 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
92
95
|
compactStatus,
|
|
93
96
|
statusMessage,
|
|
94
97
|
sessionTitle,
|
|
95
|
-
migratedSessionId,
|
|
96
98
|
sendMessage,
|
|
97
99
|
respondToApproval,
|
|
98
100
|
cancelStreaming,
|
|
@@ -104,12 +106,14 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
104
106
|
markTeamRead,
|
|
105
107
|
} = useChat(sessionId, providerId, projectName);
|
|
106
108
|
|
|
107
|
-
//
|
|
109
|
+
// Flush pending message once WS connects (replaces unreliable setTimeout)
|
|
108
110
|
useEffect(() => {
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
+
if (isConnected && pendingSendRef.current) {
|
|
112
|
+
const { content, permissionMode: pm } = pendingSendRef.current;
|
|
113
|
+
pendingSendRef.current = null;
|
|
114
|
+
sendMessage(content, { permissionMode: pm });
|
|
111
115
|
}
|
|
112
|
-
}, [
|
|
116
|
+
}, [isConnected, sendMessage]);
|
|
113
117
|
|
|
114
118
|
// Auto-clear notification badge when this tab is active and document is visible.
|
|
115
119
|
// Handles the case where notification arrived while browser tab was hidden.
|
|
@@ -222,9 +226,8 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
222
226
|
});
|
|
223
227
|
setSessionId(session.id);
|
|
224
228
|
setProviderId(session.providerId);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
}, 500);
|
|
229
|
+
// Queue message — will be sent by effect when WS reports isConnected
|
|
230
|
+
pendingSendRef.current = { content: fullContent, permissionMode };
|
|
228
231
|
return;
|
|
229
232
|
} catch (e) {
|
|
230
233
|
console.error("Failed to create session:", e);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useMemo, useRef, useEffect } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
|
-
import { Plus, Settings, Pencil, Trash2, Palette, Bug, Cloud, X } from "lucide-react";
|
|
3
|
+
import { Plus, Settings, Pencil, Trash2, Palette, Bug, Cloud, X, Copy } from "lucide-react";
|
|
4
4
|
import { CloudSharePopover } from "./cloud-share-popover";
|
|
5
5
|
import { openBugReportPopup } from "@/lib/report-bug";
|
|
6
6
|
import { useProjectStore, resolveOrder } from "@/stores/project-store";
|
|
@@ -23,7 +23,6 @@ import {
|
|
|
23
23
|
DialogFooter,
|
|
24
24
|
} from "@/components/ui/dialog";
|
|
25
25
|
import { AddProjectForm } from "@/components/layout/add-project-form";
|
|
26
|
-
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
|
27
26
|
import { useNotificationStore, selectProjectUrgentType, notificationColor } from "@/stores/notification-store";
|
|
28
27
|
import { cn } from "@/lib/utils";
|
|
29
28
|
|
|
@@ -110,6 +109,45 @@ export function ProjectBar() {
|
|
|
110
109
|
const [colorValue, setColorValue] = useState("");
|
|
111
110
|
const [colorSaving, setColorSaving] = useState(false);
|
|
112
111
|
|
|
112
|
+
// Hover expand (desktop only — ignored on touch devices)
|
|
113
|
+
const [expanded, setExpanded] = useState(false);
|
|
114
|
+
const enterTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
115
|
+
const leaveTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
116
|
+
const mouseInsideRef = useRef(false);
|
|
117
|
+
const contextMenuOpenRef = useRef(false);
|
|
118
|
+
const canHoverRef = useRef(
|
|
119
|
+
typeof window !== "undefined" && window.matchMedia("(hover: hover) and (pointer: fine)").matches,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
return () => {
|
|
124
|
+
if (enterTimerRef.current) clearTimeout(enterTimerRef.current);
|
|
125
|
+
if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current);
|
|
126
|
+
};
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
const handleBarMouseEnter = useCallback(() => {
|
|
130
|
+
if (!canHoverRef.current) return;
|
|
131
|
+
mouseInsideRef.current = true;
|
|
132
|
+
if (leaveTimerRef.current) { clearTimeout(leaveTimerRef.current); leaveTimerRef.current = null; }
|
|
133
|
+
enterTimerRef.current = setTimeout(() => setExpanded(true), 150);
|
|
134
|
+
}, []);
|
|
135
|
+
|
|
136
|
+
const handleBarMouseLeave = useCallback(() => {
|
|
137
|
+
if (!canHoverRef.current) return;
|
|
138
|
+
mouseInsideRef.current = false;
|
|
139
|
+
if (enterTimerRef.current) { clearTimeout(enterTimerRef.current); enterTimerRef.current = null; }
|
|
140
|
+
if (contextMenuOpenRef.current) return;
|
|
141
|
+
leaveTimerRef.current = setTimeout(() => setExpanded(false), 300);
|
|
142
|
+
}, []);
|
|
143
|
+
|
|
144
|
+
const handleCtxMenuChange = useCallback((open: boolean) => {
|
|
145
|
+
contextMenuOpenRef.current = open;
|
|
146
|
+
if (!open && !mouseInsideRef.current) {
|
|
147
|
+
leaveTimerRef.current = setTimeout(() => setExpanded(false), 300);
|
|
148
|
+
}
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
113
151
|
const openRename = useCallback((name: string) => {
|
|
114
152
|
setRenameTarget(name);
|
|
115
153
|
setRenameValue(name);
|
|
@@ -172,7 +210,15 @@ export function ProjectBar() {
|
|
|
172
210
|
}
|
|
173
211
|
|
|
174
212
|
return (
|
|
175
|
-
<
|
|
213
|
+
<div
|
|
214
|
+
className="hidden md:block relative w-[52px] min-w-[52px]"
|
|
215
|
+
onMouseEnter={handleBarMouseEnter}
|
|
216
|
+
onMouseLeave={handleBarMouseLeave}
|
|
217
|
+
>
|
|
218
|
+
<aside className={cn(
|
|
219
|
+
"absolute inset-y-0 left-0 flex flex-col bg-background border-r border-border overflow-hidden transition-[width] duration-200 ease-out",
|
|
220
|
+
expanded ? "w-[240px] shadow-lg z-30" : "w-[52px]",
|
|
221
|
+
)}>
|
|
176
222
|
{/* Logo + version */}
|
|
177
223
|
<div className="shrink-0 flex flex-col items-center justify-center h-[41px] border-b border-border gap-0.5">
|
|
178
224
|
<span className="text-[11px] font-bold text-primary leading-none">PPM</span>
|
|
@@ -182,47 +228,49 @@ export function ProjectBar() {
|
|
|
182
228
|
</div>
|
|
183
229
|
|
|
184
230
|
{/* Project avatar list */}
|
|
185
|
-
<div className="flex-1 overflow-y-auto py-2 flex flex-col
|
|
231
|
+
<div className={cn("flex-1 overflow-y-auto py-2 flex flex-col gap-2 min-h-0", expanded ? "items-stretch px-1.5" : "items-center")}>
|
|
186
232
|
{ordered.map((project, idx) => {
|
|
187
233
|
const color = resolveProjectColor(project.color, idx);
|
|
188
234
|
const isActive = activeProject?.name === project.name;
|
|
189
235
|
const isDragging = dragIdx === idx;
|
|
190
236
|
const isDropTarget = dropIdx === idx && dragIdx !== idx;
|
|
191
237
|
return (
|
|
192
|
-
<ContextMenu key={project.name}>
|
|
193
|
-
<
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
238
|
+
<ContextMenu key={project.name} onOpenChange={handleCtxMenuChange}>
|
|
239
|
+
<ContextMenuTrigger asChild>
|
|
240
|
+
<button
|
|
241
|
+
draggable
|
|
242
|
+
onDragStart={() => setDragIdx(idx)}
|
|
243
|
+
onDragOver={(e) => { e.preventDefault(); setDropIdx(idx); }}
|
|
244
|
+
onDragLeave={() => setDropIdx(null)}
|
|
245
|
+
onDragEnd={() => { setDragIdx(null); setDropIdx(null); }}
|
|
246
|
+
onDrop={() => {
|
|
247
|
+
if (dragIdx != null && dragIdx !== idx) {
|
|
248
|
+
const names = ordered.map((p) => p.name);
|
|
249
|
+
const [moved] = names.splice(dragIdx, 1);
|
|
250
|
+
names.splice(idx, 0, moved!);
|
|
251
|
+
reorderProjects(names);
|
|
252
|
+
}
|
|
253
|
+
setDragIdx(null);
|
|
254
|
+
setDropIdx(null);
|
|
255
|
+
}}
|
|
256
|
+
onClick={() => setActiveProject(project)}
|
|
257
|
+
className={cn(
|
|
258
|
+
"p-1 rounded-lg transition-all",
|
|
259
|
+
!expanded && "hover:bg-surface-elevated",
|
|
260
|
+
isDragging && "opacity-40 scale-90",
|
|
261
|
+
isDropTarget && "ring-2 ring-accent",
|
|
262
|
+
expanded && "w-full flex items-center gap-2 px-2 hover:bg-muted/50",
|
|
263
|
+
)}
|
|
264
|
+
>
|
|
265
|
+
<ProjectAvatar name={project.name} color={color} active={isActive} allNames={allNames} />
|
|
266
|
+
{expanded && (
|
|
267
|
+
<div className="min-w-0 text-left">
|
|
268
|
+
<p className="text-sm font-medium truncate">{project.name}</p>
|
|
269
|
+
<p className="text-[11px] text-text-subtle truncate [direction:rtl] text-left">{project.path}</p>
|
|
270
|
+
</div>
|
|
271
|
+
)}
|
|
272
|
+
</button>
|
|
273
|
+
</ContextMenuTrigger>
|
|
226
274
|
<ContextMenuContent>
|
|
227
275
|
<ContextMenuItem onClick={() => openRename(project.name)}>
|
|
228
276
|
<Pencil className="size-3.5 mr-2" /> Rename
|
|
@@ -230,6 +278,9 @@ export function ProjectBar() {
|
|
|
230
278
|
<ContextMenuItem onClick={() => openColor(project.name, color)}>
|
|
231
279
|
<Palette className="size-3.5 mr-2" /> Change Color
|
|
232
280
|
</ContextMenuItem>
|
|
281
|
+
<ContextMenuItem onClick={() => navigator.clipboard.writeText(project.path)}>
|
|
282
|
+
<Copy className="size-3.5 mr-2" /> Copy Path
|
|
283
|
+
</ContextMenuItem>
|
|
233
284
|
<ContextMenuSeparator />
|
|
234
285
|
<ContextMenuItem
|
|
235
286
|
className="text-destructive focus:text-destructive"
|
|
@@ -243,36 +294,32 @@ export function ProjectBar() {
|
|
|
243
294
|
})}
|
|
244
295
|
|
|
245
296
|
{/* Add project button */}
|
|
246
|
-
<
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
</
|
|
255
|
-
|
|
256
|
-
</Tooltip>
|
|
297
|
+
<button
|
|
298
|
+
onClick={handleAddProject}
|
|
299
|
+
className={cn(
|
|
300
|
+
"border-2 border-dashed border-border flex items-center justify-center text-text-subtle hover:border-primary hover:text-primary transition-colors",
|
|
301
|
+
expanded ? "w-full h-10 gap-2 rounded-lg px-2" : "size-10 rounded-full",
|
|
302
|
+
)}
|
|
303
|
+
>
|
|
304
|
+
<Plus className="size-4 shrink-0" />
|
|
305
|
+
{expanded && <span className="text-sm whitespace-nowrap">Add Project</span>}
|
|
306
|
+
</button>
|
|
257
307
|
</div>
|
|
258
308
|
|
|
259
309
|
{/* Footer: cloud + report bug + settings */}
|
|
260
|
-
<div className="shrink-0 flex flex-col
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
</TooltipTrigger>
|
|
274
|
-
<TooltipContent side="right">Cloud & Share</TooltipContent>
|
|
275
|
-
</Tooltip>
|
|
310
|
+
<div className={cn("shrink-0 flex flex-col gap-1 py-2 border-t border-border", expanded ? "items-stretch px-1.5" : "items-center")}>
|
|
311
|
+
<button
|
|
312
|
+
ref={cloudBtnRef}
|
|
313
|
+
onClick={() => setCloudOpen(!cloudOpen)}
|
|
314
|
+
className={cn(
|
|
315
|
+
"flex items-center rounded-md transition-colors",
|
|
316
|
+
expanded ? "w-full h-8 gap-2 px-2 justify-start" : "justify-center size-8",
|
|
317
|
+
cloudOpen ? "text-primary bg-primary/10" : "text-text-subtle hover:text-foreground hover:bg-surface-elevated",
|
|
318
|
+
)}
|
|
319
|
+
>
|
|
320
|
+
<Cloud className="size-4 shrink-0" />
|
|
321
|
+
{expanded && <span className="text-xs whitespace-nowrap">Cloud & Share</span>}
|
|
322
|
+
</button>
|
|
276
323
|
|
|
277
324
|
{/* Cloud popover — rendered via portal to escape overflow-hidden */}
|
|
278
325
|
{cloudOpen && popoverPos && createPortal(
|
|
@@ -288,28 +335,26 @@ export function ProjectBar() {
|
|
|
288
335
|
document.body,
|
|
289
336
|
)}
|
|
290
337
|
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
</
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
<TooltipContent side="right">Settings</TooltipContent>
|
|
312
|
-
</Tooltip>
|
|
338
|
+
<button
|
|
339
|
+
onClick={handleReportBug}
|
|
340
|
+
className={cn(
|
|
341
|
+
"flex items-center rounded-md text-text-subtle hover:text-foreground hover:bg-surface-elevated transition-colors",
|
|
342
|
+
expanded ? "w-full h-8 gap-2 px-2 justify-start" : "justify-center size-8",
|
|
343
|
+
)}
|
|
344
|
+
>
|
|
345
|
+
<Bug className="size-4 shrink-0" />
|
|
346
|
+
{expanded && <span className="text-xs whitespace-nowrap">Report Bug</span>}
|
|
347
|
+
</button>
|
|
348
|
+
<button
|
|
349
|
+
onClick={handleSettings}
|
|
350
|
+
className={cn(
|
|
351
|
+
"flex items-center rounded-md text-text-subtle hover:text-foreground hover:bg-surface-elevated transition-colors",
|
|
352
|
+
expanded ? "w-full h-8 gap-2 px-2 justify-start" : "justify-center size-8",
|
|
353
|
+
)}
|
|
354
|
+
>
|
|
355
|
+
<Settings className="size-4 shrink-0" />
|
|
356
|
+
{expanded && <span className="text-xs whitespace-nowrap">Settings</span>}
|
|
357
|
+
</button>
|
|
313
358
|
</div>
|
|
314
359
|
|
|
315
360
|
{/* Add project dialog */}
|
|
@@ -388,5 +433,6 @@ export function ProjectBar() {
|
|
|
388
433
|
</DialogContent>
|
|
389
434
|
</Dialog>
|
|
390
435
|
</aside>
|
|
436
|
+
</div>
|
|
391
437
|
);
|
|
392
438
|
}
|
|
@@ -44,8 +44,6 @@ interface UseChatReturn {
|
|
|
44
44
|
compactStatus: "compacting" | null;
|
|
45
45
|
statusMessage: string | null;
|
|
46
46
|
sessionTitle: string | null;
|
|
47
|
-
/** When CLI provider assigns a different session ID, this holds the new ID */
|
|
48
|
-
migratedSessionId: string | null;
|
|
49
47
|
/** Team activity state from WS events */
|
|
50
48
|
teamActivity: TeamActivityState;
|
|
51
49
|
/** All team messages (ref-backed, updated live) */
|
|
@@ -82,7 +80,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
82
80
|
const [statusMessage, setStatusMessage] = useState<string | null>(null);
|
|
83
81
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
84
82
|
const [isConnected, setIsConnected] = useState(false);
|
|
85
|
-
const [migratedSessionId, setMigratedSessionId] = useState<string | null>(null);
|
|
86
83
|
const streamingContentRef = useRef("");
|
|
87
84
|
const streamingEventsRef = useRef<ChatEvent[]>([]);
|
|
88
85
|
const streamingAccountRef = useRef<{ accountId: string; accountLabel: string } | null>(null);
|
|
@@ -356,13 +353,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
356
353
|
// Ignore keepalive pings
|
|
357
354
|
if ((data as any).type === "ping") return;
|
|
358
355
|
|
|
359
|
-
// Handle session ID migration (CLI provider assigned different ID)
|
|
360
|
-
if ((data as any).type === "session_migrated") {
|
|
361
|
-
const newId = (data as any).newSessionId as string;
|
|
362
|
-
if (newId) setMigratedSessionId(newId);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
|
|
366
356
|
// Handle title updates from SDK summary
|
|
367
357
|
if ((data as any).type === "title_updated") {
|
|
368
358
|
setSessionTitle((data as any).title ?? null);
|
|
@@ -671,7 +661,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
671
661
|
compactStatus,
|
|
672
662
|
statusMessage,
|
|
673
663
|
sessionTitle,
|
|
674
|
-
migratedSessionId,
|
|
675
664
|
teamActivity,
|
|
676
665
|
teamMessages,
|
|
677
666
|
markTeamRead,
|
package/AGENTS.md
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# AGENTS.md
|
|
2
|
-
|
|
3
|
-
## Project
|
|
4
|
-
|
|
5
|
-
PPM (Project & Process Manager) — a web-based IDE/project manager with AI chat powered by Codex Agent SDK.
|
|
6
|
-
|
|
7
|
-
## Stack
|
|
8
|
-
|
|
9
|
-
- **Runtime**: Bun
|
|
10
|
-
- **Backend**: Hono (HTTP) + Bun WebSocket
|
|
11
|
-
- **Frontend**: React + Vite + Tailwind + shadcn/ui
|
|
12
|
-
- **AI**: @anthropic-ai/Codex-agent-sdk
|
|
13
|
-
- **Tests**: bun:test
|
|
14
|
-
|
|
15
|
-
## Commands
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
bun dev:server # Start backend dev (port 8081, uses ~/.ppm/ppm.dev.db)
|
|
19
|
-
bun dev:web # Start Vite frontend (port 5173)
|
|
20
|
-
bun test # Run all tests
|
|
21
|
-
bun test tests/integration/ # Integration tests only
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
## Dev Config
|
|
25
|
-
|
|
26
|
-
Config is stored in **SQLite** (`~/.ppm/ppm.db`). Dev uses a separate DB:
|
|
27
|
-
|
|
28
|
-
- **Dev**: `~/.ppm/ppm.dev.db` — port **8081**
|
|
29
|
-
- **Production**: `~/.ppm/ppm.db` — port **8080**
|
|
30
|
-
|
|
31
|
-
`bun dev:server` automatically uses the dev database. On a new machine, run `ppm init` to create default config, then `ppm config set port 8081` for dev.
|
|
32
|
-
|
|
33
|
-
## Release Process
|
|
34
|
-
|
|
35
|
-
1. Commit feature/fix changes
|
|
36
|
-
2. Update `CHANGELOG.md` with all changes
|
|
37
|
-
3. Bump version in `package.json` — patch for small changes, minor/major for large ones
|
|
38
|
-
4. Commit: `chore: bump version to x.x.x`
|
|
39
|
-
5. Publish: `npm publish --access public`
|
|
40
|
-
|
|
41
|
-
## Quick SDK Tool Test
|
|
42
|
-
|
|
43
|
-
Use `test-tool.mjs` to verify SDK tool execution against any project cwd:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
bun test-tool.mjs /path/to/project # default: echo test
|
|
47
|
-
bun test-tool.mjs /path/to/project "dùng thử tool bash" # custom prompt
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
This uses `ClaudeAgentSdkProvider` directly — same env/settings overrides as production.
|
|
51
|
-
|
|
52
|
-
## Known Gotchas
|
|
53
|
-
|
|
54
|
-
- **SDK .env poisoning**: Projects with `ANTHROPIC_API_KEY` in `.env` break SDK tool execution. Provider neutralizes these vars. See `docs/lessons-learned.md`.
|
|
55
|
-
- **Project Codex settings**: `.Codex/settings.local.json` can restrict tools even with `bypassPermissions`. Provider overrides with empty settings.
|
|
56
|
-
|
|
57
|
-
## UI Rules
|
|
58
|
-
|
|
59
|
-
When creating or modifying any UI component, you MUST read and follow `docs/design-guidelines.md`, especially the **Mobile-First UI Rules** section. Key rules:
|
|
60
|
-
- Dialogs → bottom sheet on mobile (below `md:` breakpoint)
|
|
61
|
-
- No hover-only interactions — must have touch alternatives
|
|
62
|
-
- Touch targets minimum 44×44px
|
|
63
|
-
- Context menus → long-press on mobile, not tap
|
|
64
|
-
- Thumb zone: primary actions in bottom 1/3 of screen for one-handed use
|
|
65
|
-
- Always test both mobile and desktop layouts
|
|
66
|
-
|
|
67
|
-
## Roadmap & Context
|
|
68
|
-
|
|
69
|
-
Before planning or implementing a new feature, read `docs/project-roadmap.md` to understand:
|
|
70
|
-
- Which version the feature belongs to (v0.8, v0.9, v0.10, v1.0)
|
|
71
|
-
- The theme and scope of that version
|
|
72
|
-
- Dependencies between features
|
|
73
|
-
- Strategic principles (multi-device focus, extension architecture, tiered providers)
|
|
74
|
-
|
|
75
|
-
## Architecture
|
|
76
|
-
|
|
77
|
-
- `src/providers/Codex-agent-sdk.ts` — SDK integration, tool execution, streaming
|
|
78
|
-
- `src/server/ws/chat.ts` — WebSocket chat handler
|
|
79
|
-
- `src/web/hooks/use-chat.ts` — Frontend chat state management
|
|
80
|
-
- `src/services/config.service.ts` — Config from SQLite (`~/.ppm/ppm.db`)
|