@hienlh/ppm 0.8.86 → 0.8.87
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 +193 -5
- package/bun.lock +5 -0
- package/dist/web/assets/{_basePickBy-5eBmZ_lt.js → _basePickBy-5PGDJbfF.js} +1 -1
- package/dist/web/assets/{_baseUniq-DimLlN0y.js → _baseUniq-BT4Ow4Kk.js} +1 -1
- package/dist/web/assets/api-settings-Bx1GaNmQ.js +1 -0
- package/dist/web/assets/{arc-D4SasZrA.js → arc-BAOivWpI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-nv0WbM7d.js → architectureDiagram-2XIMDMQ5-DWBCPMLF.js} +1 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +1 -0
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-C1XvYrb8.js → blockDiagram-WCTKOSBZ-TEF8Ally.js} +1 -1
- package/dist/web/assets/browser-tab-DaHGm_0i.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-CygDrbWJ.js → c4Diagram-IC4MRINW-dV22iAsY.js} +1 -1
- package/dist/web/assets/channel-wrd-NHWf.js +1 -0
- package/dist/web/assets/chat-tab-BDYE0KHF.js +8 -0
- package/dist/web/assets/chevron-right-DeV0ehiG.js +1 -0
- package/dist/web/assets/{chunk-4BX2VUAB-C2FDgsgT.js → chunk-4BX2VUAB-D4tOov49.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-jF4w6cat.js → chunk-55IACEB6-DJ6BynZ4.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-BVCECZFi.js → chunk-7E7YKBS2-CiyUJxNI.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-DXTbeu5d.js → chunk-7R4GIKGN-BbIFzsIv.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-BaZqOsTs.js → chunk-C72U2L5F-D21mS_6G.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-Bky2tcH7.js → chunk-EGIJ26TM-DzqmU2Z7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-Cp4BK9A8.js → chunk-FMBD7UC4-DXncblvW.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-BosFEH7G.js → chunk-GEFDOKGD-BbQkJu8C.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-H5Gbjsbr.js → chunk-JSJVCQXG-23tyvw8k.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-CWerSUwS.js → chunk-KX2RTZJC-sQ0o-39C.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-FvwP7jUy.js → chunk-KYZI473N-BcUZNnwd.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-D1PI_ORP.js → chunk-L3YUKLVL-C7qGJrfV.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-C7Vzk_AI.js → chunk-MX3YWQON-BpS_PtKp.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-BceYBGYX.js → chunk-NQ4KR5QH-wMgTlP7f.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-WPtzgxql.js → chunk-O4XLMI2P-JC6EGoUz.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-DlHXDeLY.js → chunk-OZEHJAEY-BXhYx3nO.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-Ci_Prygb.js → chunk-PQ6SQG4A-D6BTbCQw.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-CO0zMN-z.js → chunk-PU5JKC2W-Dw8ClWch.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-IAEEzfpM.js → chunk-R5LLSJPH-CFwSJijQ.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-BLXalOgc.js → chunk-WL4C6EOR-DfofndiH.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Dx1Ri_p2.js → chunk-XIRO2GV7-Djlmrely.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-m9pPGKn7.js → chunk-XPW4576I-BPQQBakK.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-B_08ExbI.js → chunk-XZSTWKYB-DxAOx4hG.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-DqSOVcYe.js → chunk-YBOYWFTD-CeU4Q-xC.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +1 -0
- package/dist/web/assets/clone-LRxlvnMj.js +1 -0
- package/dist/web/assets/code-editor-DTA3c9Y8.js +2 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-DlL82QHu.js → cose-bilkent-S5V4N54A-B_AWZsOP.js} +1 -1
- package/dist/web/assets/csv-preview-DLqYtXxt.js +10 -0
- package/dist/web/assets/{dagre-BmVoh2At.js → dagre-Dbb5k38K.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-sDrRW9MQ.js → dagre-KLK3FWXG-BH7aWGRP.js} +1 -1
- package/dist/web/assets/database-viewer-DXk79Nel.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-ChnAhgni.js → diagram-E7M64L7V-B1Qz70Do.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-DW1J1uJd.js → diagram-IFDJBPK2-k55eVqVU.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-CQ32hyG_.js → diagram-P4PSJMXO-BkfNRc9U.js} +1 -1
- package/dist/web/assets/diff-viewer-HhIcsOQE.js +4 -0
- package/dist/web/assets/dist-DylI9XxN.js +13 -0
- package/dist/web/assets/dist-lF8CoYII.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-6CHo6nOw.js → erDiagram-INFDFZHY-CKzVujYI.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-DroDiNT0.js → flowDiagram-PKNHOUZH-DIqcTrDV.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-DP0QBh8w.js → ganttDiagram-A5KZAMGK-D4v7ZbVE.js} +1 -1
- package/dist/web/assets/git-graph-CQtWu8yE.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js → gitGraphDiagram-K3NZZRJ6-BTXo57mF.js} +1 -1
- package/dist/web/assets/{graphlib-CQBb2thr.js → graphlib-BcsNnGcW.js} +1 -1
- package/dist/web/assets/index-CgQXpBb_.css +2 -0
- package/dist/web/assets/index-DEeeRoka.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +2 -0
- package/dist/web/assets/input-BglMT33g.js +1 -0
- package/dist/web/assets/{isEmpty-B4kqZBtn.js → isEmpty-bnrF3Qbc.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-46yibrV5.js → ishikawaDiagram-PHBUUO56-BOyvKMmB.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-BcmRwjK-.js → journeyDiagram-4ABVD52K-ufoasAy6.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-B619K53y.js → kanban-definition-K7BYSVSG-Bi0UTUeN.js} +1 -1
- package/dist/web/assets/keybindings-store-1CJ7VX57.js +1 -0
- package/dist/web/assets/lib-BQ34Db2e.js +4 -0
- package/dist/web/assets/{line-1gcO63_w.js → line-B78g-52T.js} +1 -1
- package/dist/web/assets/{linear-DfRqDoVd.js → linear-DP4mkX3m.js} +1 -1
- package/dist/web/assets/markdown-renderer-Brj8_LQM.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-XtjZQOeM.js → mermaid-parser.core-DMIWdgEW.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-CifOFo_q.js → mindmap-definition-YRQLILUH-BsfWvIoO.js} +1 -1
- package/dist/web/assets/{ordinal-BJYw-iDX.js → ordinal-_K3x1fkz.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-BuHUh_fO.js → pieDiagram-SKSYHLDU-WP0XXw51.js} +1 -1
- package/dist/web/assets/postgres-viewer-CwkTGmqy.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-Bau_hj6Z.js → quadrantDiagram-337W2JSQ-FHMogtsh.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +1 -0
- package/dist/web/assets/react-dom-Bpkvzu3U.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-Cq2b-uwp.js → requirementDiagram-Z7DCOOCP-BatTxyWb.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js → sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-qPxiTUcS.js → sequenceDiagram-2WXFIKYE-ByxQqGgs.js} +1 -1
- package/dist/web/assets/settings-tab-BDE1MsIh.js +1 -0
- package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-Dulj2oa8.js → stateDiagram-RAJIS63D-f8opcZNY.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +1 -0
- package/dist/web/assets/tab-store-BJw7OCmy.js +1 -0
- package/dist/web/assets/{terminal-tab-wKgpSPAT.js → terminal-tab-CCDLZA5Y.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-BWyDnCYq.js → timeline-definition-YZTLITO2-58BlOSf9.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +1 -0
- package/dist/web/assets/use-monaco-theme-CNzekTN3.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-B9Iv2bNV.js → vennDiagram-LZ73GAT5-BOSy9ma9.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-ChXcMzBQ.js → xychartDiagram-JWTSCODW-z5MVJauZ.js} +1 -1
- package/dist/web/index.html +12 -11
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +232 -7
- package/docs/codebase-summary.md +9 -3
- package/docs/design-guidelines.md +21 -0
- package/docs/project-changelog.md +115 -1
- package/docs/project-roadmap.md +41 -19
- package/docs/system-architecture.md +212 -15
- package/package.json +3 -2
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/restart.ts +9 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/index.ts +2 -3
- package/src/providers/claude-agent-sdk.ts +92 -25
- package/src/providers/mock-provider.ts +6 -1
- package/src/server/index.ts +38 -166
- package/src/server/routes/browser-preview.ts +159 -0
- package/src/server/routes/chat.ts +52 -3
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/proxy.ts +46 -53
- package/src/server/routes/tunnel.ts +0 -32
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +207 -146
- package/src/services/account-selector.service.ts +16 -8
- package/src/services/account.service.ts +19 -13
- package/src/services/claude-usage.service.ts +48 -11
- package/src/services/cloud-ws.service.ts +227 -0
- package/src/services/cloud.service.ts +10 -6
- package/src/services/db.service.ts +111 -6
- package/src/services/proxy.service.ts +4 -19
- package/src/services/supervisor.ts +285 -25
- package/src/types/api.ts +9 -1
- package/src/types/chat.ts +3 -1
- package/src/web/app.tsx +41 -35
- package/src/web/components/browser/browser-tab.tsx +106 -97
- package/src/web/components/chat/account-rotation-settings.tsx +163 -0
- package/src/web/components/chat/chat-history-bar.tsx +72 -6
- package/src/web/components/chat/chat-tab.tsx +32 -16
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-input.tsx +107 -13
- package/src/web/components/chat/message-list.tsx +27 -15
- package/src/web/components/chat/session-picker.tsx +78 -31
- package/src/web/components/chat/usage-badge.tsx +11 -1
- package/src/web/components/editor/code-editor.tsx +36 -26
- package/src/web/components/editor/csv-preview.tsx +228 -0
- package/src/web/components/editor/editor-breadcrumb.tsx +216 -0
- package/src/web/components/editor/editor-toolbar.tsx +74 -0
- package/src/web/components/layout/command-palette.tsx +3 -1
- package/src/web/components/layout/editor-panel.tsx +162 -18
- package/src/web/components/layout/panel-layout.tsx +17 -1
- package/src/web/components/settings/proxy-settings-section.tsx +40 -42
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +211 -201
- package/src/web/hooks/use-global-keybindings.ts +25 -2
- package/src/web/hooks/use-server-reload.ts +9 -0
- package/src/web/hooks/use-url-sync.ts +173 -21
- package/src/web/hooks/use-voice-input.ts +111 -0
- package/src/web/lib/csv-parser.ts +134 -0
- package/src/web/stores/connection-store.ts +39 -0
- package/src/web/stores/keybindings-store.ts +1 -0
- package/src/web/stores/panel-store.ts +73 -19
- package/src/web/stores/panel-utils.ts +145 -3
- package/dist/web/assets/api-settings-CFw-lh5k.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +0 -1
- package/dist/web/assets/browser-tab-CmsL5eny.js +0 -1
- package/dist/web/assets/channel-DmKoFTd_.js +0 -1
- package/dist/web/assets/chat-tab-CFWsf13Z.js +0 -7
- package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +0 -1
- package/dist/web/assets/clone-CijCFRT5.js +0 -1
- package/dist/web/assets/code-editor-H_dAh_fJ.js +0 -1
- package/dist/web/assets/database-viewer-DBzsgEJ8.js +0 -1
- package/dist/web/assets/diff-viewer-DzS-OnAR.js +0 -4
- package/dist/web/assets/dist-0Va_2L7G.js +0 -16
- package/dist/web/assets/dist-D9irYETY.js +0 -41
- package/dist/web/assets/git-graph-D3C7F8o3.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +0 -1
- package/dist/web/assets/index-CIkjfera.js +0 -31
- package/dist/web/assets/index-WKLuYsBY.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +0 -2
- package/dist/web/assets/input-CGp1nFIg.js +0 -1
- package/dist/web/assets/keybindings-store-BdaoLwSo.js +0 -1
- package/dist/web/assets/markdown-renderer-DH49Zag7.js +0 -69
- package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +0 -1
- package/dist/web/assets/postgres-viewer-B9FYk8sD.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +0 -1
- package/dist/web/assets/settings-store-DWXGVHsE.js +0 -2
- package/dist/web/assets/settings-tab-D-q8pd-5.js +0 -1
- package/dist/web/assets/sqlite-viewer-CDqcTePw.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +0 -1
- package/dist/web/assets/tab-store-BPeiymiH.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +0 -1
- package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +0 -11
- package/src/services/port-tunnel.service.ts +0 -97
- /package/dist/web/assets/{api-client-DOElml5u.js → api-client-BfBM3I7n.js} +0 -0
- /package/dist/web/assets/{array-CYkMkqnU.js → array-B9UHiPd-.js} +0 -0
- /package/dist/web/assets/{columns-2-ChOTgl3e.js → columns-2-DpsNbZOc.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-HeHO0VhB.js → cytoscape.esm-BW-DbntU.js} +0 -0
- /package/dist/web/assets/{defaultLocale-Beh6XjaL.js → defaultLocale-5eAKkKJC.js} +0 -0
- /package/dist/web/assets/{dist-BUYzeuKe.js → dist-CSJdAyA9.js} +0 -0
- /package/dist/web/assets/{init-Rr1s_RiX.js → init-DlZdxViB.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-BB-mzMLb.js → isArrayLikeObject-B_v2FtYn.js} +0 -0
- /package/dist/web/assets/{katex-CKoArbIw.js → katex-Bqvo_ZG0.js} +0 -0
- /package/dist/web/assets/{math-B7b0HgJF.js → math-069Z4SuC.js} +0 -0
- /package/dist/web/assets/{path-BAQ3hXlG.js → path-6uRLdFF7.js} +0 -0
- /package/dist/web/assets/{preload-helper-DeiOTZKJ.js → preload-helper-uTix4PVD.js} +0 -0
- /package/dist/web/assets/{react-Dev-wu-s.js → react-ER-4DN55.js} +0 -0
- /package/dist/web/assets/{rough.esm-Dwml_la6.js → rough.esm-JX0wREDd.js} +0 -0
- /package/dist/web/assets/{src-B_cC68fH.js → src-BqX54PbV.js} +0 -0
- /package/dist/web/assets/{table-COiJDPRA.js → table-C7X5UAEI.js} +0 -0
- /package/dist/web/assets/{tag-LMq02LfE.js → tag-CCtdV063.js} +0 -0
- /package/dist/web/assets/{utils-btZ8C8-R.js → utils-BNytJOb1.js} +0 -0
|
@@ -5,7 +5,7 @@ import { useNotificationStore } from "@/stores/notification-store";
|
|
|
5
5
|
import { usePanelStore } from "@/stores/panel-store";
|
|
6
6
|
import { playNotificationSound } from "@/lib/notification-sounds";
|
|
7
7
|
import type { ChatMessage, ChatEvent } from "../../types/chat";
|
|
8
|
-
import type { ChatWsServerMessage } from "../../types/api";
|
|
8
|
+
import type { ChatWsServerMessage, SessionPhase } from "../../types/api";
|
|
9
9
|
|
|
10
10
|
interface ApprovalRequest {
|
|
11
11
|
requestId: string;
|
|
@@ -13,21 +13,17 @@ interface ApprovalRequest {
|
|
|
13
13
|
input: unknown;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/** Streaming phase: connecting → streaming → idle */
|
|
17
|
-
export type StreamingStatus = "idle" | "connecting" | "streaming";
|
|
18
|
-
|
|
19
16
|
interface UseChatReturn {
|
|
20
17
|
messages: ChatMessage[];
|
|
21
18
|
messagesLoading: boolean;
|
|
22
19
|
isStreaming: boolean;
|
|
23
|
-
|
|
20
|
+
phase: SessionPhase;
|
|
21
|
+
isReconnecting: boolean;
|
|
24
22
|
connectingElapsed: number;
|
|
25
|
-
thinkingWarningThreshold: number;
|
|
26
23
|
pendingApproval: ApprovalRequest | null;
|
|
27
|
-
/** Context window usage % from last completed query (0–100) */
|
|
28
24
|
contextWindowPct: number | null;
|
|
29
|
-
/** Updated session title from SDK summary (set after stream completes) */
|
|
30
25
|
sessionTitle: string | null;
|
|
26
|
+
streamingAccountLabel: string | null;
|
|
31
27
|
sendMessage: (content: string, opts?: { permissionMode?: string }) => void;
|
|
32
28
|
respondToApproval: (requestId: string, approved: boolean, data?: unknown) => void;
|
|
33
29
|
cancelStreaming: () => void;
|
|
@@ -49,201 +45,148 @@ function isSessionTabActive(sid: string): boolean {
|
|
|
49
45
|
export function useChat(sessionId: string | null, providerId = "claude", projectName = ""): UseChatReturn {
|
|
50
46
|
const [messages, setMessages] = useState<ChatMessage[]>([]);
|
|
51
47
|
const [messagesLoading, setMessagesLoading] = useState(false);
|
|
52
|
-
const [
|
|
53
|
-
const [
|
|
54
|
-
/** Elapsed seconds while connecting (sent by BE heartbeat every 5s) */
|
|
48
|
+
const [phase, setPhase] = useState<SessionPhase>("idle");
|
|
49
|
+
const [isReconnecting, setIsReconnecting] = useState(false);
|
|
55
50
|
const [connectingElapsed, setConnectingElapsed] = useState(0);
|
|
56
|
-
/** Warning threshold in seconds — higher for deeper thinking modes */
|
|
57
|
-
const [thinkingWarningThreshold, setThinkingWarningThreshold] = useState(15);
|
|
58
51
|
const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
|
|
59
52
|
const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
|
|
60
53
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
54
|
+
const [streamingAccountLabel, setStreamingAccountLabel] = useState<string | null>(null);
|
|
61
55
|
const [isConnected, setIsConnected] = useState(false);
|
|
62
56
|
const streamingContentRef = useRef("");
|
|
63
57
|
const streamingEventsRef = useRef<ChatEvent[]>([]);
|
|
64
58
|
const streamingAccountRef = useRef<{ accountId: string; accountLabel: string } | null>(null);
|
|
65
|
-
const
|
|
59
|
+
const phaseRef = useRef<SessionPhase>("idle");
|
|
66
60
|
const pendingMessageRef = useRef<string | null>(null);
|
|
67
61
|
const sendRef = useRef<(data: string) => void>(() => {});
|
|
68
62
|
const refetchRef = useRef<(() => void) | null>(null);
|
|
69
|
-
// Refs for notification dispatch inside handleMessage (which has [] deps)
|
|
70
63
|
const sessionIdRef = useRef(sessionId);
|
|
71
64
|
sessionIdRef.current = sessionId;
|
|
72
65
|
const projectNameRef = useRef(projectName);
|
|
73
66
|
projectNameRef.current = projectName;
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
68
|
+
// Derived state
|
|
69
|
+
const isStreaming = phase !== "idle";
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Route a child event to its parent Agent/Task tool_use's children array.
|
|
73
|
+
* Creates a new parent object to ensure React detects the change on re-render.
|
|
74
|
+
* Returns true if routed (caller should skip flat append), false if no parent found.
|
|
75
|
+
*/
|
|
76
|
+
const routeToParent = useCallback((childEvent: ChatEvent, parentToolUseId: string): boolean => {
|
|
77
|
+
const idx = streamingEventsRef.current.findIndex(
|
|
78
|
+
(e) => e.type === "tool_use"
|
|
79
|
+
&& (e.tool === "Agent" || e.tool === "Task")
|
|
80
|
+
&& (e as any).toolUseId === parentToolUseId,
|
|
81
|
+
);
|
|
82
|
+
if (idx === -1) return false;
|
|
83
|
+
const parent = streamingEventsRef.current[idx]!;
|
|
84
|
+
if (parent.type !== "tool_use") return false;
|
|
85
|
+
const newChildren = [...(parent.children ?? []), childEvent];
|
|
86
|
+
streamingEventsRef.current[idx] = { ...parent, children: newChildren };
|
|
87
|
+
return true;
|
|
88
|
+
}, []);
|
|
91
89
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Higher thinking = longer acceptable wait time
|
|
102
|
-
let threshold = 15; // default
|
|
103
|
-
if (budget && budget > 0) {
|
|
104
|
-
// Rough: 1k tokens ≈ 2s thinking time
|
|
105
|
-
threshold = Math.max(15, Math.round(budget / 500));
|
|
106
|
-
} else if (effort === "high") {
|
|
107
|
-
threshold = 30;
|
|
108
|
-
} else if (effort === "low") {
|
|
109
|
-
threshold = 10;
|
|
110
|
-
}
|
|
111
|
-
setThinkingWarningThreshold(threshold);
|
|
90
|
+
/** Trigger re-render with latest events snapshot */
|
|
91
|
+
const syncMessages = useCallback(() => {
|
|
92
|
+
const content = streamingContentRef.current;
|
|
93
|
+
const events = [...streamingEventsRef.current];
|
|
94
|
+
const account = streamingAccountRef.current;
|
|
95
|
+
setMessages((prev) => {
|
|
96
|
+
const last = prev[prev.length - 1];
|
|
97
|
+
if (last?.role === "assistant" && !last.id.startsWith("final-")) {
|
|
98
|
+
return [...prev.slice(0, -1), { ...last, content, events, ...account }];
|
|
112
99
|
}
|
|
113
|
-
return
|
|
114
|
-
|
|
100
|
+
return [...prev, {
|
|
101
|
+
id: `streaming-${Date.now()}`,
|
|
102
|
+
role: "assistant" as const,
|
|
103
|
+
content,
|
|
104
|
+
events,
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
...account,
|
|
107
|
+
}];
|
|
108
|
+
});
|
|
109
|
+
}, []);
|
|
115
110
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
111
|
+
/** Process a single stream event — reused by live events and turn_events replay */
|
|
112
|
+
const processStreamEvent = useCallback((data: unknown) => {
|
|
113
|
+
const ev = data as any;
|
|
114
|
+
const evType = ev?.type;
|
|
115
|
+
if (!evType) return;
|
|
122
116
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (status.isStreaming) {
|
|
129
|
-
isStreamingRef.current = true;
|
|
130
|
-
setIsStreaming(true);
|
|
131
|
-
}
|
|
132
|
-
if (status.pendingApproval) {
|
|
133
|
-
setPendingApproval({
|
|
134
|
-
requestId: status.pendingApproval.requestId,
|
|
135
|
-
tool: status.pendingApproval.tool,
|
|
136
|
-
input: status.pendingApproval.input,
|
|
137
|
-
});
|
|
117
|
+
switch (evType) {
|
|
118
|
+
case "account_info": {
|
|
119
|
+
streamingAccountRef.current = { accountId: ev.accountId, accountLabel: ev.accountLabel };
|
|
120
|
+
setStreamingAccountLabel(ev.accountLabel ?? null);
|
|
121
|
+
break;
|
|
138
122
|
}
|
|
139
|
-
// Refetch history to catch up on events missed during disconnect
|
|
140
|
-
refetchRef.current?.();
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Route a child event to its parent Agent/Task tool_use's children array.
|
|
146
|
-
* Creates a new parent object to ensure React detects the change on re-render.
|
|
147
|
-
* Returns true if routed (caller should skip flat append), false if no parent found.
|
|
148
|
-
*/
|
|
149
|
-
const routeToParent = (childEvent: ChatEvent, parentToolUseId: string): boolean => {
|
|
150
|
-
const idx = streamingEventsRef.current.findIndex(
|
|
151
|
-
(e) => e.type === "tool_use"
|
|
152
|
-
&& (e.tool === "Agent" || e.tool === "Task")
|
|
153
|
-
&& (e as any).toolUseId === parentToolUseId,
|
|
154
|
-
);
|
|
155
|
-
if (idx === -1) return false;
|
|
156
|
-
const parent = streamingEventsRef.current[idx]!;
|
|
157
|
-
if (parent.type !== "tool_use") return false;
|
|
158
|
-
// Create new object so React detects the change via shallow comparison
|
|
159
|
-
const newChildren = [...(parent.children ?? []), childEvent];
|
|
160
|
-
streamingEventsRef.current[idx] = { ...parent, children: newChildren };
|
|
161
|
-
return true;
|
|
162
|
-
};
|
|
163
123
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
setMessages((prev) => {
|
|
170
|
-
const last = prev[prev.length - 1];
|
|
171
|
-
if (last?.role === "assistant" && !last.id.startsWith("final-")) {
|
|
172
|
-
return [...prev.slice(0, -1), { ...last, content, events, ...account }];
|
|
124
|
+
case "account_retry": {
|
|
125
|
+
// Update streaming account to the new one being tried
|
|
126
|
+
if (ev.accountId && ev.accountLabel) {
|
|
127
|
+
streamingAccountRef.current = { accountId: ev.accountId, accountLabel: ev.accountLabel };
|
|
128
|
+
setStreamingAccountLabel(ev.accountLabel);
|
|
173
129
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
content,
|
|
178
|
-
events,
|
|
179
|
-
timestamp: new Date().toISOString(),
|
|
180
|
-
...account,
|
|
181
|
-
}];
|
|
182
|
-
});
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
switch (data.type) {
|
|
186
|
-
case "account_info": {
|
|
187
|
-
streamingAccountRef.current = { accountId: (data as any).accountId, accountLabel: (data as any).accountLabel };
|
|
130
|
+
// Surface retry as a system-level event in the stream
|
|
131
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
132
|
+
syncMessages();
|
|
188
133
|
break;
|
|
189
134
|
}
|
|
190
135
|
|
|
191
136
|
case "text": {
|
|
192
|
-
const pid =
|
|
193
|
-
if (pid && routeToParent(
|
|
194
|
-
// Child text routed to parent — just re-render
|
|
137
|
+
const pid = ev.parentToolUseId as string | undefined;
|
|
138
|
+
if (pid && routeToParent(ev as ChatEvent, pid)) {
|
|
195
139
|
syncMessages();
|
|
196
140
|
break;
|
|
197
141
|
}
|
|
198
|
-
streamingContentRef.current +=
|
|
199
|
-
streamingEventsRef.current.push(
|
|
142
|
+
streamingContentRef.current += ev.content;
|
|
143
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
200
144
|
syncMessages();
|
|
201
145
|
break;
|
|
202
146
|
}
|
|
203
147
|
|
|
204
148
|
case "thinking": {
|
|
205
|
-
const pid =
|
|
206
|
-
if (pid && routeToParent(
|
|
149
|
+
const pid = ev.parentToolUseId as string | undefined;
|
|
150
|
+
if (pid && routeToParent(ev as ChatEvent, pid)) {
|
|
207
151
|
syncMessages();
|
|
208
152
|
break;
|
|
209
153
|
}
|
|
210
|
-
streamingEventsRef.current.push(
|
|
154
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
211
155
|
syncMessages();
|
|
212
156
|
break;
|
|
213
157
|
}
|
|
214
158
|
|
|
215
159
|
case "tool_use": {
|
|
216
|
-
const pid =
|
|
217
|
-
if (pid && routeToParent(
|
|
160
|
+
const pid = ev.parentToolUseId as string | undefined;
|
|
161
|
+
if (pid && routeToParent(ev as ChatEvent, pid)) {
|
|
218
162
|
syncMessages();
|
|
219
163
|
break;
|
|
220
164
|
}
|
|
221
|
-
streamingEventsRef.current.push(
|
|
165
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
222
166
|
syncMessages();
|
|
223
167
|
break;
|
|
224
168
|
}
|
|
225
169
|
|
|
226
170
|
case "tool_result": {
|
|
227
|
-
const pid =
|
|
228
|
-
if (pid && routeToParent(
|
|
171
|
+
const pid = ev.parentToolUseId as string | undefined;
|
|
172
|
+
if (pid && routeToParent(ev as ChatEvent, pid)) {
|
|
229
173
|
syncMessages();
|
|
230
174
|
break;
|
|
231
175
|
}
|
|
232
|
-
streamingEventsRef.current.push(
|
|
176
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
233
177
|
syncMessages();
|
|
234
178
|
break;
|
|
235
179
|
}
|
|
236
180
|
|
|
237
181
|
case "approval_request": {
|
|
238
|
-
streamingEventsRef.current.push(
|
|
182
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
239
183
|
setPendingApproval({
|
|
240
|
-
requestId:
|
|
241
|
-
tool:
|
|
242
|
-
input:
|
|
184
|
+
requestId: ev.requestId,
|
|
185
|
+
tool: ev.tool,
|
|
186
|
+
input: ev.input,
|
|
243
187
|
});
|
|
244
|
-
// Local notification badge — only if this tab is NOT active
|
|
245
188
|
if (sessionIdRef.current && !isSessionTabActive(sessionIdRef.current)) {
|
|
246
|
-
const nType =
|
|
189
|
+
const nType = ev.tool === "AskUserQuestion" ? "question" : "approval_request";
|
|
247
190
|
useNotificationStore.getState().addNotification(sessionIdRef.current, nType, projectNameRef.current);
|
|
248
191
|
playNotificationSound(nType);
|
|
249
192
|
}
|
|
@@ -251,73 +194,148 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
251
194
|
}
|
|
252
195
|
|
|
253
196
|
case "error": {
|
|
254
|
-
streamingEventsRef.current.push(
|
|
197
|
+
streamingEventsRef.current.push(ev as ChatEvent);
|
|
255
198
|
const errEvents = [...streamingEventsRef.current];
|
|
256
199
|
setMessages((prev) => {
|
|
257
200
|
const last = prev[prev.length - 1];
|
|
258
201
|
if (last?.role === "assistant") {
|
|
259
|
-
return [
|
|
260
|
-
...prev.slice(0, -1),
|
|
261
|
-
{ ...last, events: errEvents },
|
|
262
|
-
];
|
|
202
|
+
return [...prev.slice(0, -1), { ...last, events: errEvents }];
|
|
263
203
|
}
|
|
264
|
-
return [
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
timestamp: new Date().toISOString(),
|
|
272
|
-
},
|
|
273
|
-
];
|
|
204
|
+
return [...prev, {
|
|
205
|
+
id: `error-${Date.now()}`,
|
|
206
|
+
role: "system" as const,
|
|
207
|
+
content: ev.message,
|
|
208
|
+
events: [ev as ChatEvent],
|
|
209
|
+
timestamp: new Date().toISOString(),
|
|
210
|
+
}];
|
|
274
211
|
});
|
|
275
|
-
|
|
276
|
-
setIsStreaming(false);
|
|
277
|
-
setStreamingStatus("idle");
|
|
212
|
+
// Phase reset comes from BE via phase_changed
|
|
278
213
|
break;
|
|
279
214
|
}
|
|
280
215
|
|
|
281
216
|
case "done": {
|
|
282
217
|
// Idempotent: may receive duplicate done (provider + stream loop finally)
|
|
283
|
-
if (
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
setContextWindowPct(data.contextWindowPct);
|
|
218
|
+
if (phaseRef.current === "idle") break;
|
|
219
|
+
if (ev.contextWindowPct != null) {
|
|
220
|
+
setContextWindowPct(ev.contextWindowPct);
|
|
287
221
|
}
|
|
288
|
-
// Local notification badge — only if this tab is NOT active
|
|
289
222
|
if (sessionIdRef.current && !isSessionTabActive(sessionIdRef.current)) {
|
|
290
223
|
useNotificationStore.getState().addNotification(sessionIdRef.current, "done", projectNameRef.current);
|
|
291
224
|
playNotificationSound("done");
|
|
292
225
|
}
|
|
293
|
-
// Finalize the streaming message
|
|
226
|
+
// Finalize the streaming message
|
|
294
227
|
const finalContent = streamingContentRef.current;
|
|
295
228
|
const finalEvents = [...streamingEventsRef.current];
|
|
296
229
|
setMessages((prev) => {
|
|
297
230
|
const last = prev[prev.length - 1];
|
|
298
231
|
if (last?.role === "assistant") {
|
|
299
|
-
return [
|
|
300
|
-
...
|
|
301
|
-
{
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
events: finalEvents.length > 0 ? finalEvents : last.events,
|
|
306
|
-
},
|
|
307
|
-
];
|
|
232
|
+
return [...prev.slice(0, -1), {
|
|
233
|
+
...last,
|
|
234
|
+
id: `final-${Date.now()}`,
|
|
235
|
+
content: finalContent || last.content,
|
|
236
|
+
events: finalEvents.length > 0 ? finalEvents : last.events,
|
|
237
|
+
}];
|
|
308
238
|
}
|
|
309
239
|
return prev;
|
|
310
240
|
});
|
|
311
241
|
streamingContentRef.current = "";
|
|
312
242
|
streamingEventsRef.current = [];
|
|
313
243
|
streamingAccountRef.current = null;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
setStreamingStatus("idle");
|
|
244
|
+
setStreamingAccountLabel(null);
|
|
245
|
+
// Phase transition to idle comes from BE via phase_changed
|
|
317
246
|
break;
|
|
318
247
|
}
|
|
319
248
|
}
|
|
320
|
-
}, []);
|
|
249
|
+
}, [routeToParent, syncMessages]);
|
|
250
|
+
|
|
251
|
+
const handleMessage = useCallback((event: MessageEvent) => {
|
|
252
|
+
let data: ChatWsServerMessage;
|
|
253
|
+
try {
|
|
254
|
+
data = JSON.parse(event.data as string) as ChatWsServerMessage;
|
|
255
|
+
} catch {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Ignore keepalive pings
|
|
260
|
+
if ((data as any).type === "ping") return;
|
|
261
|
+
|
|
262
|
+
// Handle title updates from SDK summary
|
|
263
|
+
if ((data as any).type === "title_updated") {
|
|
264
|
+
setSessionTitle((data as any).title ?? null);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Handle phase transitions from BE
|
|
269
|
+
if ((data as any).type === "phase_changed") {
|
|
270
|
+
const p = (data as any).phase as SessionPhase;
|
|
271
|
+
setPhase(p);
|
|
272
|
+
phaseRef.current = p;
|
|
273
|
+
setConnectingElapsed(p === "connecting" ? ((data as any).elapsed ?? 0) : 0);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Handle session state (replaces connected + status)
|
|
278
|
+
if ((data as any).type === "session_state") {
|
|
279
|
+
setIsConnected(true);
|
|
280
|
+
const state = data as any;
|
|
281
|
+
const p = state.phase as SessionPhase;
|
|
282
|
+
setPhase(p);
|
|
283
|
+
phaseRef.current = p;
|
|
284
|
+
if (state.sessionTitle) setSessionTitle(state.sessionTitle);
|
|
285
|
+
if (state.pendingApproval) {
|
|
286
|
+
setPendingApproval({
|
|
287
|
+
requestId: state.pendingApproval.requestId,
|
|
288
|
+
tool: state.pendingApproval.tool,
|
|
289
|
+
input: state.pendingApproval.input,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
// If idle, refetch history (completed turns) and hide overlay
|
|
293
|
+
if (p === "idle") {
|
|
294
|
+
refetchRef.current?.();
|
|
295
|
+
setIsReconnecting(false);
|
|
296
|
+
}
|
|
297
|
+
// If streaming, turn_events message will follow
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Handle turn_events (reconnect sync with rAF chunking)
|
|
302
|
+
if ((data as any).type === "turn_events") {
|
|
303
|
+
const events = (data as any).events as unknown[];
|
|
304
|
+
if (!events?.length) { setIsReconnecting(false); return; }
|
|
305
|
+
|
|
306
|
+
// Truncate messages after last user message
|
|
307
|
+
setMessages(prev => {
|
|
308
|
+
const lastUserIdx = prev.findLastIndex(m => m.role === "user");
|
|
309
|
+
return lastUserIdx >= 0 ? prev.slice(0, lastUserIdx + 1) : prev;
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
// Reset streaming refs
|
|
313
|
+
streamingContentRef.current = "";
|
|
314
|
+
streamingEventsRef.current = [];
|
|
315
|
+
streamingAccountRef.current = null;
|
|
316
|
+
|
|
317
|
+
// Process events in chunks via requestAnimationFrame to avoid blocking main thread
|
|
318
|
+
const CHUNK_SIZE = 100;
|
|
319
|
+
let offset = 0;
|
|
320
|
+
const processChunk = () => {
|
|
321
|
+
const end = Math.min(offset + CHUNK_SIZE, events.length);
|
|
322
|
+
for (let i = offset; i < end; i++) {
|
|
323
|
+
processStreamEvent(events[i]);
|
|
324
|
+
}
|
|
325
|
+
offset = end;
|
|
326
|
+
if (offset < events.length) {
|
|
327
|
+
requestAnimationFrame(processChunk);
|
|
328
|
+
} else {
|
|
329
|
+
setIsReconnecting(false);
|
|
330
|
+
}
|
|
331
|
+
};
|
|
332
|
+
requestAnimationFrame(processChunk);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Route content events through processStreamEvent
|
|
337
|
+
processStreamEvent(data);
|
|
338
|
+
}, [processStreamEvent]);
|
|
321
339
|
|
|
322
340
|
const wsUrl = sessionId && projectName
|
|
323
341
|
? `/ws/project/${encodeURIComponent(projectName)}/chat/${sessionId}`
|
|
@@ -336,21 +354,21 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
336
354
|
useEffect(() => {
|
|
337
355
|
let cancelled = false;
|
|
338
356
|
|
|
339
|
-
|
|
357
|
+
setPhase("idle");
|
|
358
|
+
phaseRef.current = "idle";
|
|
340
359
|
setPendingApproval(null);
|
|
341
360
|
streamingContentRef.current = "";
|
|
342
361
|
streamingEventsRef.current = [];
|
|
343
362
|
setIsConnected(false);
|
|
344
363
|
|
|
345
364
|
if (sessionId && projectName) {
|
|
346
|
-
// Load message history
|
|
347
365
|
setMessagesLoading(true);
|
|
348
366
|
fetch(`${projectUrl(projectName)}/chat/sessions/${sessionId}/messages?providerId=${providerId}`, {
|
|
349
367
|
headers: { Authorization: `Bearer ${getAuthToken()}` },
|
|
350
368
|
})
|
|
351
369
|
.then((r) => r.json())
|
|
352
370
|
.then((json: any) => {
|
|
353
|
-
if (cancelled ||
|
|
371
|
+
if (cancelled || phaseRef.current !== "idle") return;
|
|
354
372
|
if (json.ok && Array.isArray(json.data) && json.data.length > 0) {
|
|
355
373
|
setMessages(json.data);
|
|
356
374
|
} else {
|
|
@@ -358,7 +376,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
358
376
|
}
|
|
359
377
|
})
|
|
360
378
|
.catch(() => {
|
|
361
|
-
if (!cancelled &&
|
|
379
|
+
if (!cancelled && phaseRef.current === "idle") setMessages([]);
|
|
362
380
|
})
|
|
363
381
|
.finally(() => {
|
|
364
382
|
if (!cancelled) setMessagesLoading(false);
|
|
@@ -377,8 +395,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
377
395
|
if (!content.trim()) return;
|
|
378
396
|
|
|
379
397
|
// If streaming, cancel current stream first then send immediately
|
|
380
|
-
if (
|
|
381
|
-
// Finalize current streaming message
|
|
398
|
+
if (phaseRef.current !== "idle") {
|
|
382
399
|
const finalContent = streamingContentRef.current;
|
|
383
400
|
const finalEvents = [...streamingEventsRef.current];
|
|
384
401
|
setMessages((prev) => {
|
|
@@ -391,7 +408,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
391
408
|
}
|
|
392
409
|
return prev;
|
|
393
410
|
});
|
|
394
|
-
// Tell backend to abort current query
|
|
395
411
|
send(JSON.stringify({ type: "cancel" }));
|
|
396
412
|
}
|
|
397
413
|
|
|
@@ -410,9 +426,8 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
410
426
|
streamingContentRef.current = "";
|
|
411
427
|
streamingEventsRef.current = [];
|
|
412
428
|
pendingMessageRef.current = null;
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
setStreamingStatus("connecting");
|
|
429
|
+
setPhase("initializing");
|
|
430
|
+
phaseRef.current = "initializing";
|
|
416
431
|
setPendingApproval(null);
|
|
417
432
|
|
|
418
433
|
send(JSON.stringify({ type: "message", content, permissionMode: opts?.permissionMode }));
|
|
@@ -441,13 +456,11 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
441
456
|
(e as any).tool === "AskUserQuestion",
|
|
442
457
|
);
|
|
443
458
|
if (askEvt) {
|
|
444
|
-
// Mutate input to include answers — this updates the rendered ToolCard
|
|
445
459
|
const inp = (askEvt as any).input;
|
|
446
460
|
if (inp && typeof inp === "object") {
|
|
447
461
|
(inp as Record<string, unknown>).answers = data;
|
|
448
462
|
}
|
|
449
463
|
}
|
|
450
|
-
// Force re-render messages
|
|
451
464
|
setMessages((prev) => [...prev]);
|
|
452
465
|
}
|
|
453
466
|
|
|
@@ -457,10 +470,8 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
457
470
|
);
|
|
458
471
|
|
|
459
472
|
const cancelStreaming = useCallback(() => {
|
|
460
|
-
if (
|
|
461
|
-
// Tell backend to abort
|
|
473
|
+
if (phaseRef.current === "idle") return;
|
|
462
474
|
send(JSON.stringify({ type: "cancel" }));
|
|
463
|
-
// Finalize current message on FE
|
|
464
475
|
const finalContent = streamingContentRef.current;
|
|
465
476
|
const finalEvents = [...streamingEventsRef.current];
|
|
466
477
|
setMessages((prev) => {
|
|
@@ -481,16 +492,15 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
481
492
|
streamingContentRef.current = "";
|
|
482
493
|
streamingEventsRef.current = [];
|
|
483
494
|
pendingMessageRef.current = null;
|
|
484
|
-
|
|
485
|
-
|
|
495
|
+
setPhase("idle");
|
|
496
|
+
phaseRef.current = "idle";
|
|
486
497
|
setPendingApproval(null);
|
|
487
498
|
}, [send]);
|
|
488
499
|
|
|
489
500
|
const reconnect = useCallback(() => {
|
|
490
501
|
setIsConnected(false);
|
|
502
|
+
setIsReconnecting(true);
|
|
491
503
|
wsReconnect();
|
|
492
|
-
// Refetch history on manual reconnect to catch up on missed events
|
|
493
|
-
refetchRef.current?.();
|
|
494
504
|
}, [wsReconnect]);
|
|
495
505
|
|
|
496
506
|
const refetchMessages = useCallback(() => {
|
|
@@ -503,7 +513,6 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
503
513
|
.then((json: any) => {
|
|
504
514
|
if (json.ok && Array.isArray(json.data) && json.data.length > 0) {
|
|
505
515
|
setMessages(json.data);
|
|
506
|
-
// Reset streaming content refs so live tokens append cleanly after history
|
|
507
516
|
streamingContentRef.current = "";
|
|
508
517
|
streamingEventsRef.current = [];
|
|
509
518
|
}
|
|
@@ -512,19 +521,20 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
512
521
|
.finally(() => setMessagesLoading(false));
|
|
513
522
|
}, [sessionId, providerId, projectName]);
|
|
514
523
|
|
|
515
|
-
// Keep refetchRef in sync
|
|
524
|
+
// Keep refetchRef in sync
|
|
516
525
|
refetchRef.current = refetchMessages;
|
|
517
526
|
|
|
518
527
|
return {
|
|
519
528
|
messages,
|
|
520
529
|
messagesLoading,
|
|
521
530
|
isStreaming,
|
|
522
|
-
|
|
531
|
+
phase,
|
|
532
|
+
isReconnecting,
|
|
523
533
|
connectingElapsed,
|
|
524
|
-
thinkingWarningThreshold,
|
|
525
534
|
pendingApproval,
|
|
526
535
|
contextWindowPct,
|
|
527
536
|
sessionTitle,
|
|
537
|
+
streamingAccountLabel,
|
|
528
538
|
sendMessage,
|
|
529
539
|
respondToApproval,
|
|
530
540
|
cancelStreaming,
|