@hienlh/ppm 0.5.2 → 0.5.3

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 (61) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/web/assets/{api-client-ANLU-Irq.js → api-client-BxCvlogn.js} +1 -1
  3. package/dist/web/assets/chat-tab-AwRs7rWS.js +7 -0
  4. package/dist/web/assets/code-editor-BviTme00.js +1 -0
  5. package/dist/web/assets/diff-viewer-CCZM_VBl.js +4 -0
  6. package/dist/web/assets/git-graph-UCZZ6fX6.js +1 -0
  7. package/dist/web/assets/index-BxHR8fUA.css +2 -0
  8. package/dist/web/assets/index-yvVRZ65D.js +21 -0
  9. package/dist/web/assets/{input-D-F4ITU0.js → input-Bzyi1GeB.js} +1 -1
  10. package/dist/web/assets/{jsx-runtime-B4BJKQ1u.js → jsx-runtime-Bzk8w7Zh.js} +1 -1
  11. package/dist/web/assets/markdown-renderer-DzVh1Ft8.js +59 -0
  12. package/dist/web/assets/{rotate-ccw-BesidNnx.js → rotate-ccw-ZqeedZLA.js} +1 -1
  13. package/dist/web/assets/settings-store-DikslxSJ.js +1 -0
  14. package/dist/web/assets/settings-tab-C-AGuxll.js +1 -0
  15. package/dist/web/assets/tab-store-BNgVKR5w.js +1 -0
  16. package/dist/web/assets/terminal-tab-CnbdkUFt.js +36 -0
  17. package/dist/web/assets/{use-monaco-theme-CsNwoeyj.js → use-monaco-theme-BFv4d2_j.js} +2 -2
  18. package/dist/web/assets/{utils-bntUtdc7.js → utils-EM9hC5pN.js} +1 -1
  19. package/dist/web/index.html +8 -9
  20. package/dist/web/sw.js +1 -1
  21. package/package.json +1 -1
  22. package/src/cli/commands/init.ts +2 -2
  23. package/src/cli/commands/status.ts +66 -1
  24. package/src/cli/commands/stop.ts +39 -2
  25. package/src/index.ts +4 -2
  26. package/src/providers/claude-agent-sdk.ts +0 -4
  27. package/src/server/index.ts +2 -1
  28. package/src/services/config.service.ts +11 -1
  29. package/src/web/components/chat/attachment-chips.tsx +1 -1
  30. package/src/web/components/chat/chat-history-bar.tsx +0 -3
  31. package/src/web/components/chat/message-input.tsx +13 -14
  32. package/src/web/components/chat/message-list.tsx +5 -4
  33. package/src/web/components/chat/tool-cards.tsx +3 -6
  34. package/src/web/components/editor/code-editor.tsx +2 -1
  35. package/src/web/components/editor/diff-viewer.tsx +43 -22
  36. package/src/web/components/explorer/file-tree.tsx +3 -3
  37. package/src/web/components/git/git-graph.tsx +2 -1
  38. package/src/web/components/git/git-status-panel.tsx +166 -89
  39. package/src/web/components/layout/command-palette.tsx +2 -1
  40. package/src/web/components/layout/mobile-drawer.tsx +2 -2
  41. package/src/web/components/layout/mobile-nav.tsx +1 -1
  42. package/src/web/components/layout/panel-layout.tsx +16 -16
  43. package/src/web/components/layout/split-drop-overlay.tsx +3 -3
  44. package/src/web/components/shared/markdown-renderer.tsx +16 -10
  45. package/src/web/hooks/use-terminal.ts +66 -23
  46. package/src/web/lib/utils.ts +5 -0
  47. package/src/web/stores/panel-store.ts +15 -14
  48. package/src/web/stores/panel-utils.ts +12 -10
  49. package/src/web/stores/settings-store.ts +1 -1
  50. package/dist/web/assets/chat-tab-CWBzraGA.js +0 -7
  51. package/dist/web/assets/code-editor-C4JSoO8E.js +0 -1
  52. package/dist/web/assets/diff-viewer-BdxT3tDC.js +0 -4
  53. package/dist/web/assets/git-graph-C7Rc_ZjF.js +0 -1
  54. package/dist/web/assets/index-DHOHCLrc.js +0 -21
  55. package/dist/web/assets/index-DhsWierF.css +0 -2
  56. package/dist/web/assets/markdown-renderer-Cv9PPnXe.js +0 -59
  57. package/dist/web/assets/react-WvgCEYPV.js +0 -1
  58. package/dist/web/assets/settings-store-CGtTcr8r.js +0 -1
  59. package/dist/web/assets/settings-tab-DYv7J4Vw.js +0 -1
  60. package/dist/web/assets/tab-store-Dq1kMOkJ.js +0 -1
  61. package/dist/web/assets/terminal-tab-BeYE7Lrg.js +0 -36
