@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.
- package/CHANGELOG.md +25 -0
- package/dist/web/assets/{api-client-ANLU-Irq.js → api-client-BxCvlogn.js} +1 -1
- package/dist/web/assets/chat-tab-AwRs7rWS.js +7 -0
- package/dist/web/assets/code-editor-BviTme00.js +1 -0
- package/dist/web/assets/diff-viewer-CCZM_VBl.js +4 -0
- package/dist/web/assets/git-graph-UCZZ6fX6.js +1 -0
- package/dist/web/assets/index-BxHR8fUA.css +2 -0
- package/dist/web/assets/index-yvVRZ65D.js +21 -0
- package/dist/web/assets/{input-D-F4ITU0.js → input-Bzyi1GeB.js} +1 -1
- package/dist/web/assets/{jsx-runtime-B4BJKQ1u.js → jsx-runtime-Bzk8w7Zh.js} +1 -1
- package/dist/web/assets/markdown-renderer-DzVh1Ft8.js +59 -0
- package/dist/web/assets/{rotate-ccw-BesidNnx.js → rotate-ccw-ZqeedZLA.js} +1 -1
- package/dist/web/assets/settings-store-DikslxSJ.js +1 -0
- package/dist/web/assets/settings-tab-C-AGuxll.js +1 -0
- package/dist/web/assets/tab-store-BNgVKR5w.js +1 -0
- package/dist/web/assets/terminal-tab-CnbdkUFt.js +36 -0
- package/dist/web/assets/{use-monaco-theme-CsNwoeyj.js → use-monaco-theme-BFv4d2_j.js} +2 -2
- package/dist/web/assets/{utils-bntUtdc7.js → utils-EM9hC5pN.js} +1 -1
- package/dist/web/index.html +8 -9
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/cli/commands/init.ts +2 -2
- package/src/cli/commands/status.ts +66 -1
- package/src/cli/commands/stop.ts +39 -2
- package/src/index.ts +4 -2
- package/src/providers/claude-agent-sdk.ts +0 -4
- package/src/server/index.ts +2 -1
- package/src/services/config.service.ts +11 -1
- package/src/web/components/chat/attachment-chips.tsx +1 -1
- package/src/web/components/chat/chat-history-bar.tsx +0 -3
- package/src/web/components/chat/message-input.tsx +13 -14
- package/src/web/components/chat/message-list.tsx +5 -4
- package/src/web/components/chat/tool-cards.tsx +3 -6
- package/src/web/components/editor/code-editor.tsx +2 -1
- package/src/web/components/editor/diff-viewer.tsx +43 -22
- package/src/web/components/explorer/file-tree.tsx +3 -3
- package/src/web/components/git/git-graph.tsx +2 -1
- package/src/web/components/git/git-status-panel.tsx +166 -89
- package/src/web/components/layout/command-palette.tsx +2 -1
- package/src/web/components/layout/mobile-drawer.tsx +2 -2
- package/src/web/components/layout/mobile-nav.tsx +1 -1
- package/src/web/components/layout/panel-layout.tsx +16 -16
- package/src/web/components/layout/split-drop-overlay.tsx +3 -3
- package/src/web/components/shared/markdown-renderer.tsx +16 -10
- package/src/web/hooks/use-terminal.ts +66 -23
- package/src/web/lib/utils.ts +5 -0
- package/src/web/stores/panel-store.ts +15 -14
- package/src/web/stores/panel-utils.ts +12 -10
- package/src/web/stores/settings-store.ts +1 -1
- package/dist/web/assets/chat-tab-CWBzraGA.js +0 -7
- package/dist/web/assets/code-editor-C4JSoO8E.js +0 -1
- package/dist/web/assets/diff-viewer-BdxT3tDC.js +0 -4
- package/dist/web/assets/git-graph-C7Rc_ZjF.js +0 -1
- package/dist/web/assets/index-DHOHCLrc.js +0 -21
- package/dist/web/assets/index-DhsWierF.css +0 -2
- package/dist/web/assets/markdown-renderer-Cv9PPnXe.js +0 -59
- package/dist/web/assets/react-WvgCEYPV.js +0 -1
- package/dist/web/assets/settings-store-CGtTcr8r.js +0 -1
- package/dist/web/assets/settings-tab-DYv7J4Vw.js +0 -1
- package/dist/web/assets/tab-store-Dq1kMOkJ.js +0 -1
- 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
|
-
|
|
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={
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
|
224
|
-
const name2 = 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
|
|
586
|
+
title: `Diff ${basename(file.path)}`,
|
|
586
587
|
closable: true,
|
|
587
588
|
metadata: {
|
|
588
589
|
projectName,
|