@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.
Files changed (38) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/web/assets/chat-tab-CmSLt4tg.js +10 -0
  3. package/dist/web/assets/{code-editor-kyaXcsZW.js → code-editor-BFe-hnpF.js} +1 -1
  4. package/dist/web/assets/{database-viewer-DmAux3OF.js → database-viewer-BeY2V5QI.js} +1 -1
  5. package/dist/web/assets/{diff-viewer-Cikon1YK.js → diff-viewer-D6xzs8PP.js} +1 -1
  6. package/dist/web/assets/{extension-webview-DVvC7SQ-.js → extension-webview-Cd1XYFXO.js} +1 -1
  7. package/dist/web/assets/{git-graph-Bon2J1_A.js → git-graph-D2XXpiMQ.js} +1 -1
  8. package/dist/web/assets/index-BtwsLrdT.css +2 -0
  9. package/dist/web/assets/index-D6_wwsL_.js +30 -0
  10. package/dist/web/assets/keybindings-store-C8ryKudw.js +1 -0
  11. package/dist/web/assets/{markdown-renderer-ttL1fRGG.js → markdown-renderer-xYMhd9cE.js} +1 -1
  12. package/dist/web/assets/{port-forwarding-tab-Bljq2XEH.js → port-forwarding-tab-B5rj_I66.js} +1 -1
  13. package/dist/web/assets/{postgres-viewer-CqburCkJ.js → postgres-viewer-DnlqzOnm.js} +1 -1
  14. package/dist/web/assets/{settings-tab-CQVn8u_D.js → settings-tab-CNZpuPD3.js} +1 -1
  15. package/dist/web/assets/{sql-query-editor-DP6Kh2R8.js → sql-query-editor-Df2kzbPj.js} +1 -1
  16. package/dist/web/assets/{sqlite-viewer-CrqzbhyF.js → sqlite-viewer-Cj1G70z4.js} +1 -1
  17. package/dist/web/assets/{terminal-tab-BmBB838x.js → terminal-tab-Dv9A7Xe2.js} +1 -1
  18. package/dist/web/assets/{use-monaco-theme-ZmSrfclJ.js → use-monaco-theme-CPfIEo8t.js} +1 -1
  19. package/dist/web/index.html +2 -2
  20. package/dist/web/sw.js +1 -1
  21. package/package.json +1 -1
  22. package/src/providers/claude-agent-sdk.ts +33 -99
  23. package/src/providers/cli-provider-base.ts +1 -1
  24. package/src/server/routes/chat.ts +26 -22
  25. package/src/server/ws/chat.ts +11 -17
  26. package/src/services/config.service.ts +4 -3
  27. package/src/services/db.service.ts +67 -37
  28. package/src/services/ppmbot/ppmbot-streamer.ts +0 -6
  29. package/src/types/chat.ts +0 -1
  30. package/src/web/components/chat/chat-tab.tsx +11 -8
  31. package/src/web/components/layout/project-bar.tsx +133 -87
  32. package/src/web/hooks/use-chat.ts +0 -11
  33. package/AGENTS.md +0 -80
  34. package/dist/web/assets/chat-tab-B3gpx-qv.js +0 -10
  35. package/dist/web/assets/index-B_sM201v.css +0 -2
  36. package/dist/web/assets/index-Buc4QA5O.js +0 -30
  37. package/dist/web/assets/keybindings-store-CT_EvCrb.js +0 -1
  38. 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
