@hienlh/ppm 0.8.59 → 0.8.60
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 +7 -0
- package/dist/web/assets/chat-tab-C5H74y2z.js +7 -0
- package/dist/web/assets/{code-editor-DgTfBijB.js → code-editor-DMw26mUm.js} +1 -1
- package/dist/web/assets/{database-viewer-DSlQhR7c.js → database-viewer-gnj_8u4T.js} +1 -1
- package/dist/web/assets/{diff-viewer-C5A-ZnrC.js → diff-viewer-DVqfhdBN.js} +1 -1
- package/dist/web/assets/{git-graph-B5QR_Cf-.js → git-graph-CJy7tOAJ.js} +1 -1
- package/dist/web/assets/index-BAioKo_2.css +2 -0
- package/dist/web/assets/index-Dg6TQ3Iu.js +37 -0
- package/dist/web/assets/keybindings-store-DcxZ6WAa.js +1 -0
- package/dist/web/assets/{markdown-renderer-DK-YZN0m.js → markdown-renderer--Ss7hHOm.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CV0kVl2C.js → postgres-viewer-DMcvp0H7.js} +1 -1
- package/dist/web/assets/{settings-tab-DofusrxH.js → settings-tab-lC12I-a1.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D5L6DIMB.js → sqlite-viewer-BK2emL4i.js} +1 -1
- package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DcIBZTD4.js} +1 -1
- package/dist/web/assets/{terminal-tab-lu-7WWOT.js → terminal-tab--Ag9kqvS.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +16 -14
- package/src/providers/mock-provider.ts +6 -1
- package/src/server/ws/chat.ts +194 -139
- package/src/types/api.ts +9 -1
- package/src/web/components/chat/chat-tab.tsx +14 -5
- package/src/web/components/chat/message-input.tsx +39 -12
- package/src/web/components/chat/message-list.tsx +15 -12
- package/src/web/components/layout/panel-layout.tsx +17 -1
- package/src/web/hooks/use-chat.ts +196 -203
- package/src/web/stores/panel-store.ts +10 -10
- package/dist/web/assets/chat-tab-CM6zFolq.js +0 -7
- package/dist/web/assets/index-WKLuYsBY.css +0 -2
- package/dist/web/assets/index-frRaTxEm.js +0 -37
- package/dist/web/assets/keybindings-store-Bjy78BoD.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
|
|
2
|
-
import { ArrowUp, Square, Paperclip } from "lucide-react";
|
|
2
|
+
import { ArrowUp, Square, Paperclip, Loader2 } from "lucide-react";
|
|
3
3
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
4
4
|
import { randomId } from "@/lib/utils";
|
|
5
5
|
import { isSupportedFile, isImageFile } from "@/lib/file-support";
|
|
@@ -67,6 +67,7 @@ export const MessageInput = memo(function MessageInput({
|
|
|
67
67
|
const [value, setValue] = useState(initialValue ?? "");
|
|
68
68
|
const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
|
|
69
69
|
const [modeSelectorOpen, setModeSelectorOpen] = useState(false);
|
|
70
|
+
const [pendingSend, setPendingSend] = useState(false);
|
|
70
71
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
71
72
|
const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
|
|
72
73
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
@@ -274,11 +275,14 @@ export const MessageInput = memo(function MessageInput({
|
|
|
274
275
|
});
|
|
275
276
|
}, []);
|
|
276
277
|
|
|
277
|
-
|
|
278
|
+
/** Execute the actual send (called directly or after uploads complete) */
|
|
279
|
+
const executeSend = useCallback(() => {
|
|
278
280
|
const trimmed = value.trim();
|
|
279
281
|
const readyAttachments = attachments.filter((a) => a.status === "ready");
|
|
280
|
-
if (!trimmed && readyAttachments.length === 0)
|
|
281
|
-
|
|
282
|
+
if (!trimmed && readyAttachments.length === 0) {
|
|
283
|
+
setPendingSend(false);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
282
286
|
|
|
283
287
|
onSlashStateChange?.(false, "");
|
|
284
288
|
onFileStateChange?.(false, "");
|
|
@@ -289,9 +293,32 @@ export const MessageInput = memo(function MessageInput({
|
|
|
289
293
|
if (att.previewUrl) URL.revokeObjectURL(att.previewUrl);
|
|
290
294
|
}
|
|
291
295
|
setAttachments([]);
|
|
296
|
+
setPendingSend(false);
|
|
292
297
|
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
293
298
|
if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
|
|
294
|
-
}, [value, attachments,
|
|
299
|
+
}, [value, attachments, onSend, onSlashStateChange, onFileStateChange]);
|
|
300
|
+
|
|
301
|
+
const handleSend = useCallback(() => {
|
|
302
|
+
if (disabled) return;
|
|
303
|
+
|
|
304
|
+
// If files are still uploading, queue the send for when they finish
|
|
305
|
+
if (attachments.some((a) => a.status === "uploading")) {
|
|
306
|
+
const trimmed = value.trim();
|
|
307
|
+
if (trimmed || attachments.some((a) => a.status !== "error")) {
|
|
308
|
+
setPendingSend(true);
|
|
309
|
+
}
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
executeSend();
|
|
314
|
+
}, [value, attachments, disabled, executeSend]);
|
|
315
|
+
|
|
316
|
+
// Auto-send when queued and all uploads complete
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (!pendingSend) return;
|
|
319
|
+
if (attachments.some((a) => a.status === "uploading")) return;
|
|
320
|
+
executeSend();
|
|
321
|
+
}, [pendingSend, attachments, executeSend]);
|
|
295
322
|
|
|
296
323
|
const handleKeyDown = useCallback(
|
|
297
324
|
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -404,7 +431,7 @@ export const MessageInput = memo(function MessageInput({
|
|
|
404
431
|
[processFiles],
|
|
405
432
|
);
|
|
406
433
|
|
|
407
|
-
const hasContent = value.trim().length > 0 || attachments.some((a) => a.status
|
|
434
|
+
const hasContent = value.trim().length > 0 || attachments.some((a) => a.status !== "error");
|
|
408
435
|
const showCancel = isStreaming && !hasContent;
|
|
409
436
|
|
|
410
437
|
return (
|
|
@@ -472,12 +499,12 @@ export const MessageInput = memo(function MessageInput({
|
|
|
472
499
|
</button>
|
|
473
500
|
) : (
|
|
474
501
|
<button
|
|
475
|
-
onClick={(e) => { e.stopPropagation(); handleSend(); }}
|
|
502
|
+
onClick={(e) => { e.stopPropagation(); pendingSend ? setPendingSend(false) : handleSend(); }}
|
|
476
503
|
disabled={disabled || !hasContent}
|
|
477
504
|
className="flex items-center justify-center size-7 shrink-0 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 transition-colors"
|
|
478
|
-
aria-label="Send"
|
|
505
|
+
aria-label={pendingSend ? "Cancel queued send" : "Send"}
|
|
479
506
|
>
|
|
480
|
-
<ArrowUp className="size-3.5" />
|
|
507
|
+
{pendingSend ? <Loader2 className="size-3.5 animate-spin" /> : <ArrowUp className="size-3.5" />}
|
|
481
508
|
</button>
|
|
482
509
|
)}
|
|
483
510
|
</div>
|
|
@@ -533,12 +560,12 @@ export const MessageInput = memo(function MessageInput({
|
|
|
533
560
|
</button>
|
|
534
561
|
) : (
|
|
535
562
|
<button
|
|
536
|
-
onClick={(e) => { e.stopPropagation(); handleSend(); }}
|
|
563
|
+
onClick={(e) => { e.stopPropagation(); pendingSend ? setPendingSend(false) : handleSend(); }}
|
|
537
564
|
disabled={disabled || !hasContent}
|
|
538
565
|
className="flex items-center justify-center size-8 rounded-full bg-primary text-primary-foreground hover:bg-primary/90 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
|
|
539
|
-
aria-label="Send message"
|
|
566
|
+
aria-label={pendingSend ? "Cancel queued send" : "Send message"}
|
|
540
567
|
>
|
|
541
|
-
<ArrowUp className="size-4" />
|
|
568
|
+
{pendingSend ? <Loader2 className="size-4 animate-spin" /> : <ArrowUp className="size-4" />}
|
|
542
569
|
</button>
|
|
543
570
|
)}
|
|
544
571
|
</div>
|
|
@@ -2,7 +2,7 @@ import { useEffect, useRef, useState, useMemo, useCallback } from "react";
|
|
|
2
2
|
import { StickToBottom, useStickToBottomContext } from "use-stick-to-bottom";
|
|
3
3
|
import { getAuthToken } from "@/lib/api-client";
|
|
4
4
|
import type { ChatMessage, ChatEvent } from "../../../types/chat";
|
|
5
|
-
import type {
|
|
5
|
+
import type { SessionPhase } from "../../../types/api";
|
|
6
6
|
import { ToolCard } from "./tool-cards";
|
|
7
7
|
import { MarkdownRenderer } from "@/components/shared/markdown-renderer";
|
|
8
8
|
import { cn, basename } from "@/lib/utils";
|
|
@@ -39,9 +39,8 @@ interface MessageListProps {
|
|
|
39
39
|
pendingApproval: { requestId: string; tool: string; input: unknown } | null;
|
|
40
40
|
onApprovalResponse: (requestId: string, approved: boolean, data?: unknown) => void;
|
|
41
41
|
isStreaming: boolean;
|
|
42
|
-
|
|
42
|
+
phase?: SessionPhase;
|
|
43
43
|
connectingElapsed?: number;
|
|
44
|
-
thinkingWarningThreshold?: number;
|
|
45
44
|
projectName?: string;
|
|
46
45
|
/** Called when user clicks Fork/Rewind — opens new forked chat tab */
|
|
47
46
|
onFork?: (userMessage: string) => void;
|
|
@@ -53,9 +52,8 @@ export function MessageList({
|
|
|
53
52
|
pendingApproval,
|
|
54
53
|
onApprovalResponse,
|
|
55
54
|
isStreaming,
|
|
56
|
-
|
|
55
|
+
phase,
|
|
57
56
|
connectingElapsed,
|
|
58
|
-
thinkingWarningThreshold,
|
|
59
57
|
projectName,
|
|
60
58
|
onFork,
|
|
61
59
|
}: MessageListProps) {
|
|
@@ -108,7 +106,7 @@ export function MessageList({
|
|
|
108
106
|
: <ApprovalCard approval={pendingApproval} onRespond={onApprovalResponse} />
|
|
109
107
|
)}
|
|
110
108
|
|
|
111
|
-
{isStreaming && <ThinkingIndicator lastMessage={messages[messages.length - 1]}
|
|
109
|
+
{isStreaming && <ThinkingIndicator lastMessage={messages[messages.length - 1]} phase={phase} elapsed={connectingElapsed} />}
|
|
112
110
|
</StickToBottom.Content>
|
|
113
111
|
<ScrollToBottomButton />
|
|
114
112
|
</StickToBottom>
|
|
@@ -751,14 +749,13 @@ function StreamingText({ content, animate: isStreaming, projectName }: { content
|
|
|
751
749
|
* - After tool: "Processing..."
|
|
752
750
|
* - Text streaming: hidden
|
|
753
751
|
*/
|
|
754
|
-
function ThinkingIndicator({ lastMessage,
|
|
755
|
-
// Show
|
|
752
|
+
function ThinkingIndicator({ lastMessage, phase, elapsed }: { lastMessage?: ChatMessage; phase?: SessionPhase; elapsed?: number }) {
|
|
753
|
+
// Show indicator when:
|
|
756
754
|
// 1. No assistant message yet (waiting for first response)
|
|
757
|
-
// 2. Last event is
|
|
755
|
+
// 2. Last event is tool_result (Claude thinking after tool execution)
|
|
758
756
|
// Hide when text is actively streaming (text itself is the indicator)
|
|
759
757
|
|
|
760
758
|
const isWaiting = !lastMessage || lastMessage.role !== "assistant";
|
|
761
|
-
// Show Thinking only after tool_result (tool finished), not tool_use (tool still running)
|
|
762
759
|
const isAfterTool = (() => {
|
|
763
760
|
if (!lastMessage?.events?.length) return false;
|
|
764
761
|
const last = lastMessage.events[lastMessage.events.length - 1]!;
|
|
@@ -767,13 +764,19 @@ function ThinkingIndicator({ lastMessage, streamingStatus, elapsed, warningThres
|
|
|
767
764
|
|
|
768
765
|
if (!isWaiting && !isAfterTool) return null;
|
|
769
766
|
|
|
770
|
-
const
|
|
767
|
+
const label = phase === "initializing" ? "Initializing"
|
|
768
|
+
: phase === "connecting" ? "Connecting"
|
|
769
|
+
: phase === "thinking" ? "Thinking"
|
|
770
|
+
: "Processing";
|
|
771
|
+
|
|
772
|
+
const isLong = phase === "connecting" && (elapsed ?? 0) >= 30;
|
|
773
|
+
|
|
771
774
|
return (
|
|
772
775
|
<div className="flex flex-col gap-1 text-sm">
|
|
773
776
|
<div className="flex items-center gap-2 text-text-subtle">
|
|
774
777
|
<Loader2 className="size-3 animate-spin" />
|
|
775
778
|
<span>
|
|
776
|
-
|
|
779
|
+
{label}
|
|
777
780
|
{isWaiting && (elapsed ?? 0) > 0 && <span className="text-text-subtle/60">... ({elapsed}s)</span>}
|
|
778
781
|
</span>
|
|
779
782
|
</div>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
1
2
|
import { Panel, Group, Separator } from "react-resizable-panels";
|
|
2
3
|
import { GripVertical, GripHorizontal } from "lucide-react";
|
|
3
4
|
import { usePanelStore } from "@/stores/panel-store";
|
|
5
|
+
import { createPanel } from "@/stores/panel-utils";
|
|
4
6
|
import { EditorPanel } from "./editor-panel";
|
|
5
7
|
|
|
6
8
|
interface PanelLayoutProps {
|
|
@@ -13,7 +15,21 @@ export function PanelLayout({ projectName }: PanelLayoutProps) {
|
|
|
13
15
|
);
|
|
14
16
|
const panelCount = grid.flat().length;
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
// Recover from empty grid (corrupt persisted state or edge-case bug)
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (panelCount === 0) {
|
|
21
|
+
const p = createPanel();
|
|
22
|
+
usePanelStore.setState((s) => ({
|
|
23
|
+
panels: { ...s.panels, [p.id]: p },
|
|
24
|
+
grid: [[p.id]],
|
|
25
|
+
focusedPanelId: p.id,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
}, [panelCount]);
|
|
29
|
+
|
|
30
|
+
if (panelCount === 0) return null;
|
|
31
|
+
|
|
32
|
+
if (panelCount === 1 && grid[0]?.[0]) {
|
|
17
33
|
return <EditorPanel panelId={grid[0][0]} projectName={projectName} />;
|
|
18
34
|
}
|
|
19
35
|
|