@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.
Files changed (32) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/web/assets/chat-tab-C5H74y2z.js +7 -0
  3. package/dist/web/assets/{code-editor-DgTfBijB.js → code-editor-DMw26mUm.js} +1 -1
  4. package/dist/web/assets/{database-viewer-DSlQhR7c.js → database-viewer-gnj_8u4T.js} +1 -1
  5. package/dist/web/assets/{diff-viewer-C5A-ZnrC.js → diff-viewer-DVqfhdBN.js} +1 -1
  6. package/dist/web/assets/{git-graph-B5QR_Cf-.js → git-graph-CJy7tOAJ.js} +1 -1
  7. package/dist/web/assets/index-BAioKo_2.css +2 -0
  8. package/dist/web/assets/index-Dg6TQ3Iu.js +37 -0
  9. package/dist/web/assets/keybindings-store-DcxZ6WAa.js +1 -0
  10. package/dist/web/assets/{markdown-renderer-DK-YZN0m.js → markdown-renderer--Ss7hHOm.js} +1 -1
  11. package/dist/web/assets/{postgres-viewer-CV0kVl2C.js → postgres-viewer-DMcvp0H7.js} +1 -1
  12. package/dist/web/assets/{settings-tab-DofusrxH.js → settings-tab-lC12I-a1.js} +1 -1
  13. package/dist/web/assets/{sqlite-viewer-D5L6DIMB.js → sqlite-viewer-BK2emL4i.js} +1 -1
  14. package/dist/web/assets/{tab-store-dpsCvqhH.js → tab-store-DcIBZTD4.js} +1 -1
  15. package/dist/web/assets/{terminal-tab-lu-7WWOT.js → terminal-tab--Ag9kqvS.js} +1 -1
  16. package/dist/web/index.html +3 -3
  17. package/dist/web/sw.js +1 -1
  18. package/package.json +1 -1
  19. package/src/providers/claude-agent-sdk.ts +16 -14
  20. package/src/providers/mock-provider.ts +6 -1
  21. package/src/server/ws/chat.ts +194 -139
  22. package/src/types/api.ts +9 -1
  23. package/src/web/components/chat/chat-tab.tsx +14 -5
  24. package/src/web/components/chat/message-input.tsx +39 -12
  25. package/src/web/components/chat/message-list.tsx +15 -12
  26. package/src/web/components/layout/panel-layout.tsx +17 -1
  27. package/src/web/hooks/use-chat.ts +196 -203
  28. package/src/web/stores/panel-store.ts +10 -10
  29. package/dist/web/assets/chat-tab-CM6zFolq.js +0 -7
  30. package/dist/web/assets/index-WKLuYsBY.css +0 -2
  31. package/dist/web/assets/index-frRaTxEm.js +0 -37
  32. 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
- const handleSend = useCallback(() => {
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) return;
281
- if (disabled) return;
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, disabled, onSend, onSlashStateChange, onFileStateChange]);
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 === "ready");
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 { StreamingStatus } from "@/hooks/use-chat";
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
- streamingStatus?: StreamingStatus;
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
- streamingStatus,
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]} streamingStatus={streamingStatus} elapsed={connectingElapsed} warningThreshold={thinkingWarningThreshold} />}
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, streamingStatus, elapsed, warningThreshold = 15 }: { lastMessage?: ChatMessage; streamingStatus?: StreamingStatus; elapsed?: number; warningThreshold?: number }) {
755
- // Show "Thinking" when:
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 tool_use/tool_result (Claude thinking after tool execution)
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 isLong = isWaiting && (elapsed ?? 0) >= warningThreshold;
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
- Thinking
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
- if (panelCount <= 1 && grid[0]?.[0]) {
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