- // When CLI provider assigns a different session ID, update our state
109
+ // Flush pending message once WS connects (replaces unreliable setTimeout)
108
110
  useEffect(() => {
109
- if (migratedSessionId && migratedSessionId !== sessionId) {
110
- setSessionId(migratedSessionId);
111
+ if (isConnected && pendingSendRef.current) {
112
+ const { content, permissionMode: pm } = pendingSendRef.current;
113
+ pendingSendRef.current = null;
114
+ sendMessage(content, { permissionMode: pm });
111
115
  }
112
- }, [migratedSessionId]); // eslint-disable-line react-hooks/exhaustive-deps
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
- setTimeout(() => {
226
- sendMessage(fullContent, { permissionMode });
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
- <aside className="hidden md:flex flex-col w-[52px] min-w-[52px] bg-background border-r border-border overflow-hidden">
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 items-center gap-2 min-h-0">
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
- <Tooltip>
194
- <TooltipTrigger asChild>
195
- <ContextMenuTrigger asChild>
196
- <button
197
- draggable
198
- onDragStart={() => setDragIdx(idx)}
199
- onDragOver={(e) => { e.preventDefault(); setDropIdx(idx); }}
200
- onDragLeave={() => setDropIdx(null)}
201
- onDragEnd={() => { setDragIdx(null); setDropIdx(null); }}
202
- onDrop={() => {
203
- if (dragIdx != null && dragIdx !== idx) {
204
- const names = ordered.map((p) => p.name);
205
- const [moved] = names.splice(dragIdx, 1);
206
- names.splice(idx, 0, moved!);
207
- reorderProjects(names);
208
- }
209
- setDragIdx(null);
210
- setDropIdx(null);
211
- }}
212
- onClick={() => setActiveProject(project)}
213
- className={`p-1 rounded-lg hover:bg-surface-elevated transition-all ${
214
- isDragging ? "opacity-40 scale-90" : ""
215
- } ${isDropTarget ? "ring-2 ring-accent" : ""}`}
216
- >
217
- <ProjectAvatar name={project.name} color={color} active={isActive} allNames={allNames} />
218
- </button>
219
- </ContextMenuTrigger>
220
- </TooltipTrigger>
221
- <TooltipContent side="right" className="max-w-[200px]">
222
- <p className="font-medium">{project.name}</p>
223
- <p className="text-xs text-text-subtle truncate">{project.path}</p>
224
- </TooltipContent>
225
- </Tooltip>
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
- <Tooltip>
247
- <TooltipTrigger asChild>
248
- <button
249
- onClick={handleAddProject}
250
- className="size-10 rounded-full border-2 border-dashed border-border flex items-center justify-center text-text-subtle hover:border-primary hover:text-primary transition-colors"
251
- >
252
- <Plus className="size-4" />
253
- </button>
254
- </TooltipTrigger>
255
- <TooltipContent side="right">Add Project</TooltipContent>
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 items-center gap-1 py-2 border-t border-border">
261
- <Tooltip>
262
- <TooltipTrigger asChild>
263
- <button
264
- ref={cloudBtnRef}
265
- onClick={() => setCloudOpen(!cloudOpen)}
266
- className={cn(
267
- "flex items-center justify-center size-8 rounded-md transition-colors",
268
- cloudOpen ? "text-primary bg-primary/10" : "text-text-subtle hover:text-foreground hover:bg-surface-elevated",
269
- )}
270
- >
271
- <Cloud className="size-4" />
272
- </button>
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
- <Tooltip>
292
- <TooltipTrigger asChild>
293
- <button
294
- onClick={handleReportBug}
295
- className="flex items-center justify-center size-8 rounded-md text-text-subtle hover:text-foreground hover:bg-surface-elevated transition-colors"
296
- >
297
- <Bug className="size-4" />
298
- </button>
299
- </TooltipTrigger>
300
- <TooltipContent side="right">Report Bug</TooltipContent>
301
- </Tooltip>
302
- <Tooltip>
303
- <TooltipTrigger asChild>
304
- <button
305
- onClick={handleSettings}
306
- className="flex items-center justify-center size-8 rounded-md text-text-subtle hover:text-foreground hover:bg-surface-elevated transition-colors"
307
- >
308
- <Settings className="size-4" />
309
- </button>
310
- </TooltipTrigger>
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`)