@@ -57,6 +57,7 @@ export function MessageInput({
57
57
  const [value, setValue] = useState(initialValue ?? "");
58
58
  const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
59
59
  const textareaRef = useRef<HTMLTextAreaElement>(null);
60
+ const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
60
61
  const fileInputRef = useRef<HTMLInputElement>(null);
61
62
  const slashItemsRef = useRef<SlashItem[]>([]);
62
63
  const fileItemsRef = useRef<FileNode[]>([]);
@@ -238,7 +239,7 @@ export function MessageInput({
238
239
  );
239
240
  });
240
241
  }
241
- textareaRef.current?.focus();
242
+ (mobileTextareaRef.current ?? textareaRef.current)?.focus();
242
243
  },
243
244
  [uploadFile],
244
245
  );
@@ -266,9 +267,8 @@ export function MessageInput({
266
267
  if (att.previewUrl) URL.revokeObjectURL(att.previewUrl);
267
268
  }
268
269
  setAttachments([]);
269
- if (textareaRef.current) {
270
- textareaRef.current.style.height = "auto";
271
- }
270
+ if (textareaRef.current) textareaRef.current.style.height = "auto";
271
+ if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
272
272
  }, [value, attachments, disabled, onSend, onSlashStateChange, onFileStateChange]);
273
273
 
