@hienlh/ppm 0.8.85 → 0.8.86
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/.claude.bak/agent-memory/tester/MEMORY.md +3 -0
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +32 -0
- package/CHANGELOG.md +5 -187
- package/bun.lock +0 -5
- package/dist/web/assets/{_basePickBy-5PGDJbfF.js → _basePickBy-5eBmZ_lt.js} +1 -1
- package/dist/web/assets/{_baseUniq-BT4Ow4Kk.js → _baseUniq-DimLlN0y.js} +1 -1
- package/dist/web/assets/api-settings-CFw-lh5k.js +1 -0
- package/dist/web/assets/{arc-BAOivWpI.js → arc-D4SasZrA.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CJupe6q_.js +1 -0
- package/dist/web/assets/{architectureDiagram-2XIMDMQ5-DWBCPMLF.js → architectureDiagram-2XIMDMQ5-nv0WbM7d.js} +1 -1
- package/dist/web/assets/{blockDiagram-WCTKOSBZ-TEF8Ally.js → blockDiagram-WCTKOSBZ-C1XvYrb8.js} +1 -1
- package/dist/web/assets/browser-tab-CmsL5eny.js +1 -0
- package/dist/web/assets/{c4Diagram-IC4MRINW-dV22iAsY.js → c4Diagram-IC4MRINW-CygDrbWJ.js} +1 -1
- package/dist/web/assets/channel-DmKoFTd_.js +1 -0
- package/dist/web/assets/chat-tab-CFWsf13Z.js +7 -0
- package/dist/web/assets/{chunk-4BX2VUAB-D4tOov49.js → chunk-4BX2VUAB-C2FDgsgT.js} +1 -1
- package/dist/web/assets/{chunk-55IACEB6-DJ6BynZ4.js → chunk-55IACEB6-jF4w6cat.js} +1 -1
- package/dist/web/assets/{chunk-7E7YKBS2-CiyUJxNI.js → chunk-7E7YKBS2-BVCECZFi.js} +1 -1
- package/dist/web/assets/{chunk-7R4GIKGN-BbIFzsIv.js → chunk-7R4GIKGN-DXTbeu5d.js} +2 -2
- package/dist/web/assets/{chunk-C72U2L5F-D21mS_6G.js → chunk-C72U2L5F-BaZqOsTs.js} +1 -1
- package/dist/web/assets/{chunk-EGIJ26TM-DzqmU2Z7.js → chunk-EGIJ26TM-Bky2tcH7.js} +1 -1
- package/dist/web/assets/{chunk-FMBD7UC4-DXncblvW.js → chunk-FMBD7UC4-Cp4BK9A8.js} +1 -1
- package/dist/web/assets/{chunk-GEFDOKGD-BbQkJu8C.js → chunk-GEFDOKGD-BosFEH7G.js} +1 -1
- package/dist/web/assets/chunk-GLR3WWYH-BnP-hOp6.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-DKDPTPEZ.js +1 -0
- package/dist/web/assets/{chunk-JSJVCQXG-23tyvw8k.js → chunk-JSJVCQXG-H5Gbjsbr.js} +1 -1
- package/dist/web/assets/{chunk-KX2RTZJC-sQ0o-39C.js → chunk-KX2RTZJC-CWerSUwS.js} +1 -1
- package/dist/web/assets/{chunk-KYZI473N-BcUZNnwd.js → chunk-KYZI473N-FvwP7jUy.js} +1 -1
- package/dist/web/assets/{chunk-L3YUKLVL-C7qGJrfV.js → chunk-L3YUKLVL-D1PI_ORP.js} +1 -1
- package/dist/web/assets/{chunk-MX3YWQON-BpS_PtKp.js → chunk-MX3YWQON-C7Vzk_AI.js} +1 -1
- package/dist/web/assets/{chunk-NQ4KR5QH-wMgTlP7f.js → chunk-NQ4KR5QH-BceYBGYX.js} +1 -1
- package/dist/web/assets/{chunk-O4XLMI2P-JC6EGoUz.js → chunk-O4XLMI2P-WPtzgxql.js} +1 -1
- package/dist/web/assets/{chunk-OZEHJAEY-BXhYx3nO.js → chunk-OZEHJAEY-DlHXDeLY.js} +1 -1
- package/dist/web/assets/{chunk-PQ6SQG4A-D6BTbCQw.js → chunk-PQ6SQG4A-Ci_Prygb.js} +1 -1
- package/dist/web/assets/{chunk-PU5JKC2W-Dw8ClWch.js → chunk-PU5JKC2W-CO0zMN-z.js} +1 -1
- package/dist/web/assets/chunk-QZHKN3VN-C_wpI9wz.js +1 -0
- package/dist/web/assets/{chunk-R5LLSJPH-CFwSJijQ.js → chunk-R5LLSJPH-IAEEzfpM.js} +1 -1
- package/dist/web/assets/{chunk-WL4C6EOR-DfofndiH.js → chunk-WL4C6EOR-BLXalOgc.js} +1 -1
- package/dist/web/assets/{chunk-XIRO2GV7-Djlmrely.js → chunk-XIRO2GV7-Dx1Ri_p2.js} +1 -1
- package/dist/web/assets/{chunk-XPW4576I-BPQQBakK.js → chunk-XPW4576I-m9pPGKn7.js} +1 -1
- package/dist/web/assets/{chunk-XZSTWKYB-DxAOx4hG.js → chunk-XZSTWKYB-B_08ExbI.js} +1 -1
- package/dist/web/assets/{chunk-YBOYWFTD-CeU4Q-xC.js → chunk-YBOYWFTD-DqSOVcYe.js} +1 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-B1T5uY-F.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-xs5vI3xC.js +1 -0
- package/dist/web/assets/clone-CijCFRT5.js +1 -0
- package/dist/web/assets/code-editor-H_dAh_fJ.js +1 -0
- package/dist/web/assets/{cose-bilkent-S5V4N54A-B_AWZsOP.js → cose-bilkent-S5V4N54A-DlL82QHu.js} +1 -1
- package/dist/web/assets/{dagre-Dbb5k38K.js → dagre-BmVoh2At.js} +1 -1
- package/dist/web/assets/{dagre-KLK3FWXG-BH7aWGRP.js → dagre-KLK3FWXG-sDrRW9MQ.js} +1 -1
- package/dist/web/assets/database-viewer-DBzsgEJ8.js +1 -0
- package/dist/web/assets/{diagram-E7M64L7V-B1Qz70Do.js → diagram-E7M64L7V-ChnAhgni.js} +1 -1
- package/dist/web/assets/{diagram-IFDJBPK2-k55eVqVU.js → diagram-IFDJBPK2-DW1J1uJd.js} +1 -1
- package/dist/web/assets/{diagram-P4PSJMXO-BkfNRc9U.js → diagram-P4PSJMXO-CQ32hyG_.js} +1 -1
- package/dist/web/assets/diff-viewer-DzS-OnAR.js +4 -0
- package/dist/web/assets/dist-0Va_2L7G.js +16 -0
- package/dist/web/assets/dist-D9irYETY.js +41 -0
- package/dist/web/assets/{erDiagram-INFDFZHY-CKzVujYI.js → erDiagram-INFDFZHY-6CHo6nOw.js} +1 -1
- package/dist/web/assets/{flowDiagram-PKNHOUZH-DIqcTrDV.js → flowDiagram-PKNHOUZH-DroDiNT0.js} +1 -1
- package/dist/web/assets/{ganttDiagram-A5KZAMGK-D4v7ZbVE.js → ganttDiagram-A5KZAMGK-DP0QBh8w.js} +1 -1
- package/dist/web/assets/git-graph-D3C7F8o3.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-B0KvGQG8.js +1 -0
- package/dist/web/assets/{gitGraphDiagram-K3NZZRJ6-BTXo57mF.js → gitGraphDiagram-K3NZZRJ6-DvU3JGZn.js} +1 -1
- package/dist/web/assets/{graphlib-BcsNnGcW.js → graphlib-CQBb2thr.js} +1 -1
- package/dist/web/assets/index-CIkjfera.js +31 -0
- package/dist/web/assets/index-WKLuYsBY.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-1uJ6_hCm.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-DLA5Q-3y.js +2 -0
- package/dist/web/assets/input-CGp1nFIg.js +1 -0
- package/dist/web/assets/{isEmpty-bnrF3Qbc.js → isEmpty-B4kqZBtn.js} +1 -1
- package/dist/web/assets/{ishikawaDiagram-PHBUUO56-BOyvKMmB.js → ishikawaDiagram-PHBUUO56-46yibrV5.js} +1 -1
- package/dist/web/assets/{journeyDiagram-4ABVD52K-ufoasAy6.js → journeyDiagram-4ABVD52K-BcmRwjK-.js} +1 -1
- package/dist/web/assets/{kanban-definition-K7BYSVSG-Bi0UTUeN.js → kanban-definition-K7BYSVSG-B619K53y.js} +1 -1
- package/dist/web/assets/keybindings-store-BdaoLwSo.js +1 -0
- package/dist/web/assets/{line-B78g-52T.js → line-1gcO63_w.js} +1 -1
- package/dist/web/assets/{linear-DP4mkX3m.js → linear-DfRqDoVd.js} +1 -1
- package/dist/web/assets/markdown-renderer-DH49Zag7.js +69 -0
- package/dist/web/assets/{mermaid-parser.core-DMIWdgEW.js → mermaid-parser.core-XtjZQOeM.js} +2 -2
- package/dist/web/assets/{mindmap-definition-YRQLILUH-BsfWvIoO.js → mindmap-definition-YRQLILUH-CifOFo_q.js} +1 -1
- package/dist/web/assets/{ordinal-_K3x1fkz.js → ordinal-BJYw-iDX.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-34C4o9yj.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-D9ekKlh9.js +1 -0
- package/dist/web/assets/{pieDiagram-SKSYHLDU-WP0XXw51.js → pieDiagram-SKSYHLDU-BuHUh_fO.js} +1 -1
- package/dist/web/assets/postgres-viewer-B9FYk8sD.js +1 -0
- package/dist/web/assets/{quadrantDiagram-337W2JSQ-FHMogtsh.js → quadrantDiagram-337W2JSQ-Bau_hj6Z.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-DEuXOXSD.js +1 -0
- package/dist/web/assets/{requirementDiagram-Z7DCOOCP-BatTxyWb.js → requirementDiagram-Z7DCOOCP-Cq2b-uwp.js} +1 -1
- package/dist/web/assets/{sankeyDiagram-WA2Y5GQK-ClJuW3Hv.js → sankeyDiagram-WA2Y5GQK-DrdGQxWQ.js} +1 -1
- package/dist/web/assets/{sequenceDiagram-2WXFIKYE-ByxQqGgs.js → sequenceDiagram-2WXFIKYE-qPxiTUcS.js} +1 -1
- package/dist/web/assets/settings-store-DWXGVHsE.js +2 -0
- package/dist/web/assets/settings-tab-D-q8pd-5.js +1 -0
- package/dist/web/assets/sqlite-viewer-CDqcTePw.js +1 -0
- package/dist/web/assets/{stateDiagram-RAJIS63D-f8opcZNY.js → stateDiagram-RAJIS63D-Dulj2oa8.js} +1 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CAkzLlhk.js +1 -0
- package/dist/web/assets/tab-store-BPeiymiH.js +1 -0
- package/dist/web/assets/{terminal-tab-CCDLZA5Y.js → terminal-tab-wKgpSPAT.js} +2 -2
- package/dist/web/assets/{timeline-definition-YZTLITO2-58BlOSf9.js → timeline-definition-YZTLITO2-BWyDnCYq.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-nc7a1Ia1.js +1 -0
- package/dist/web/assets/use-monaco-theme-CCBTQ0S3.js +11 -0
- package/dist/web/assets/{vennDiagram-LZ73GAT5-BOSy9ma9.js → vennDiagram-LZ73GAT5-B9Iv2bNV.js} +1 -1
- package/dist/web/assets/{xychartDiagram-JWTSCODW-z5MVJauZ.js → xychartDiagram-JWTSCODW-ChXcMzBQ.js} +1 -1
- package/dist/web/index.html +11 -12
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +7 -232
- package/docs/codebase-summary.md +3 -9
- package/docs/design-guidelines.md +0 -21
- package/docs/project-changelog.md +1 -115
- package/docs/project-roadmap.md +19 -41
- package/docs/system-architecture.md +15 -212
- package/package.json +2 -3
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/restart.ts +1 -9
- package/src/cli/commands/status.ts +0 -19
- package/src/index.ts +3 -2
- package/src/providers/claude-agent-sdk.ts +31 -94
- package/src/providers/mock-provider.ts +1 -6
- package/src/server/index.ts +166 -38
- package/src/server/routes/chat.ts +3 -52
- package/src/server/routes/project-scoped.ts +0 -2
- package/src/server/routes/proxy.ts +53 -46
- package/src/server/routes/tunnel.ts +32 -0
- package/src/server/ws/chat.ts +146 -207
- package/src/services/account-selector.service.ts +8 -16
- package/src/services/account.service.ts +13 -19
- package/src/services/claude-usage.service.ts +11 -48
- package/src/services/cloud.service.ts +6 -10
- package/src/services/db.service.ts +6 -111
- package/src/services/port-tunnel.service.ts +97 -0
- package/src/services/proxy.service.ts +19 -4
- package/src/services/supervisor.ts +25 -285
- package/src/types/api.ts +1 -9
- package/src/types/chat.ts +1 -3
- package/src/web/app.tsx +35 -41
- package/src/web/components/browser/browser-tab.tsx +97 -106
- package/src/web/components/chat/chat-history-bar.tsx +6 -72
- package/src/web/components/chat/chat-tab.tsx +16 -32
- package/src/web/components/chat/message-input.tsx +13 -107
- package/src/web/components/chat/message-list.tsx +15 -27
- package/src/web/components/chat/session-picker.tsx +31 -78
- package/src/web/components/chat/usage-badge.tsx +1 -11
- package/src/web/components/editor/code-editor.tsx +26 -36
- package/src/web/components/layout/command-palette.tsx +1 -3
- package/src/web/components/layout/editor-panel.tsx +18 -162
- package/src/web/components/layout/panel-layout.tsx +1 -17
- package/src/web/components/settings/proxy-settings-section.tsx +42 -40
- package/src/web/hooks/use-chat.ts +201 -211
- package/src/web/hooks/use-global-keybindings.ts +2 -25
- package/src/web/hooks/use-server-reload.ts +0 -9
- package/src/web/hooks/use-url-sync.ts +21 -173
- package/src/web/stores/keybindings-store.ts +0 -1
- package/src/web/stores/panel-store.ts +19 -73
- package/src/web/stores/panel-utils.ts +3 -145
- package/dist/web/assets/api-settings-Bx1GaNmQ.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DEO2f3VD.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-DaHGm_0i.js +0 -1
- package/dist/web/assets/channel-wrd-NHWf.js +0 -1
- package/dist/web/assets/chat-tab-BDYE0KHF.js +0 -8
- package/dist/web/assets/chevron-right-DeV0ehiG.js +0 -1
- package/dist/web/assets/chunk-GLR3WWYH-CzYx4w-r.js +0 -2
- package/dist/web/assets/chunk-HHEYEP7N-HRhYy3kG.js +0 -1
- package/dist/web/assets/chunk-QZHKN3VN-CYaTbeZf.js +0 -1
- package/dist/web/assets/classDiagram-VBA2DB6C-lse8oZoJ.js +0 -1
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-CxkwuInd.js +0 -1
- package/dist/web/assets/clone-LRxlvnMj.js +0 -1
- package/dist/web/assets/code-editor-DTA3c9Y8.js +0 -2
- package/dist/web/assets/csv-preview-DLqYtXxt.js +0 -10
- package/dist/web/assets/database-viewer-DXk79Nel.js +0 -1
- package/dist/web/assets/diff-viewer-HhIcsOQE.js +0 -4
- package/dist/web/assets/dist-DylI9XxN.js +0 -13
- package/dist/web/assets/dist-lF8CoYII.js +0 -41
- package/dist/web/assets/git-graph-CQtWu8yE.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-Bwna3and.js +0 -1
- package/dist/web/assets/index-CgQXpBb_.css +0 -2
- package/dist/web/assets/index-DEeeRoka.js +0 -37
- package/dist/web/assets/info-3K5VOQVL-_vRxVNUm.js +0 -1
- package/dist/web/assets/infoDiagram-LFFYTUFH-B1CX0pbC.js +0 -2
- package/dist/web/assets/input-BglMT33g.js +0 -1
- package/dist/web/assets/keybindings-store-1CJ7VX57.js +0 -1
- package/dist/web/assets/lib-BQ34Db2e.js +0 -4
- package/dist/web/assets/markdown-renderer-Brj8_LQM.js +0 -69
- package/dist/web/assets/packet-RMMSAZCW-DY5PNnZU.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-BHncZutv.js +0 -1
- package/dist/web/assets/postgres-viewer-CwkTGmqy.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-DH0AOkUy.js +0 -1
- package/dist/web/assets/react-dom-Bpkvzu3U.js +0 -1
- package/dist/web/assets/settings-tab-BDE1MsIh.js +0 -1
- package/dist/web/assets/sqlite-viewer-CFYTwgA8.js +0 -1
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-DrxVDY9q.js +0 -1
- package/dist/web/assets/tab-store-BJw7OCmy.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-B2Xkyv-K.js +0 -1
- package/dist/web/assets/use-monaco-theme-CNzekTN3.js +0 -11
- package/docs/streaming-input-guide.md +0 -267
- package/snapshot-state.md +0 -1526
- package/src/server/routes/browser-preview.ts +0 -159
- package/src/server/routes/workspace.ts +0 -35
- package/src/services/cloud-ws.service.ts +0 -227
- package/src/web/components/chat/account-rotation-settings.tsx +0 -163
- package/src/web/components/chat/chat-welcome.tsx +0 -148
- package/src/web/components/editor/csv-preview.tsx +0 -228
- package/src/web/components/editor/editor-breadcrumb.tsx +0 -216
- package/src/web/components/editor/editor-toolbar.tsx +0 -74
- package/src/web/components/shared/connection-lost-overlay.tsx +0 -89
- package/src/web/hooks/use-voice-input.ts +0 -111
- package/src/web/lib/csv-parser.ts +0 -134
- package/src/web/stores/connection-store.ts +0 -39
- package/test-tokens.mjs +0 -212
- /package/dist/web/assets/{api-client-BfBM3I7n.js → api-client-DOElml5u.js} +0 -0
- /package/dist/web/assets/{array-B9UHiPd-.js → array-CYkMkqnU.js} +0 -0
- /package/dist/web/assets/{columns-2-DpsNbZOc.js → columns-2-ChOTgl3e.js} +0 -0
- /package/dist/web/assets/{cytoscape.esm-BW-DbntU.js → cytoscape.esm-HeHO0VhB.js} +0 -0
- /package/dist/web/assets/{defaultLocale-5eAKkKJC.js → defaultLocale-Beh6XjaL.js} +0 -0
- /package/dist/web/assets/{dist-CSJdAyA9.js → dist-BUYzeuKe.js} +0 -0
- /package/dist/web/assets/{init-DlZdxViB.js → init-Rr1s_RiX.js} +0 -0
- /package/dist/web/assets/{isArrayLikeObject-B_v2FtYn.js → isArrayLikeObject-BB-mzMLb.js} +0 -0
- /package/dist/web/assets/{katex-Bqvo_ZG0.js → katex-CKoArbIw.js} +0 -0
- /package/dist/web/assets/{math-069Z4SuC.js → math-B7b0HgJF.js} +0 -0
- /package/dist/web/assets/{path-6uRLdFF7.js → path-BAQ3hXlG.js} +0 -0
- /package/dist/web/assets/{preload-helper-uTix4PVD.js → preload-helper-DeiOTZKJ.js} +0 -0
- /package/dist/web/assets/{react-ER-4DN55.js → react-Dev-wu-s.js} +0 -0
- /package/dist/web/assets/{rough.esm-JX0wREDd.js → rough.esm-Dwml_la6.js} +0 -0
- /package/dist/web/assets/{src-BqX54PbV.js → src-B_cC68fH.js} +0 -0
- /package/dist/web/assets/{table-C7X5UAEI.js → table-COiJDPRA.js} +0 -0
- /package/dist/web/assets/{tag-CCtdV063.js → tag-LMq02LfE.js} +0 -0
- /package/dist/web/assets/{utils-BNytJOb1.js → utils-btZ8C8-R.js} +0 -0
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { useState, useRef, useCallback, useEffect, memo, type KeyboardEvent, type DragEvent, type ClipboardEvent } from "react";
|
|
2
|
-
import { ArrowUp, Square, Paperclip
|
|
3
|
-
import { useVoiceInput } from "@/hooks/use-voice-input";
|
|
2
|
+
import { ArrowUp, Square, Paperclip } from "lucide-react";
|
|
4
3
|
import { api, projectUrl, getAuthToken } from "@/lib/api-client";
|
|
5
4
|
import { randomId } from "@/lib/utils";
|
|
6
5
|
import { isSupportedFile, isImageFile } from "@/lib/file-support";
|
|
@@ -68,48 +67,12 @@ export const MessageInput = memo(function MessageInput({
|
|
|
68
67
|
const [value, setValue] = useState(initialValue ?? "");
|
|
69
68
|
const [attachments, setAttachments] = useState<ChatAttachment[]>([]);
|
|
70
69
|
const [modeSelectorOpen, setModeSelectorOpen] = useState(false);
|
|
71
|
-
const [pendingSend, setPendingSend] = useState(false);
|
|
72
70
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
73
71
|
const mobileTextareaRef = useRef<HTMLTextAreaElement>(null);
|
|
74
72
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
75
73
|
const slashItemsRef = useRef<SlashItem[]>([]);
|
|
76
74
|
const fileItemsRef = useRef<FileNode[]>([]);
|
|
77
75
|
|
|
78
|
-
// Voice input (Web Speech API)
|
|
79
|
-
const voice = useVoiceInput();
|
|
80
|
-
// Store pre-voice text so voice appends to existing input
|
|
81
|
-
const preVoiceTextRef = useRef("");
|
|
82
|
-
const voiceResultCb = useCallback((text: string) => {
|
|
83
|
-
const prefix = preVoiceTextRef.current;
|
|
84
|
-
const newValue = prefix ? prefix + " " + text : text;
|
|
85
|
-
setValue(newValue);
|
|
86
|
-
// Auto-resize textarea
|
|
87
|
-
requestAnimationFrame(() => {
|
|
88
|
-
const ta = window.matchMedia("(min-width: 768px)").matches
|
|
89
|
-
? textareaRef.current
|
|
90
|
-
: mobileTextareaRef.current;
|
|
91
|
-
if (ta) {
|
|
92
|
-
ta.style.height = "auto";
|
|
93
|
-
ta.style.height = Math.min(ta.scrollHeight, 160) + "px";
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
}, []);
|
|
97
|
-
const handleVoiceToggle = useCallback(() => {
|
|
98
|
-
if (voice.isListening) {
|
|
99
|
-
voice.stop();
|
|
100
|
-
} else {
|
|
101
|
-
preVoiceTextRef.current = value.trim();
|
|
102
|
-
voice.start(voiceResultCb);
|
|
103
|
-
}
|
|
104
|
-
}, [voice.isListening, voice.start, voice.stop, value, voiceResultCb]);
|
|
105
|
-
|
|
106
|
-
// Listen for global keyboard shortcut (Cmd+Shift+V) to toggle voice
|
|
107
|
-
useEffect(() => {
|
|
108
|
-
const handler = () => { if (voice.supported) handleVoiceToggle(); };
|
|
109
|
-
window.addEventListener("toggle-voice-input", handler);
|
|
110
|
-
return () => window.removeEventListener("toggle-voice-input", handler);
|
|
111
|
-
}, [voice.supported, handleVoiceToggle]);
|
|
112
|
-
|
|
113
76
|
// Apply initialValue when it changes (e.g. "Ask AI" from command palette)
|
|
114
77
|
useEffect(() => {
|
|
115
78
|
if (initialValue) {
|
|
@@ -311,18 +274,14 @@ export const MessageInput = memo(function MessageInput({
|
|
|
311
274
|
});
|
|
312
275
|
}, []);
|
|
313
276
|
|
|
314
|
-
|
|
315
|
-
const executeSend = useCallback(() => {
|
|
277
|
+
const handleSend = useCallback(() => {
|
|
316
278
|
const trimmed = value.trim();
|
|
317
279
|
const readyAttachments = attachments.filter((a) => a.status === "ready");
|
|
318
|
-
if (!trimmed && readyAttachments.length === 0)
|
|
319
|
-
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
280
|
+
if (!trimmed && readyAttachments.length === 0) return;
|
|
281
|
+
if (disabled) return;
|
|
322
282
|
|
|
323
283
|
onSlashStateChange?.(false, "");
|
|
324
284
|
onFileStateChange?.(false, "");
|
|
325
|
-
if (voice.isListening) voice.stop();
|
|
326
285
|
onSend(trimmed, readyAttachments);
|
|
327
286
|
setValue("");
|
|
328
287
|
// Revoke preview URLs
|
|
@@ -330,32 +289,9 @@ export const MessageInput = memo(function MessageInput({
|
|
|
330
289
|
if (att.previewUrl) URL.revokeObjectURL(att.previewUrl);
|
|
331
290
|
}
|
|
332
291
|
setAttachments([]);
|
|
333
|
-
setPendingSend(false);
|
|
334
292
|
if (textareaRef.current) textareaRef.current.style.height = "auto";
|
|
335
293
|
if (mobileTextareaRef.current) mobileTextareaRef.current.style.height = "auto";
|
|
336
|
-
}, [value, attachments, onSend, onSlashStateChange, onFileStateChange]);
|
|
337
|
-
|
|
338
|
-
const handleSend = useCallback(() => {
|
|
339
|
-
if (disabled) return;
|
|
340
|
-
|
|
341
|
-
// If files are still uploading, queue the send for when they finish
|
|
342
|
-
if (attachments.some((a) => a.status === "uploading")) {
|
|
343
|
-
const trimmed = value.trim();
|
|
344
|
-
if (trimmed || attachments.some((a) => a.status !== "error")) {
|
|
345
|
-
setPendingSend(true);
|
|
346
|
-
}
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
executeSend();
|
|
351
|
-
}, [value, attachments, disabled, executeSend]);
|
|
352
|
-
|
|
353
|
-
// Auto-send when queued and all uploads complete
|
|
354
|
-
useEffect(() => {
|
|
355
|
-
if (!pendingSend) return;
|
|
356
|
-
if (attachments.some((a) => a.status === "uploading")) return;
|
|
357
|
-
executeSend();
|
|
358
|
-
}, [pendingSend, attachments, executeSend]);
|
|
294
|
+
}, [value, attachments, disabled, onSend, onSlashStateChange, onFileStateChange]);
|
|
359
295
|
|
|
360
296
|
const handleKeyDown = useCallback(
|
|
361
297
|
(e: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
@@ -468,7 +404,7 @@ export const MessageInput = memo(function MessageInput({
|
|
|
468
404
|
[processFiles],
|
|
469
405
|
);
|
|
470
406
|
|
|
471
|
-
const hasContent = value.trim().length > 0 || attachments.some((a) => a.status
|
|
407
|
+
const hasContent = value.trim().length > 0 || attachments.some((a) => a.status === "ready");
|
|
472
408
|
const showCancel = isStreaming && !hasContent;
|
|
473
409
|
|
|
474
410
|
return (
|
|
@@ -502,7 +438,7 @@ export const MessageInput = memo(function MessageInput({
|
|
|
502
438
|
onOpenChange={setModeSelectorOpen}
|
|
503
439
|
/>
|
|
504
440
|
</div>
|
|
505
|
-
{/* Mobile: single row — attach +
|
|
441
|
+
{/* Mobile: single row — attach + textarea + send */}
|
|
506
442
|
<div className="flex items-end gap-1 md:hidden px-2 py-2">
|
|
507
443
|
<button
|
|
508
444
|
type="button"
|
|
@@ -513,21 +449,6 @@ export const MessageInput = memo(function MessageInput({
|
|
|
513
449
|
>
|
|
514
450
|
<Paperclip className="size-4" />
|
|
515
451
|
</button>
|
|
516
|
-
{voice.supported && (
|
|
517
|
-
<button
|
|
518
|
-
type="button"
|
|
519
|
-
onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
|
|
520
|
-
disabled={disabled}
|
|
521
|
-
className={`flex items-center justify-center size-7 shrink-0 rounded-full transition-colors disabled:opacity-50 ${
|
|
522
|
-
voice.isListening
|
|
523
|
-
? "bg-red-600 text-white animate-pulse"
|
|
524
|
-
: "text-text-subtle hover:text-text-primary"
|
|
525
|
-
}`}
|
|
526
|
-
aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
|
|
527
|
-
>
|
|
528
|
-
{voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
|
|
529
|
-
</button>
|
|
530
|
-
)}
|
|
531
452
|
<textarea
|
|
532
453
|
ref={mobileTextareaRef}
|
|
533
454
|
value={value}
|
|
@@ -551,12 +472,12 @@ export const MessageInput = memo(function MessageInput({
|
|
|
551
472
|
</button>
|
|
552
473
|
) : (
|
|
553
474
|
<button
|
|
554
|
-
onClick={(e) => { e.stopPropagation();
|
|
475
|
+
onClick={(e) => { e.stopPropagation(); handleSend(); }}
|
|
555
476
|
disabled={disabled || !hasContent}
|
|
556
477
|
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"
|
|
557
|
-
aria-label=
|
|
478
|
+
aria-label="Send"
|
|
558
479
|
>
|
|
559
|
-
|
|
480
|
+
<ArrowUp className="size-3.5" />
|
|
560
481
|
</button>
|
|
561
482
|
)}
|
|
562
483
|
</div>
|
|
@@ -587,21 +508,6 @@ export const MessageInput = memo(function MessageInput({
|
|
|
587
508
|
>
|
|
588
509
|
<Paperclip className="size-4" />
|
|
589
510
|
</button>
|
|
590
|
-
{voice.supported && (
|
|
591
|
-
<button
|
|
592
|
-
type="button"
|
|
593
|
-
onClick={(e) => { e.stopPropagation(); handleVoiceToggle(); }}
|
|
594
|
-
disabled={disabled}
|
|
595
|
-
className={`flex items-center justify-center size-8 rounded-full transition-colors disabled:opacity-50 ${
|
|
596
|
-
voice.isListening
|
|
597
|
-
? "bg-red-600 text-white animate-pulse"
|
|
598
|
-
: "text-text-subtle hover:text-text-primary hover:bg-surface-elevated"
|
|
599
|
-
}`}
|
|
600
|
-
aria-label={voice.isListening ? "Stop voice input" : "Start voice input"}
|
|
601
|
-
>
|
|
602
|
-
{voice.isListening ? <MicOff className="size-4" /> : <Mic className="size-4" />}
|
|
603
|
-
</button>
|
|
604
|
-
)}
|
|
605
511
|
{/* Mode indicator chip */}
|
|
606
512
|
<div className="relative">
|
|
607
513
|
<ModeChip
|
|
@@ -627,12 +533,12 @@ export const MessageInput = memo(function MessageInput({
|
|
|
627
533
|
</button>
|
|
628
534
|
) : (
|
|
629
535
|
<button
|
|
630
|
-
onClick={(e) => { e.stopPropagation();
|
|
536
|
+
onClick={(e) => { e.stopPropagation(); handleSend(); }}
|
|
631
537
|
disabled={disabled || !hasContent}
|
|
632
538
|
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"
|
|
633
|
-
aria-label=
|
|
539
|
+
aria-label="Send message"
|
|
634
540
|
>
|
|
635
|
-
|
|
541
|
+
<ArrowUp className="size-4" />
|
|
636
542
|
</button>
|
|
637
543
|
)}
|
|
638
544
|
</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 { StreamingStatus } from "@/hooks/use-chat";
|
|
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,8 +39,9 @@ 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
|
+
streamingStatus?: StreamingStatus;
|
|
43
43
|
connectingElapsed?: number;
|
|
44
|
+
thinkingWarningThreshold?: number;
|
|
44
45
|
projectName?: string;
|
|
45
46
|
/** Called when user clicks Fork/Rewind — opens new forked chat tab */
|
|
46
47
|
onFork?: (userMessage: string) => void;
|
|
@@ -52,8 +53,9 @@ export function MessageList({
|
|
|
52
53
|
pendingApproval,
|
|
53
54
|
onApprovalResponse,
|
|
54
55
|
isStreaming,
|
|
55
|
-
|
|
56
|
+
streamingStatus,
|
|
56
57
|
connectingElapsed,
|
|
58
|
+
thinkingWarningThreshold,
|
|
57
59
|
projectName,
|
|
58
60
|
onFork,
|
|
59
61
|
}: MessageListProps) {
|
|
@@ -106,7 +108,7 @@ export function MessageList({
|
|
|
106
108
|
: <ApprovalCard approval={pendingApproval} onRespond={onApprovalResponse} />
|
|
107
109
|
)}
|
|
108
110
|
|
|
109
|
-
{isStreaming && <ThinkingIndicator lastMessage={messages[messages.length - 1]}
|
|
111
|
+
{isStreaming && <ThinkingIndicator lastMessage={messages[messages.length - 1]} streamingStatus={streamingStatus} elapsed={connectingElapsed} warningThreshold={thinkingWarningThreshold} />}
|
|
110
112
|
</StickToBottom.Content>
|
|
111
113
|
<ScrollToBottomButton />
|
|
112
114
|
</StickToBottom>
|
|
@@ -610,13 +612,6 @@ function InterleavedEvents({ events, isStreaming, projectName }: { events: ChatE
|
|
|
610
612
|
groups.push({ kind: "thinking", content: thinkingBuffer });
|
|
611
613
|
thinkingBuffer = "";
|
|
612
614
|
}
|
|
613
|
-
if (event.type === "account_retry") {
|
|
614
|
-
if (textBuffer) { groups.push({ kind: "text", content: textBuffer }); textBuffer = ""; }
|
|
615
|
-
const label = (event as any).accountLabel ?? "another account";
|
|
616
|
-
const reason = (event as any).reason ?? "Auth failed";
|
|
617
|
-
groups.push({ kind: "text", content: `\n\n> ↻ ${reason} — retrying with **${label}**...\n\n` });
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
615
|
if (event.type === "text") {
|
|
621
616
|
textBuffer += event.content;
|
|
622
617
|
} else if (event.type === "tool_use") {
|
|
@@ -726,11 +721,9 @@ function ThinkingBlock({ content, isStreaming }: { content: string; isStreaming:
|
|
|
726
721
|
{!isStreaming && <span className="text-text-subtle/50 ml-auto">{content.length > 100 ? `${Math.round(content.length / 4)} tokens` : ""}</span>}
|
|
727
722
|
</button>
|
|
728
723
|
{expanded && (
|
|
729
|
-
<
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
</StickToBottom.Content>
|
|
733
|
-
</StickToBottom>
|
|
724
|
+
<div className="px-2 pb-2 text-text-subtle/80 whitespace-pre-wrap max-h-60 overflow-y-auto text-[11px] leading-relaxed">
|
|
725
|
+
{content}
|
|
726
|
+
</div>
|
|
734
727
|
)}
|
|
735
728
|
</div>
|
|
736
729
|
);
|
|
@@ -758,13 +751,14 @@ function StreamingText({ content, animate: isStreaming, projectName }: { content
|
|
|
758
751
|
* - After tool: "Processing..."
|
|
759
752
|
* - Text streaming: hidden
|
|
760
753
|
*/
|
|
761
|
-
function ThinkingIndicator({ lastMessage,
|
|
762
|
-
// Show
|
|
754
|
+
function ThinkingIndicator({ lastMessage, streamingStatus, elapsed, warningThreshold = 15 }: { lastMessage?: ChatMessage; streamingStatus?: StreamingStatus; elapsed?: number; warningThreshold?: number }) {
|
|
755
|
+
// Show "Thinking" when:
|
|
763
756
|
// 1. No assistant message yet (waiting for first response)
|
|
764
|
-
// 2. Last event is tool_result (Claude thinking after tool execution)
|
|
757
|
+
// 2. Last event is tool_use/tool_result (Claude thinking after tool execution)
|
|
765
758
|
// Hide when text is actively streaming (text itself is the indicator)
|
|
766
759
|
|
|
767
760
|
const isWaiting = !lastMessage || lastMessage.role !== "assistant";
|
|
761
|
+
// Show Thinking only after tool_result (tool finished), not tool_use (tool still running)
|
|
768
762
|
const isAfterTool = (() => {
|
|
769
763
|
if (!lastMessage?.events?.length) return false;
|
|
770
764
|
const last = lastMessage.events[lastMessage.events.length - 1]!;
|
|
@@ -773,19 +767,13 @@ function ThinkingIndicator({ lastMessage, phase, elapsed }: { lastMessage?: Chat
|
|
|
773
767
|
|
|
774
768
|
if (!isWaiting && !isAfterTool) return null;
|
|
775
769
|
|
|
776
|
-
const
|
|
777
|
-
: phase === "connecting" ? "Connecting"
|
|
778
|
-
: phase === "thinking" ? "Thinking"
|
|
779
|
-
: "Processing";
|
|
780
|
-
|
|
781
|
-
const isLong = phase === "connecting" && (elapsed ?? 0) >= 30;
|
|
782
|
-
|
|
770
|
+
const isLong = isWaiting && (elapsed ?? 0) >= warningThreshold;
|
|
783
771
|
return (
|
|
784
772
|
<div className="flex flex-col gap-1 text-sm">
|
|
785
773
|
<div className="flex items-center gap-2 text-text-subtle">
|
|
786
774
|
<Loader2 className="size-3 animate-spin" />
|
|
787
775
|
<span>
|
|
788
|
-
|
|
776
|
+
Thinking
|
|
789
777
|
{isWaiting && (elapsed ?? 0) > 0 && <span className="text-text-subtle/60">... ({elapsed}s)</span>}
|
|
790
778
|
</span>
|
|
791
779
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback } from "react";
|
|
2
2
|
import { api, projectUrl } from "@/lib/api-client";
|
|
3
|
-
import { Plus, Trash2, MessageSquare, ChevronDown
|
|
3
|
+
import { Plus, Trash2, MessageSquare, ChevronDown } from "lucide-react";
|
|
4
4
|
import type { SessionInfo } from "../../../types/chat";
|
|
5
5
|
|
|
6
6
|
interface SessionPickerProps {
|
|
@@ -57,75 +57,6 @@ export function SessionPicker({
|
|
|
57
57
|
}
|
|
58
58
|
};
|
|
59
59
|
|
|
60
|
-
const handleTogglePin = async (e: React.MouseEvent, session: SessionInfo) => {
|
|
61
|
-
e.stopPropagation();
|
|
62
|
-
if (!projectName) return;
|
|
63
|
-
const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
|
|
64
|
-
try {
|
|
65
|
-
if (session.pinned) {
|
|
66
|
-
await api.del(url);
|
|
67
|
-
} else {
|
|
68
|
-
await api.put(url);
|
|
69
|
-
}
|
|
70
|
-
setSessions((prev) => {
|
|
71
|
-
const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
|
|
72
|
-
return updated.sort((a, b) => {
|
|
73
|
-
if (a.pinned && !b.pinned) return -1;
|
|
74
|
-
if (!a.pinned && b.pinned) return 1;
|
|
75
|
-
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
} catch {
|
|
79
|
-
// Silently fail
|
|
80
|
-
}
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
function renderSessionRow(session: SessionInfo) {
|
|
84
|
-
return (
|
|
85
|
-
<div
|
|
86
|
-
key={session.id}
|
|
87
|
-
onClick={() => {
|
|
88
|
-
onSelectSession(session);
|
|
89
|
-
setOpen(false);
|
|
90
|
-
}}
|
|
91
|
-
className={`group flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-surface-elevated transition-colors ${
|
|
92
|
-
session.id === currentSessionId
|
|
93
|
-
? "bg-surface-elevated text-text-primary"
|
|
94
|
-
: "text-text-secondary"
|
|
95
|
-
}`}
|
|
96
|
-
>
|
|
97
|
-
<div className="flex flex-col min-w-0 flex-1">
|
|
98
|
-
<span className="truncate text-xs font-medium">
|
|
99
|
-
{session.title}
|
|
100
|
-
</span>
|
|
101
|
-
<span className="text-xs text-text-subtle">
|
|
102
|
-
{new Date(session.createdAt).toLocaleDateString()}
|
|
103
|
-
</span>
|
|
104
|
-
</div>
|
|
105
|
-
<div className="flex items-center gap-0.5 shrink-0">
|
|
106
|
-
<button
|
|
107
|
-
onClick={(e) => handleTogglePin(e, session)}
|
|
108
|
-
className={`p-1 rounded transition-colors ${
|
|
109
|
-
session.pinned
|
|
110
|
-
? "text-primary hover:text-primary/70"
|
|
111
|
-
: "text-text-subtle md:opacity-0 md:group-hover:opacity-100 hover:text-text-primary"
|
|
112
|
-
}`}
|
|
113
|
-
aria-label={session.pinned ? "Unpin session" : "Pin session"}
|
|
114
|
-
>
|
|
115
|
-
{session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
116
|
-
</button>
|
|
117
|
-
<button
|
|
118
|
-
onClick={(e) => handleDelete(e, session)}
|
|
119
|
-
className="p-1 rounded hover:bg-red-500/20 text-text-subtle hover:text-red-400 transition-colors md:opacity-0 md:group-hover:opacity-100"
|
|
120
|
-
aria-label="Delete session"
|
|
121
|
-
>
|
|
122
|
-
<Trash2 className="size-3" />
|
|
123
|
-
</button>
|
|
124
|
-
</div>
|
|
125
|
-
</div>
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
60
|
return (
|
|
130
61
|
<div className="relative">
|
|
131
62
|
<button
|
|
@@ -169,14 +100,36 @@ export function SessionPicker({
|
|
|
169
100
|
No sessions yet
|
|
170
101
|
</p>
|
|
171
102
|
)}
|
|
172
|
-
{sessions.
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
103
|
+
{sessions.map((session) => (
|
|
104
|
+
<div
|
|
105
|
+
key={session.id}
|
|
106
|
+
onClick={() => {
|
|
107
|
+
onSelectSession(session);
|
|
108
|
+
setOpen(false);
|
|
109
|
+
}}
|
|
110
|
+
className={`flex items-center justify-between px-3 py-2 text-sm cursor-pointer hover:bg-surface-elevated transition-colors ${
|
|
111
|
+
session.id === currentSessionId
|
|
112
|
+
? "bg-surface-elevated text-text-primary"
|
|
113
|
+
: "text-text-secondary"
|
|
114
|
+
}`}
|
|
115
|
+
>
|
|
116
|
+
<div className="flex flex-col min-w-0 flex-1">
|
|
117
|
+
<span className="truncate text-xs font-medium">
|
|
118
|
+
{session.title}
|
|
119
|
+
</span>
|
|
120
|
+
<span className="text-xs text-text-subtle">
|
|
121
|
+
{new Date(session.createdAt).toLocaleDateString()}
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
<button
|
|
125
|
+
onClick={(e) => handleDelete(e, session)}
|
|
126
|
+
className="p-1 rounded hover:bg-red-500/20 text-text-subtle hover:text-red-400 transition-colors shrink-0"
|
|
127
|
+
aria-label="Delete session"
|
|
128
|
+
>
|
|
129
|
+
<Trash2 className="size-3" />
|
|
130
|
+
</button>
|
|
131
|
+
</div>
|
|
132
|
+
))}
|
|
180
133
|
</div>
|
|
181
134
|
</div>
|
|
182
135
|
</>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
import { Activity, RefreshCw, Eye, Download, Upload, Plus, X
|
|
2
|
+
import { Activity, RefreshCw, Eye, Download, Upload, Plus, X } from "lucide-react";
|
|
3
3
|
import { Switch } from "@/components/ui/switch";
|
|
4
4
|
import type { UsageInfo, LimitBucket } from "../../../types/chat";
|
|
5
5
|
import {
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
type OAuthProfileData,
|
|
13
13
|
} from "../../lib/api-settings";
|
|
14
14
|
import { AddAccountDialog, ExportAccountsDialog, ImportAccountsDialog } from "./account-dialogs";
|
|
15
|
-
import { AccountRotationSettings } from "./account-rotation-settings";
|
|
16
15
|
|
|
17
16
|
interface UsageBadgeProps {
|
|
18
17
|
usage: UsageInfo;
|
|
@@ -246,7 +245,6 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
246
245
|
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
247
246
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
248
247
|
const [showImportDialog, setShowImportDialog] = useState(false);
|
|
249
|
-
const [showRotationSettings, setShowRotationSettings] = useState(false);
|
|
250
248
|
const [exportPreselect, setExportPreselect] = useState<string | null>(null);
|
|
251
249
|
const [message, setMessage] = useState<string | null>(null);
|
|
252
250
|
const msgTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
@@ -340,13 +338,6 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
340
338
|
)}
|
|
341
339
|
</div>
|
|
342
340
|
<div className="flex items-center gap-1">
|
|
343
|
-
<button
|
|
344
|
-
onClick={() => setShowRotationSettings(true)}
|
|
345
|
-
className="text-xs text-text-subtle hover:text-text-primary px-1 cursor-pointer"
|
|
346
|
-
title="Rotation & retry settings"
|
|
347
|
-
>
|
|
348
|
-
<Settings className="size-3" />
|
|
349
|
-
</button>
|
|
350
341
|
{onReload && (
|
|
351
342
|
<button
|
|
352
343
|
onClick={() => { onReload(); loadAll(); }}
|
|
@@ -464,7 +455,6 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
464
455
|
<AddAccountDialog open={showAddDialog} onOpenChange={setShowAddDialog} onSuccess={handleSuccess} />
|
|
465
456
|
<ExportAccountsDialog open={showExportDialog} onOpenChange={(v) => { setShowExportDialog(v); if (!v) setExportPreselect(null); }} accounts={accounts} preselectId={exportPreselect} onMessage={showMessage} />
|
|
466
457
|
<ImportAccountsDialog open={showImportDialog} onOpenChange={setShowImportDialog} onSuccess={handleSuccess} />
|
|
467
|
-
<AccountRotationSettings open={showRotationSettings} onOpenChange={setShowRotationSettings} />
|
|
468
458
|
</div>
|
|
469
459
|
);
|
|
470
460
|
}
|
|
@@ -7,12 +7,7 @@ import { useTabStore } from "@/stores/tab-store";
|
|
|
7
7
|
import { useSettingsStore } from "@/stores/settings-store";
|
|
8
8
|
import { basename } from "@/lib/utils";
|
|
9
9
|
import { useMonacoTheme } from "@/lib/use-monaco-theme";
|
|
10
|
-
import { Loader2, FileWarning, ExternalLink } from "lucide-react";
|
|
11
|
-
import { EditorBreadcrumb } from "./editor-breadcrumb";
|
|
12
|
-
import { EditorToolbar } from "./editor-toolbar";
|
|
13
|
-
import { lazy, Suspense } from "react";
|
|
14
|
-
|
|
15
|
-
const CsvPreview = lazy(() => import("./csv-preview").then((m) => ({ default: m.CsvPreview })));
|
|
10
|
+
import { Loader2, FileWarning, ExternalLink, Code, Eye, WrapText } from "lucide-react";
|
|
16
11
|
|
|
17
12
|
/** Image extensions renderable inline */
|
|
18
13
|
const IMAGE_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp", "svg", "ico"]);
|
|
@@ -63,9 +58,7 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
63
58
|
const isPdf = ext === "pdf";
|
|
64
59
|
const isSqlite = SQLITE_EXTS.has(ext);
|
|
65
60
|
const isMarkdown = ext === "md" || ext === "mdx";
|
|
66
|
-
const isCsv = ext === "csv";
|
|
67
61
|
const [mdMode, setMdMode] = useState<"edit" | "preview">("preview");
|
|
68
|
-
const [csvMode, setCsvMode] = useState<"table" | "raw">("table");
|
|
69
62
|
|
|
70
63
|
// Redirect .db files to sqlite viewer by changing tab type
|
|
71
64
|
useEffect(() => {
|
|
@@ -203,36 +196,33 @@ export function CodeEditor({ metadata, tabId }: CodeEditorProps) {
|
|
|
203
196
|
);
|
|
204
197
|
}
|
|
205
198
|
|
|
199
|
+
const mdModeButtons = isMarkdown ? (
|
|
200
|
+
<>
|
|
201
|
+
<button type="button" onClick={() => setMdMode("edit")}
|
|
202
|
+
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "edit" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
203
|
+
>
|
|
204
|
+
<Code className="size-3" /> Edit
|
|
205
|
+
</button>
|
|
206
|
+
<button type="button" onClick={() => setMdMode("preview")}
|
|
207
|
+
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${mdMode === "preview" ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
208
|
+
>
|
|
209
|
+
<Eye className="size-3" /> Preview
|
|
210
|
+
</button>
|
|
211
|
+
</>
|
|
212
|
+
) : null;
|
|
213
|
+
|
|
214
|
+
const wrapBtn = (
|
|
215
|
+
<button type="button" onClick={toggleWordWrap} title="Toggle word wrap (Alt+Z)"
|
|
216
|
+
className={`flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors ${wordWrap ? "bg-muted text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
|
217
|
+
>
|
|
218
|
+
<WrapText className="size-3" />
|
|
219
|
+
<span className="hidden sm:inline">Wrap</span>
|
|
220
|
+
</button>
|
|
221
|
+
);
|
|
222
|
+
|
|
206
223
|
return (
|
|
207
224
|
<div className="flex flex-col h-full w-full overflow-hidden">
|
|
208
|
-
{
|
|
209
|
-
{filePath && projectName && tabId && (
|
|
210
|
-
<div className="hidden md:flex items-center h-7 border-b border-border bg-background shrink-0">
|
|
211
|
-
<EditorBreadcrumb
|
|
212
|
-
filePath={filePath}
|
|
213
|
-
projectName={projectName}
|
|
214
|
-
tabId={tabId}
|
|
215
|
-
className="flex items-center flex-1 min-w-0 overflow-x-auto scrollbar-none px-2 gap-0.5"
|
|
216
|
-
/>
|
|
217
|
-
<EditorToolbar
|
|
218
|
-
ext={ext}
|
|
219
|
-
mdMode={mdMode}
|
|
220
|
-
onMdModeChange={setMdMode}
|
|
221
|
-
csvMode={csvMode}
|
|
222
|
-
onCsvModeChange={setCsvMode}
|
|
223
|
-
wordWrap={wordWrap}
|
|
224
|
-
onToggleWordWrap={toggleWordWrap}
|
|
225
|
-
className="shrink-0 flex items-center gap-1 px-2"
|
|
226
|
-
/>
|
|
227
|
-
</div>
|
|
228
|
-
)}
|
|
229
|
-
|
|
230
|
-
{/* Content area */}
|
|
231
|
-
{isCsv && csvMode === "table" ? (
|
|
232
|
-
<Suspense fallback={<div className="flex items-center justify-center h-full"><Loader2 className="size-5 animate-spin text-text-subtle" /></div>}>
|
|
233
|
-
<CsvPreview content={content ?? ""} onContentChange={handleChange} wordWrap={wordWrap} />
|
|
234
|
-
</Suspense>
|
|
235
|
-
) : isMarkdown && mdMode === "preview" ? (
|
|
225
|
+
{isMarkdown && mdMode === "preview" ? (
|
|
236
226
|
<MarkdownPreview content={content ?? ""} />
|
|
237
227
|
) : (
|
|
238
228
|
<div className="flex-1 overflow-hidden">
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
FolderOpen,
|
|
12
12
|
Loader2,
|
|
13
13
|
Globe,
|
|
14
|
-
Mic,
|
|
15
14
|
} from "lucide-react";
|
|
16
15
|
import { useTabStore, type TabType } from "@/stores/tab-store";
|
|
17
16
|
import { useProjectStore } from "@/stores/project-store";
|
|
@@ -158,9 +157,8 @@ export function CommandPalette({ open, onClose, initialQuery = "" }: { open: boo
|
|
|
158
157
|
{ id: "chat", label: "New AI Chat", icon: MessageSquare, action: openNewTab("chat", "AI Chat"), keywords: "ai assistant claude", group: "action", shortcut: formatShortcut(getBinding("open-chat")) },
|
|
159
158
|
{ id: "terminal", label: "New Terminal", icon: Terminal, action: openNewTab("terminal", "Terminal"), keywords: "bash shell console", group: "action", shortcut: formatShortcut(getBinding("open-terminal")) },
|
|
160
159
|
{ id: "git-graph", label: "Git Graph", icon: GitBranch, action: openNewTab("git-graph", "Git Graph"), keywords: "branch history log", group: "action", shortcut: formatShortcut(getBinding("open-git-graph")) },
|
|
161
|
-
{ id: "browser", label: "Open Browser", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "web preview localhost iframe url", group: "action" },
|
|
162
160
|
{ id: "postgres", label: "PostgreSQL", icon: Database, action: openNewTab("postgres", "PostgreSQL"), keywords: "database pg sql query", group: "action" },
|
|
163
|
-
{ id: "
|
|
161
|
+
{ id: "browser", label: "Browser (Port Tunnel)", icon: Globe, action: openNewTab("browser", "Browser"), keywords: "preview tunnel port iframe web", group: "action" },
|
|
164
162
|
{ id: "git-status", label: "Git Status", icon: GitCommitHorizontal, action: () => { setSidebarActiveTab("git"); onClose(); }, keywords: "changes diff staged", group: "action", shortcut: formatShortcut(getBinding("open-git-status")) },
|
|
165
163
|
{
|
|
166
164
|
id: "settings", label: "Settings", icon: Settings,
|