274
274
  const handleKeyDown = useCallback(
@@ -320,8 +320,8 @@ export function MessageInput({
320
320
  [updatePickerState],
321
321
  );
322
322
 
323
- const handleInput = useCallback(() => {
324
- const el = textareaRef.current;
323
+ const handleInput = useCallback((e?: React.ChangeEvent<HTMLTextAreaElement>) => {
324
+ const el = e?.target ?? textareaRef.current;
325
325
  if (!el) return;
326
326
  el.style.height = "auto";
327
327
  el.style.height = Math.min(el.scrollHeight, 160) + "px";
@@ -382,14 +382,13 @@ export function MessageInput({
382
382
 
383
383
  return (
384
384
  <div className="p-2 md:p-3 bg-background">
385
- {/* Attachment chips (above input) */}
386
- <AttachmentChips attachments={attachments} onRemove={removeAttachment} />
387
-
388
385
  {/* Rounded input container */}
389
386
  <div
390
387
  className="border border-border rounded-xl md:rounded-2xl bg-surface shadow-sm cursor-text"
391
- onClick={() => !disabled && textareaRef.current?.focus()}
388
+ onClick={() => !disabled && (mobileTextareaRef.current ?? textareaRef.current)?.focus()}
392
389
  >
390
+ {/* Attachment chips (inside container, aligned with input) */}
391
+ <AttachmentChips attachments={attachments} onRemove={removeAttachment} />
393
392
  {/* Mobile: single row — attach + textarea + send */}
394
393
  <div className="flex items-end gap-1 md:hidden px-2 py-2">
395
394
  <button
@@ -402,9 +401,9 @@ export function MessageInput({
402
401
  <Paperclip className="size-4" />
403
402
  </button>
404
403
  <textarea
405
- ref={textareaRef}
404
+ ref={mobileTextareaRef}
406
405
  value={value}
407
- onChange={(e) => { handleChange(e.target.value); handleInput(); }}
406
+ onChange={(e) => { handleChange(e.target.value); handleInput(e); }}
408
407
  onKeyDown={handleKeyDown}
409
408
  onPaste={handlePaste}
410
409
  onDrop={handleDrop}
@@ -412,7 +411,7 @@ export function MessageInput({
412
411
  placeholder={isStreaming ? "Follow-up..." : "Ask anything..."}
413
412
  disabled={disabled}
414
413
  rows={1}
415
- className="flex-1 resize-none bg-transparent py-1.5 text-sm text-foreground placeholder:text-text-subtle focus:outline-none disabled:opacity-50 max-h-32"
414
+ className="flex-1 resize-none bg-transparent py-1.5 text-sm text-foreground placeholder:text-text-subtle focus:outline-none disabled:opacity-50 max-h-20"
416
415
  />
417
416
  {showCancel ? (
418
417
  <button
@@ -439,7 +438,7 @@ export function MessageInput({
439
438
  <textarea
440
439
  ref={textareaRef}
441
440
  value={value}
442
- onChange={(e) => { handleChange(e.target.value); handleInput(); }}
441
+ onChange={(e) => { handleChange(e.target.value); handleInput(e); }}
443
442
  onKeyDown={handleKeyDown}
444
443
  onPaste={handlePaste}
445
444
  onDrop={handleDrop}
@@ -5,6 +5,7 @@ import type { ChatMessage, ChatEvent } from "../../../types/chat";
5
5
  import type { StreamingStatus } from "@/hooks/use-chat";
6
6
  import { ToolCard } from "./tool-cards";
7
7
  import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
8
+ import { basename } from "@/lib/utils";
8
9
 
9
10
  import {
10
11
  AlertCircle,
@@ -164,7 +165,7 @@ function parseUserAttachments(content: string): { files: string[]; text: string
164
165
 
165
166
  /** Build a preview URL for an uploaded file (served from /chat/uploads/:filename) */
166
167
  function uploadPreviewUrl(filePath: string, projectName?: string): string {
167
- const filename = filePath.split("/").pop() ?? "";
168
+ const filename = basename(filePath);
168
169
  // Use a generic project name — the upload route is project-scoped but files are global
169
170
  return `/api/project/${encodeURIComponent(projectName ?? "_")}/chat/uploads/${encodeURIComponent(filename)}`;
170
171
  }
@@ -195,13 +196,13 @@ function UserBubble({ content, projectName, onFork }: { content: string; project
195
196
  <AuthImage
196
197
  key={i}
197
198
  src={uploadPreviewUrl(filePath, projectName)}
198
- alt={filePath.split("/").pop() ?? "image"}
199
+ alt={basename(filePath) || "image"}
199
200
  />
200
201
  ) : isPdfPath(filePath) ? (
201
202
  <AuthFileLink
202
203
  key={i}
203
204
  src={uploadPreviewUrl(filePath, projectName)}
204
- filename={filePath.split("/").pop() ?? "document.pdf"}
205
+ filename={basename(filePath) || "document.pdf"}
205
206
  mimeType="application/pdf"
206
207
  />
207
208
  ) : (
@@ -210,7 +211,7 @@ function UserBubble({ content, projectName, onFork }: { content: string; project
210
211
  className="flex items-center gap-1.5 rounded-md border border-border bg-background/50 px-2 py-1 text-xs text-text-secondary"
211
212
  >
212
213
  <FileText className="size-3.5 shrink-0" />
213
- <span className="truncate max-w-40">{filePath.split("/").pop()}</span>
214
+ <span className="truncate max-w-40">{basename(filePath)}</span>
214
215
  </div>
215
216
  ),
216
217
  )}
@@ -21,6 +21,7 @@ import {
21
21
  } from "lucide-react";
22
22
  import type { ChatEvent } from "../../../types/chat";
23
23
  import { useTabStore } from "@/stores/tab-store";
24
+ import { basename } from "@/lib/utils";
24
25
 
25
26
  /** Extract tool name and input from a ChatEvent */
26
27
  function extractToolInfo(tool: ChatEvent): { toolName: string; input: Record<string, unknown> } {
@@ -165,7 +166,7 @@ function ToolDetails({
165
166
  if (!projectName) return;
166
167
  openTab({
167
168
  type: "editor",
168
- title: filePath.split("/").pop() ?? filePath,
169
+ title: basename(filePath),
169
170
  metadata: { filePath, projectName },
170
171
  projectId: projectName,
171
172
  closable: true,
@@ -176,7 +177,7 @@ function ToolDetails({
176
177
  const openEditDiff = (filePath: string, oldStr: string, newStr: string) => {
177
178
  openTab({
178
179
  type: "git-diff",
179
- title: `Diff ${filePath.split("/").pop() ?? filePath}`,
180
+ title: `Diff ${basename(filePath)}`,
180
181
  metadata: { filePath, projectName, original: oldStr, modified: newStr },
181
182
  projectId: projectName ?? null,
182
183
  closable: true,
@@ -453,10 +454,6 @@ function MiniMarkdown({ content, maxHeight = "max-h-48" }: { content: string; ma
453
454
  return <MarkdownRenderer content={content} className={`text-text-secondary overflow-auto ${maxHeight}`} />;
454
455
  }
455
456
 
456
- function basename(path?: string): string {
457
- if (!path) return "";
458
- return path.split("/").pop() ?? path;
459
- }
460
457
 
461
458
  function truncate(str?: string, max = 50): string {
462
459
  if (!str) return "";
@@ -5,6 +5,7 @@ import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
5
5
  import { api, projectUrl, getAuthToken } from "@/lib/api-client";
6
6
  import { useTabStore } from "@/stores/tab-store";
7
7
  import { useSettingsStore } from "@/stores/settings-store";
8
+ import { basename } from "@/lib/utils";
8
9
  import { useMonacoTheme } from "@/lib/use-monaco-theme";
9
10
  import { Loader2, FileWarning, ExternalLink, Code, Eye, WrapText } from "lucide-react";
10
11
 
@@ -91,7 +92,7 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
91
92
  // Update tab title unsaved indicator
92
93
  useEffect(() => {
93
94
  if (!ownTab) return;
94
- const baseName = filePath?.split("/").pop() ?? "Untitled";
95
+ const baseName = filePath ? basename(filePath) : "Untitled";
95
96
  const newTitle = unsaved ? `${baseName} \u25CF` : baseName;
96
97
  if (ownTab.title !== newTitle) updateTab(ownTab.id, { title: newTitle });
97
98
  }, [unsaved]); // eslint-disable-line react-hooks/exhaustive-deps
@@ -1,4 +1,4 @@
1
- import { useEffect, useState, useMemo } from "react";
1
+ import { useEffect, useState, useMemo, useRef } from "react";
2
2
  import { DiffEditor } from "@monaco-editor/react";
3
3
  import { api, projectUrl } from "@/lib/api-client";
4
4
  import { useSettingsStore } from "@/stores/settings-store";
@@ -43,6 +43,20 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
43
43
  const { wordWrap, toggleWordWrap } = useSettingsStore();
44
44
  const monacoTheme = useMonacoTheme();
45
45
 
46
+ // Measure container height — Monaco needs explicit pixel height on mobile
47
+ const containerRef = useRef<HTMLDivElement>(null);
48
+ const [containerHeight, setContainerHeight] = useState<number | undefined>();
49
+
50
+ useEffect(() => {
51
+ const el = containerRef.current;
52
+ if (!el) return;
53
+ const ro = new ResizeObserver(([entry]) => {
54
+ if (entry) setContainerHeight(Math.floor(entry.contentRect.height));
55
+ });
56
+ ro.observe(el);
57
+ return () => ro.disconnect();
58
+ }, []);
59
+
46
60
  useEffect(() => {
47
61
  if (isInline) return;
48
62
  if (!projectName) return;
@@ -92,6 +106,10 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
92
106
  return langFile ? getMonacoLanguage(langFile) : "plaintext";
93
107
  }, [filePath, file1, file2]);
94
108
 
109
+ // Force inline on mobile (<768px) since side-by-side is too narrow
110
+ const isMobile = typeof window !== "undefined" && window.innerWidth < 768;
111
+ const renderSideBySide = !isMobile && expandMode === "both";
112
+
95
113
  if (!projectName && !isInline) {
96
114
  return (
97
115
  <div className="flex items-center justify-center h-full text-muted-foreground text-sm">
@@ -125,9 +143,6 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
125
143
  );
126
144
  }
127
145
 
128
- // expandMode left/right → inline diff (Monaco has no single-side mode)
129
- const renderSideBySide = expandMode === "both";
130
-
131
146
  const expandToggle = (
132
147
  <div className="flex items-center gap-0.5 shrink-0">
133
148
  <button type="button"
@@ -163,24 +178,30 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
163
178
  return (
164
179
  <div className="flex flex-col h-full">
165
180
  {/* Monaco DiffEditor */}
166
- <div className="flex-1 overflow-hidden">
167
- <DiffEditor
168
- height="100%"
169
- language={language}
170
- original={original}
171
- modified={modified}
172
- theme={monacoTheme}
173
- options={{
174
- fontSize: 13,
175
- fontFamily: "Menlo, Monaco, Consolas, monospace",
176
- wordWrap: wordWrap ? "on" : "off",
177
- renderSideBySide,
178
- readOnly: true,
179
- automaticLayout: true,
180
- scrollBeyondLastLine: false,
181
- }}
182
- loading={<Loader2 className="size-5 animate-spin text-text-subtle" />}
183
- />
181
+ <div ref={containerRef} className="flex-1 overflow-hidden">
182
+ {containerHeight && containerHeight > 0 ? (
183
+ <DiffEditor
184
+ height={containerHeight}
185
+ language={language}
186
+ original={original}
187
+ modified={modified}
188
+ theme={monacoTheme}
189
+ options={{
190
+ fontSize: isMobile ? 11 : 13,
191
+ fontFamily: "Menlo, Monaco, Consolas, monospace",
192
+ wordWrap: isMobile ? "on" : wordWrap ? "on" : "off",
193
+ renderSideBySide,
194
+ readOnly: true,
195
+ automaticLayout: true,
196
+ scrollBeyondLastLine: false,
197
+ }}
198
+ loading={<Loader2 className="size-5 animate-spin text-text-subtle" />}
199
+ />
200
+ ) : (
201
+ <div className="flex items-center justify-center h-full">
202
+ <Loader2 className="size-5 animate-spin text-text-subtle" />
203
+ </div>
204
+ )}
184
205
  </div>
185
206
  </div>
186
207
  );
@@ -14,7 +14,7 @@ import {
14
14
  import { useFileStore, type FileNode } from "@/stores/file-store";
15
15
  import { useProjectStore } from "@/stores/project-store";
16
16
  import { useTabStore } from "@/stores/tab-store";
17
- import { cn } from "@/lib/utils";
17
+ import { cn, basename } from "@/lib/utils";
18
18
  import { ScrollArea } from "@/components/ui/scroll-area";
19
19
  import {
20
20
  ContextMenu,
@@ -220,8 +220,8 @@ export function FileTree({ onFileOpen }: FileTreeProps = {}) {
220
220
  if (action === "compare-selected" && selectedFiles.length === 2) {
221
221
  const file1 = selectedFiles[0]!;
222
222
  const file2 = selectedFiles[1]!;
223
- const name1 = file1.split("/").pop() ?? file1;
224
- const name2 = file2.split("/").pop() ?? file2;
223
+ const name1 = basename(file1);
224
+ const name2 = basename(file2);
225
225
  openTab({
226
226
  type: "git-diff",
227
227
  title: `Compare ${name1} vs ${name2}`,
@@ -14,6 +14,7 @@ import {
14
14
  GripVertical,
15
15
  } from "lucide-react";
16
16
  import { api, projectUrl } from "@/lib/api-client";
17
+ import { basename } from "@/lib/utils";
17
18
  import { useTabStore } from "@/stores/tab-store";
18
19
  import { Button } from "@/components/ui/button";
19
20
  import { ScrollArea } from "@/components/ui/scroll-area";
@@ -582,7 +583,7 @@ export function GitGraph({ metadata }: GitGraphProps) {
582
583
  className="flex items-center gap-2 py-0.5 text-xs hover:bg-muted/50 rounded px-1 cursor-pointer"
583
584
  onClick={() => openTab({
584
585
  type: "git-diff",
585
- title: `Diff ${file.path.split("/").pop()}`,
586
+ title: `Diff ${basename(file.path)}`,
586
587
  closable: true,
587
588
  metadata: {
588
589
  projectName,