@hienlh/ppm 0.8.54 → 0.8.56
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 +24 -0
- package/bun.lock +250 -1
- package/dist/web/assets/_basePickBy-CZovQgWd.js +1 -0
- package/dist/web/assets/_baseUniq-ClnvscgW.js +1 -0
- package/dist/web/assets/{api-client-TUmacMRS.js → api-client-DpGMOZNf.js} +1 -1
- package/dist/web/assets/api-settings--eVrUeZM.js +1 -0
- package/dist/web/assets/arc-C2Qaz-ch.js +1 -0
- package/dist/web/assets/architecture-PBZL5I3N-ChOahOB7.js +1 -0
- package/dist/web/assets/architectureDiagram-2XIMDMQ5-Jq91S_rs.js +36 -0
- package/dist/web/assets/array-BGFCBI0e.js +1 -0
- package/dist/web/assets/blockDiagram-WCTKOSBZ-CKGufRTy.js +132 -0
- package/dist/web/assets/c4Diagram-IC4MRINW-BNP2L9r_.js +10 -0
- package/dist/web/assets/channel-w7yboq56.js +1 -0
- package/dist/web/assets/chat-tab-CM6zFolq.js +7 -0
- package/dist/web/assets/chunk-4BX2VUAB-BptTlTyl.js +1 -0
- package/dist/web/assets/chunk-55IACEB6-C4mUdyio.js +1 -0
- package/dist/web/assets/chunk-7E7YKBS2-6xAQfBwa.js +1 -0
- package/dist/web/assets/chunk-7R4GIKGN-DXaGAn_K.js +80 -0
- package/dist/web/assets/chunk-C72U2L5F-DOtEiN5f.js +1 -0
- package/dist/web/assets/chunk-CFjPhJqf.js +1 -0
- package/dist/web/assets/chunk-EGIJ26TM-D0KJTa_T.js +1 -0
- package/dist/web/assets/chunk-FMBD7UC4-C_1aG0eb.js +15 -0
- package/dist/web/assets/chunk-GEFDOKGD-DwVPiYfW.js +2 -0
- package/dist/web/assets/chunk-GLR3WWYH-D9pZakqr.js +2 -0
- package/dist/web/assets/chunk-HHEYEP7N-Dld5BpGB.js +1 -0
- package/dist/web/assets/chunk-JSJVCQXG-BSrqCL_3.js +1 -0
- package/dist/web/assets/chunk-KX2RTZJC-BCxGmbzy.js +1 -0
- package/dist/web/assets/chunk-KYZI473N-BKO5gMeU.js +53 -0
- package/dist/web/assets/chunk-L3YUKLVL-3wBgkSvL.js +1 -0
- package/dist/web/assets/chunk-MX3YWQON-BgjSEzus.js +1 -0
- package/dist/web/assets/chunk-NQ4KR5QH-DLrZwBEm.js +220 -0
- package/dist/web/assets/chunk-O4XLMI2P-BurQy8tt.js +7 -0
- package/dist/web/assets/chunk-OZEHJAEY-YTn24bGg.js +1 -0
- package/dist/web/assets/chunk-PQ6SQG4A-BxtUGYhW.js +1 -0
- package/dist/web/assets/chunk-PU5JKC2W-B66ELkQm.js +70 -0
- package/dist/web/assets/chunk-QZHKN3VN-DwSXwtjH.js +1 -0
- package/dist/web/assets/chunk-R5LLSJPH-euR2RxLN.js +1 -0
- package/dist/web/assets/chunk-WL4C6EOR-_2CBOJdI.js +189 -0
- package/dist/web/assets/chunk-XIRO2GV7-kqQ0g6wW.js +1 -0
- package/dist/web/assets/chunk-XPW4576I-CtcaMb09.js +32 -0
- package/dist/web/assets/chunk-XZSTWKYB-BYxFzZwS.js +94 -0
- package/dist/web/assets/chunk-YBOYWFTD-Dx_fX35n.js +1 -0
- package/dist/web/assets/classDiagram-VBA2DB6C-BpJ6Oog2.js +1 -0
- package/dist/web/assets/classDiagram-v2-RAHNMMFH-Bj8gIhkP.js +1 -0
- package/dist/web/assets/clone-BSi6cgDh.js +1 -0
- package/dist/web/assets/code-editor-DgTfBijB.js +1 -0
- package/dist/web/assets/columns-2-ChOTgl3e.js +1 -0
- package/dist/web/assets/cose-bilkent-S5V4N54A-CHHjH2dV.js +1 -0
- package/dist/web/assets/cytoscape.esm-Ccan6xou.js +321 -0
- package/dist/web/assets/dagre-CNtSxiE_.js +1 -0
- package/dist/web/assets/dagre-KLK3FWXG-ChenfPp1.js +4 -0
- package/dist/web/assets/database-viewer-DSlQhR7c.js +1 -0
- package/dist/web/assets/defaultLocale-CRZydyG6.js +1 -0
- package/dist/web/assets/diagram-E7M64L7V-CzKYZM0Y.js +24 -0
- package/dist/web/assets/diagram-IFDJBPK2-ChB_paPo.js +43 -0
- package/dist/web/assets/diagram-P4PSJMXO-D1eW1dkL.js +24 -0
- package/dist/web/assets/diff-viewer-C5A-ZnrC.js +4 -0
- package/dist/web/assets/dist-Cce3efmT.js +1 -0
- package/dist/web/assets/{dist-QgqOdSYG.js → dist-T0Vhi0Mh.js} +1 -1
- package/dist/web/assets/erDiagram-INFDFZHY-mCvUFSn6.js +70 -0
- package/dist/web/assets/flowDiagram-PKNHOUZH-14ohZ1M1.js +162 -0
- package/dist/web/assets/ganttDiagram-A5KZAMGK-DIX0pLbk.js +292 -0
- package/dist/web/assets/git-graph-B5QR_Cf-.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CEee2FCA.js +1 -0
- package/dist/web/assets/gitGraphDiagram-K3NZZRJ6-yEWZbdf_.js +65 -0
- package/dist/web/assets/graphlib-DhOZxqsh.js +1 -0
- package/dist/web/assets/index-WKLuYsBY.css +2 -0
- package/dist/web/assets/index-frRaTxEm.js +37 -0
- package/dist/web/assets/info-3K5VOQVL-ce_pi3En.js +1 -0
- package/dist/web/assets/infoDiagram-LFFYTUFH-BzqyoqXw.js +2 -0
- package/dist/web/assets/init-B8gtcn7T.js +1 -0
- package/dist/web/assets/input-Brjz2Vv-.js +41 -0
- package/dist/web/assets/isArrayLikeObject-B4pdpV8V.js +1 -0
- package/dist/web/assets/isEmpty-C0YYdhYj.js +1 -0
- package/dist/web/assets/ishikawaDiagram-PHBUUO56-olazD6dZ.js +70 -0
- package/dist/web/assets/journeyDiagram-4ABVD52K-CttDH9bb.js +139 -0
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +1 -0
- package/dist/web/assets/kanban-definition-K7BYSVSG-BBXbI37U.js +89 -0
- package/dist/web/assets/katex-Bbu770d9.js +265 -0
- package/dist/web/assets/keybindings-store-Bjy78BoD.js +1 -0
- package/dist/web/assets/line-DBLLF7lH.js +1 -0
- package/dist/web/assets/linear-BLFWatDe.js +1 -0
- package/dist/web/assets/markdown-renderer-DK-YZN0m.js +69 -0
- package/dist/web/assets/math-DwgHI-Cu.js +1 -0
- package/dist/web/assets/mermaid-parser.core-BKiGOTjR.js +4 -0
- package/dist/web/assets/mindmap-definition-YRQLILUH-DoT7m4Sz.js +68 -0
- package/dist/web/assets/ordinal-CCj7PWgZ.js +1 -0
- package/dist/web/assets/packet-RMMSAZCW-CdYSLjRL.js +1 -0
- package/dist/web/assets/path-DZF-JdEe.js +1 -0
- package/dist/web/assets/pie-UPGHQEXC-Bm5LiD-6.js +1 -0
- package/dist/web/assets/pieDiagram-SKSYHLDU-Bkh2E4zE.js +30 -0
- package/dist/web/assets/postgres-viewer-CV0kVl2C.js +1 -0
- package/dist/web/assets/preload-helper-qlgyTAkD.js +1 -0
- package/dist/web/assets/quadrantDiagram-337W2JSQ-B7zgALOL.js +7 -0
- package/dist/web/assets/radar-KQ55EAFF-C4PnyG7_.js +1 -0
- package/dist/web/assets/react-BGf7KNLk.js +1 -0
- package/dist/web/assets/react-nm2Ru1Pt.js +1 -0
- package/dist/web/assets/requirementDiagram-Z7DCOOCP-D_5GXNRo.js +73 -0
- package/dist/web/assets/rough.esm-VLpapkIG.js +1 -0
- package/dist/web/assets/sankeyDiagram-WA2Y5GQK-BA9EFAAe.js +10 -0
- package/dist/web/assets/sequenceDiagram-2WXFIKYE-fyWIrHiG.js +145 -0
- package/dist/web/assets/settings-store-Bbhg_ptG.js +2 -0
- package/dist/web/assets/settings-tab-DofusrxH.js +1 -0
- package/dist/web/assets/sqlite-viewer-D5L6DIMB.js +1 -0
- package/dist/web/assets/src-BoSBNdA_.js +1 -0
- package/dist/web/assets/stateDiagram-RAJIS63D-DfRBcaBu.js +1 -0
- package/dist/web/assets/stateDiagram-v2-FVOUBMTO-CMN4M2Em.js +1 -0
- package/dist/web/assets/{tab-store-NOBndc0_.js → tab-store-dpsCvqhH.js} +1 -1
- package/dist/web/assets/{table-B6neW6Hr.js → table-Yo02WRH-.js} +1 -1
- package/dist/web/assets/{tag-DJUYe5BQ.js → tag-CaC1ng2E.js} +1 -1
- package/dist/web/assets/{terminal-tab-0Y48dynP.js → terminal-tab-lu-7WWOT.js} +2 -2
- package/dist/web/assets/timeline-definition-YZTLITO2-DYfwJ1jM.js +61 -0
- package/dist/web/assets/treemap-KZPCXAKY-2_y-mhkz.js +1 -0
- package/dist/web/assets/{use-monaco-theme-DwP4EHdO.js → use-monaco-theme-DHbyUrzJ.js} +1 -1
- package/dist/web/assets/vennDiagram-LZ73GAT5-DqbKNRD9.js +34 -0
- package/dist/web/assets/xychartDiagram-JWTSCODW-DhUL86qT.js +7 -0
- package/dist/web/index.html +13 -11
- package/dist/web/sw.js +1 -1
- package/package.json +2 -1
- package/snapshot-state.md +1526 -0
- package/src/providers/claude-agent-sdk.ts +7 -0
- package/src/server/index.ts +25 -16
- package/src/server/routes/accounts.ts +3 -8
- package/src/services/account-selector.service.ts +52 -9
- package/src/services/fs-browse.service.ts +10 -7
- package/src/web/app.tsx +8 -0
- package/src/web/components/chat/account-dialogs.tsx +377 -0
- package/src/web/components/chat/message-list.tsx +196 -45
- package/src/web/components/chat/usage-badge.tsx +94 -26
- package/src/web/components/settings/settings-tab.tsx +2 -5
- package/src/web/components/shared/diagram-overlay.tsx +139 -0
- package/src/web/components/shared/image-overlay.tsx +45 -0
- package/src/web/components/shared/markdown-renderer.tsx +55 -2
- package/src/web/stores/diagram-overlay-store.ts +16 -0
- package/src/web/stores/image-overlay-store.ts +18 -0
- package/test-tokens.mjs +212 -0
- package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
- package/dist/web/assets/api-settings-D4bgXrLU.js +0 -1
- package/dist/web/assets/chat-tab-CgVh-OsO.js +0 -7
- package/dist/web/assets/code-editor-DgvZlpB7.js +0 -1
- package/dist/web/assets/columns-2-BZ5wv2wA.js +0 -1
- package/dist/web/assets/database-viewer-CRZksTo-.js +0 -1
- package/dist/web/assets/diff-viewer-CPNLuddT.js +0 -4
- package/dist/web/assets/git-graph-BCtMSQwB.js +0 -1
- package/dist/web/assets/index-CfSJP_Fv.css +0 -2
- package/dist/web/assets/index-DcJqqWbL.js +0 -37
- package/dist/web/assets/input-CE3bFwLk.js +0 -41
- package/dist/web/assets/jsx-runtime-wQxeESYQ.js +0 -1
- package/dist/web/assets/keybindings-store-C1HiSDRb.js +0 -1
- package/dist/web/assets/markdown-renderer-Ci7qz558.js +0 -59
- package/dist/web/assets/postgres-viewer-C8PRJ87B.js +0 -1
- package/dist/web/assets/react-CYzKIDNi.js +0 -1
- package/dist/web/assets/react-rgzL83kk.js +0 -1
- package/dist/web/assets/settings-store-DL2KEbtc.js +0 -2
- package/dist/web/assets/settings-tab-CqnP28Dq.js +0 -1
- package/dist/web/assets/sqlite-viewer-BSceyudC.js +0 -1
- /package/dist/web/assets/{utils-DC-bdPS3.js → utils-btZ8C8-R.js} +0 -0
|
@@ -17,14 +17,21 @@ import {
|
|
|
17
17
|
Image as ImageIcon,
|
|
18
18
|
Copy,
|
|
19
19
|
Check,
|
|
20
|
+
CheckCircle2,
|
|
20
21
|
Loader2,
|
|
21
22
|
RotateCcw,
|
|
22
23
|
TerminalSquare,
|
|
23
24
|
ChevronUp,
|
|
24
25
|
Tag,
|
|
26
|
+
XCircle,
|
|
27
|
+
ExternalLink,
|
|
25
28
|
} from "lucide-react";
|
|
26
29
|
import { QuestionCard } from "./question-card";
|
|
27
30
|
import type { Question } from "./question-card";
|
|
31
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
32
|
+
import { api } from "@/lib/api-client";
|
|
33
|
+
import { useProjectStore } from "@/stores/project-store";
|
|
34
|
+
import { useImageOverlay } from "@/stores/image-overlay-store";
|
|
28
35
|
|
|
29
36
|
interface MessageListProps {
|
|
30
37
|
messages: ChatMessage[];
|
|
@@ -75,6 +82,9 @@ export function MessageList({
|
|
|
75
82
|
const filtered = useMemo(() => messages.filter((msg) => {
|
|
76
83
|
const hasContent = msg.content && msg.content.trim().length > 0;
|
|
77
84
|
const hasEvents = msg.events && msg.events.length > 0;
|
|
85
|
+
// User bubbles only render text — hide SDK tool-result user messages
|
|
86
|
+
// that have no text content (their events are merged into assistant)
|
|
87
|
+
if (msg.role === "user") return hasContent;
|
|
78
88
|
return hasContent || hasEvents;
|
|
79
89
|
}), [messages]);
|
|
80
90
|
|
|
@@ -147,7 +157,7 @@ function MessageBubble({ message, isStreaming, projectName, onFork }: { message:
|
|
|
147
157
|
<MarkdownContent content={message.content} projectName={projectName} />
|
|
148
158
|
</div>
|
|
149
159
|
)}
|
|
150
|
-
{
|
|
160
|
+
{message.accountLabel && (
|
|
151
161
|
<p className="text-[10px] select-none" style={{ color: "var(--color-text-subtle)" }}>
|
|
152
162
|
via {message.accountLabel}
|
|
153
163
|
</p>
|
|
@@ -172,12 +182,14 @@ const TAG_LABELS: Record<string, string> = {
|
|
|
172
182
|
"currentDate": "Date",
|
|
173
183
|
"fast_mode_info": "Fast Mode",
|
|
174
184
|
"available-deferred-tools": "Tools",
|
|
185
|
+
"task-notification": "Task Result",
|
|
186
|
+
"environment_details": "Environment",
|
|
175
187
|
};
|
|
176
188
|
|
|
177
189
|
/** Extract system-injected XML tags into structured objects + clean text */
|
|
178
190
|
function extractSystemTags(text: string): { cleanText: string; tags: SystemTag[] } {
|
|
179
191
|
const tags: SystemTag[] = [];
|
|
180
|
-
const tagPattern = /<(system-reminder|available-deferred-tools|antml:[\w-]+|fast_mode_info|claudeMd|gitStatus|currentDate)[^>]*>([\s\S]*?)<\/\1>/g;
|
|
192
|
+
const tagPattern = /<(system-reminder|available-deferred-tools|antml:[\w-]+|fast_mode_info|claudeMd|gitStatus|currentDate|task-notification|environment_details)[^>]*>([\s\S]*?)<\/\1>/g;
|
|
181
193
|
let match;
|
|
182
194
|
while ((match = tagPattern.exec(text)) !== null) {
|
|
183
195
|
const name = match[1]!;
|
|
@@ -226,6 +238,9 @@ function isPdfPath(path: string): boolean {
|
|
|
226
238
|
return path.toLowerCase().endsWith(".pdf");
|
|
227
239
|
}
|
|
228
240
|
|
|
241
|
+
/** Detect if tags contain system-injected content (not real user input) */
|
|
242
|
+
const SYSTEM_TAG_NAMES = new Set(["task-notification", "environment_details"]);
|
|
243
|
+
|
|
229
244
|
/** User message bubble — full width, collapsible, with system tag badges */
|
|
230
245
|
function UserBubble({ content, projectName, onFork }: { content: string; projectName?: string; onFork?: () => void }) {
|
|
231
246
|
const { files, text, tags } = useMemo(() => {
|
|
@@ -234,6 +249,8 @@ function UserBubble({ content, projectName, onFork }: { content: string; project
|
|
|
234
249
|
return { files: parsed.files, text: cleanText, tags };
|
|
235
250
|
}, [content]);
|
|
236
251
|
|
|
252
|
+
const isSystemContext = tags.some((t) => SYSTEM_TAG_NAMES.has(t.name));
|
|
253
|
+
|
|
237
254
|
const [expanded, setExpanded] = useState(false);
|
|
238
255
|
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
239
256
|
const contentRef = useRef<HTMLDivElement>(null);
|
|
@@ -241,32 +258,39 @@ function UserBubble({ content, projectName, onFork }: { content: string; project
|
|
|
241
258
|
useEffect(() => {
|
|
242
259
|
const el = contentRef.current;
|
|
243
260
|
if (!el) return;
|
|
244
|
-
// Compare natural height vs clamped height (5 lines ≈ 5 * 1.25rem)
|
|
245
261
|
const check = () => setIsOverflowing(el.scrollHeight > el.clientHeight + 2);
|
|
246
262
|
check();
|
|
247
|
-
// Re-check on resize
|
|
248
263
|
const ro = new ResizeObserver(check);
|
|
249
264
|
ro.observe(el);
|
|
250
265
|
return () => ro.disconnect();
|
|
251
266
|
}, [text]);
|
|
252
267
|
|
|
253
268
|
return (
|
|
254
|
-
<div className=
|
|
269
|
+
<div className={cn(
|
|
270
|
+
"group/user relative rounded-lg px-3 py-2 text-sm border shadow-sm",
|
|
271
|
+
isSystemContext
|
|
272
|
+
? "bg-surface/40 border-border/40 text-text-secondary"
|
|
273
|
+
: "bg-primary/10 border-primary/15 text-text-primary",
|
|
274
|
+
)}>
|
|
255
275
|
{/* System tags as badges */}
|
|
256
276
|
{tags.length > 0 && <SystemTagBadges tags={tags} />}
|
|
257
277
|
|
|
258
|
-
{/* Attached files —
|
|
278
|
+
{/* Attached files — image thumbnails + file chips */}
|
|
259
279
|
{files.length > 0 && (
|
|
260
280
|
<div className="flex flex-wrap gap-1.5">
|
|
261
|
-
{files.map((filePath, i) =>
|
|
262
|
-
|
|
263
|
-
key={i}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
281
|
+
{files.map((filePath, i) =>
|
|
282
|
+
isImagePath(filePath) ? (
|
|
283
|
+
<AuthImageThumbnail key={i} filePath={filePath} projectName={projectName} />
|
|
284
|
+
) : (
|
|
285
|
+
<div
|
|
286
|
+
key={i}
|
|
287
|
+
className="flex items-center gap-1 rounded-md border border-border/60 bg-background/40 px-1.5 py-0.5 text-[11px] text-text-secondary"
|
|
288
|
+
>
|
|
289
|
+
<FileText className="size-3 shrink-0" />
|
|
290
|
+
<span className="truncate max-w-32">{basename(filePath)}</span>
|
|
291
|
+
</div>
|
|
292
|
+
),
|
|
293
|
+
)}
|
|
270
294
|
</div>
|
|
271
295
|
)}
|
|
272
296
|
|
|
@@ -280,29 +304,22 @@ function UserBubble({ content, projectName, onFork }: { content: string; project
|
|
|
280
304
|
expanded && "max-h-[50vh] overflow-y-auto",
|
|
281
305
|
)}
|
|
282
306
|
>
|
|
283
|
-
{text}
|
|
307
|
+
{isSystemContext ? <TextWithFilePaths text={text} projectName={projectName} /> : text}
|
|
284
308
|
</div>
|
|
285
309
|
)}
|
|
286
310
|
{(isOverflowing || expanded) && (
|
|
287
311
|
<button
|
|
288
312
|
onClick={() => setExpanded(!expanded)}
|
|
289
|
-
className=
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
<>
|
|
293
|
-
<ChevronUp className="size-3" />
|
|
294
|
-
Show less
|
|
295
|
-
</>
|
|
296
|
-
) : (
|
|
297
|
-
<>
|
|
298
|
-
<ChevronDown className="size-3" />
|
|
299
|
-
Show more
|
|
300
|
-
</>
|
|
313
|
+
className={cn(
|
|
314
|
+
"flex items-center gap-1 text-xs mt-1 transition-colors",
|
|
315
|
+
isSystemContext ? "text-text-subtle hover:text-text-secondary" : "text-primary/70 hover:text-primary",
|
|
301
316
|
)}
|
|
317
|
+
>
|
|
318
|
+
{expanded ? <><ChevronUp className="size-3" />Show less</> : <><ChevronDown className="size-3" />Show more</>}
|
|
302
319
|
</button>
|
|
303
320
|
)}
|
|
304
|
-
{/* Fork/Rewind button —
|
|
305
|
-
{onFork && (
|
|
321
|
+
{/* Fork/Rewind button — only for real user messages */}
|
|
322
|
+
{!isSystemContext && onFork && (
|
|
306
323
|
<button
|
|
307
324
|
onClick={onFork}
|
|
308
325
|
title="Retry from this message (fork session)"
|
|
@@ -328,6 +345,12 @@ function SystemTagBadges({ tags }: { tags: SystemTag[] }) {
|
|
|
328
345
|
|
|
329
346
|
function SystemTagBadge({ tag }: { tag: SystemTag }) {
|
|
330
347
|
const [open, setOpen] = useState(false);
|
|
348
|
+
|
|
349
|
+
// Task notification: render formatted instead of raw XML
|
|
350
|
+
if (tag.name === "task-notification") {
|
|
351
|
+
return <TaskNotificationBadge content={tag.content} />;
|
|
352
|
+
}
|
|
353
|
+
|
|
331
354
|
return (
|
|
332
355
|
<div className="text-xs">
|
|
333
356
|
<button
|
|
@@ -347,29 +370,132 @@ function SystemTagBadge({ tag }: { tag: SystemTag }) {
|
|
|
347
370
|
);
|
|
348
371
|
}
|
|
349
372
|
|
|
350
|
-
/**
|
|
351
|
-
function
|
|
373
|
+
/** Extract a sub-tag value from XML-like content */
|
|
374
|
+
function xmlTag(content: string, tag: string): string | undefined {
|
|
375
|
+
const m = content.match(new RegExp(`<${tag}>([\\s\\S]*?)</${tag}>`));
|
|
376
|
+
return m?.[1]?.trim() || undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Formatted badge for <task-notification> — shows status, summary, output file, result */
|
|
380
|
+
function TaskNotificationBadge({ content }: { content: string }) {
|
|
381
|
+
const [open, setOpen] = useState(false);
|
|
382
|
+
const status = xmlTag(content, "status");
|
|
383
|
+
const summary = xmlTag(content, "summary");
|
|
384
|
+
const outputFile = xmlTag(content, "output-file");
|
|
385
|
+
const result = xmlTag(content, "result");
|
|
386
|
+
const isOk = status === "completed";
|
|
387
|
+
|
|
388
|
+
return (
|
|
389
|
+
<div className="text-xs">
|
|
390
|
+
<button
|
|
391
|
+
onClick={() => setOpen(!open)}
|
|
392
|
+
className="flex items-center gap-1.5 rounded-full border border-border/60 bg-surface/50 px-2 py-0.5 text-text-subtle hover:text-text-secondary hover:bg-surface transition-colors"
|
|
393
|
+
>
|
|
394
|
+
{isOk ? <CheckCircle2 className="size-2.5 text-green-500" /> : <XCircle className="size-2.5 text-yellow-500" />}
|
|
395
|
+
<span className="truncate max-w-80">{summary ?? "Task notification"}</span>
|
|
396
|
+
<ChevronRight className={cn("size-2.5 transition-transform shrink-0", open && "rotate-90")} />
|
|
397
|
+
</button>
|
|
398
|
+
{open && (
|
|
399
|
+
<div className="mt-1 rounded border border-border/40 bg-surface/30 px-2 py-1.5 space-y-1.5">
|
|
400
|
+
{/* Full summary (button truncates it) */}
|
|
401
|
+
{summary && <p className="text-[11px] text-text-secondary">{summary}</p>}
|
|
402
|
+
{outputFile && <FilePathChip path={outputFile} />}
|
|
403
|
+
{result && (
|
|
404
|
+
<div className="text-[11px] text-text-subtle/80 max-h-60 overflow-y-auto leading-relaxed">
|
|
405
|
+
<MarkdownContent content={result} />
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
408
|
+
</div>
|
|
409
|
+
)}
|
|
410
|
+
</div>
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/** Clickable file path chip — opens file in editor tab */
|
|
415
|
+
function FilePathChip({ path, projectName }: { path: string; projectName?: string }) {
|
|
416
|
+
const handleClick = useCallback(() => {
|
|
417
|
+
const openTab = useTabStore.getState().openTab;
|
|
418
|
+
const pName = projectName ?? useProjectStore.getState().activeProject?.name;
|
|
419
|
+
const fileName = basename(path);
|
|
420
|
+
const meta: Record<string, unknown> = { filePath: path };
|
|
421
|
+
if (pName) meta.projectName = pName;
|
|
422
|
+
// Try to verify file exists, then open; fallback: open directly
|
|
423
|
+
api.get(`/api/fs/read?path=${encodeURIComponent(path)}`).then(() => {
|
|
424
|
+
openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
|
|
425
|
+
}).catch(() => {
|
|
426
|
+
openTab({ type: "editor", title: fileName, metadata: meta, projectId: null, closable: true });
|
|
427
|
+
});
|
|
428
|
+
}, [path, projectName]);
|
|
429
|
+
|
|
430
|
+
return (
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
onClick={handleClick}
|
|
434
|
+
className="inline-flex items-center gap-1 rounded border border-border/50 bg-surface/50 px-1.5 py-0.5 font-mono text-[10px] text-text-secondary hover:text-text-primary hover:bg-surface transition-colors cursor-pointer"
|
|
435
|
+
>
|
|
436
|
+
<FileText className="size-2.5 shrink-0" />
|
|
437
|
+
<span className="truncate max-w-60">{basename(path)}</span>
|
|
438
|
+
<ExternalLink className="size-2 shrink-0 opacity-50" />
|
|
439
|
+
</button>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/** Render text with absolute file paths detected and turned into clickable chips */
|
|
444
|
+
function TextWithFilePaths({ text, projectName }: { text: string; projectName?: string }) {
|
|
445
|
+
const parts = useMemo(() => {
|
|
446
|
+
// Match absolute file paths (at least 2 segments)
|
|
447
|
+
const re = /(\/(?:[\w.\-]+\/)+[\w.\-]+)/g;
|
|
448
|
+
const result: { kind: "text" | "path"; value: string }[] = [];
|
|
449
|
+
let last = 0;
|
|
450
|
+
let m;
|
|
451
|
+
while ((m = re.exec(text)) !== null) {
|
|
452
|
+
if (m.index > last) result.push({ kind: "text", value: text.slice(last, m.index) });
|
|
453
|
+
result.push({ kind: "path", value: m[1]! });
|
|
454
|
+
last = m.index + m[0].length;
|
|
455
|
+
}
|
|
456
|
+
if (last < text.length) result.push({ kind: "text", value: text.slice(last) });
|
|
457
|
+
return result;
|
|
458
|
+
}, [text]);
|
|
459
|
+
|
|
460
|
+
return (
|
|
461
|
+
<>
|
|
462
|
+
{parts.map((p, i) =>
|
|
463
|
+
p.kind === "path"
|
|
464
|
+
? <FilePathChip key={i} path={p.value} projectName={projectName} />
|
|
465
|
+
: <span key={i}>{p.value}</span>,
|
|
466
|
+
)}
|
|
467
|
+
</>
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Hook: fetch an image via auth header, return blob URL */
|
|
472
|
+
function useAuthBlob(src: string): { blobUrl: string | null; error: boolean } {
|
|
352
473
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
353
474
|
const [error, setError] = useState(false);
|
|
354
475
|
|
|
355
476
|
useEffect(() => {
|
|
356
|
-
let
|
|
477
|
+
let revoked = false;
|
|
478
|
+
let url: string | undefined;
|
|
357
479
|
const token = getAuthToken();
|
|
358
480
|
fetch(src, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
|
|
359
|
-
.then((r) => {
|
|
360
|
-
if (!r.ok) throw new Error("Failed to load");
|
|
361
|
-
return r.blob();
|
|
362
|
-
})
|
|
481
|
+
.then((r) => { if (!r.ok) throw new Error("Failed"); return r.blob(); })
|
|
363
482
|
.then((blob) => {
|
|
364
|
-
|
|
365
|
-
|
|
483
|
+
if (revoked) return;
|
|
484
|
+
url = URL.createObjectURL(blob);
|
|
366
485
|
setBlobUrl(url);
|
|
367
486
|
})
|
|
368
|
-
.catch(() => setError(true));
|
|
369
|
-
|
|
370
|
-
return () => { if (revoke) URL.revokeObjectURL(revoke); };
|
|
487
|
+
.catch(() => { if (!revoked) setError(true); });
|
|
488
|
+
return () => { revoked = true; if (url) URL.revokeObjectURL(url); };
|
|
371
489
|
}, [src]);
|
|
372
490
|
|
|
491
|
+
return { blobUrl, error };
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/** Fetches image with auth header, renders as blob URL — click opens lightbox */
|
|
495
|
+
function AuthImage({ src, alt }: { src: string; alt: string }) {
|
|
496
|
+
const { blobUrl, error } = useAuthBlob(src);
|
|
497
|
+
const openOverlay = useImageOverlay((s) => s.open);
|
|
498
|
+
|
|
373
499
|
if (error) {
|
|
374
500
|
return (
|
|
375
501
|
<div className="flex items-center gap-1.5 rounded-md border border-border bg-background/50 px-2 py-1 text-xs text-text-secondary">
|
|
@@ -384,13 +510,38 @@ function AuthImage({ src, alt }: { src: string; alt: string }) {
|
|
|
384
510
|
}
|
|
385
511
|
|
|
386
512
|
return (
|
|
387
|
-
<
|
|
513
|
+
<button type="button" onClick={() => openOverlay(blobUrl, alt)} className="block text-left">
|
|
388
514
|
<img
|
|
389
515
|
src={blobUrl}
|
|
390
516
|
alt={alt}
|
|
391
|
-
className="rounded-md max-h-48 max-w-full object-contain border border-border"
|
|
517
|
+
className="rounded-md max-h-48 max-w-full object-contain border border-border cursor-pointer hover:opacity-90 transition-opacity"
|
|
392
518
|
/>
|
|
393
|
-
</
|
|
519
|
+
</button>
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/** Chip for attached images in user bubble — tiny preview replaces icon, click opens lightbox */
|
|
524
|
+
function AuthImageThumbnail({ filePath, projectName }: { filePath: string; projectName?: string }) {
|
|
525
|
+
const src = uploadPreviewUrl(filePath, projectName);
|
|
526
|
+
const { blobUrl, error } = useAuthBlob(src);
|
|
527
|
+
const openOverlay = useImageOverlay((s) => s.open);
|
|
528
|
+
const name = basename(filePath);
|
|
529
|
+
|
|
530
|
+
return (
|
|
531
|
+
<button
|
|
532
|
+
type="button"
|
|
533
|
+
onClick={() => blobUrl && openOverlay(blobUrl, name)}
|
|
534
|
+
className="flex items-center gap-1 rounded-md border border-border/60 bg-background/40 px-1.5 py-0.5 text-[11px] text-text-secondary hover:bg-surface transition-colors cursor-pointer"
|
|
535
|
+
>
|
|
536
|
+
{blobUrl ? (
|
|
537
|
+
<img src={blobUrl} alt={name} className="size-4 rounded-sm object-cover shrink-0" />
|
|
538
|
+
) : error ? (
|
|
539
|
+
<ImageIcon className="size-3 shrink-0" />
|
|
540
|
+
) : (
|
|
541
|
+
<div className="size-4 rounded-sm bg-surface animate-pulse shrink-0" />
|
|
542
|
+
)}
|
|
543
|
+
<span className="truncate max-w-32">{name}</span>
|
|
544
|
+
</button>
|
|
394
545
|
);
|
|
395
546
|
}
|
|
396
547
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
import { Activity, RefreshCw, Eye,
|
|
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 {
|
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
getActiveAccount,
|
|
8
8
|
getAllAccountUsages,
|
|
9
9
|
patchAccount,
|
|
10
|
-
verifyAccount,
|
|
11
10
|
type AccountInfo,
|
|
12
11
|
type AccountUsageEntry,
|
|
13
12
|
type OAuthProfileData,
|
|
14
13
|
} from "../../lib/api-settings";
|
|
14
|
+
import { AddAccountDialog, ExportAccountsDialog, ImportAccountsDialog } from "./account-dialogs";
|
|
15
15
|
|
|
16
16
|
interface UsageBadgeProps {
|
|
17
17
|
usage: UsageInfo;
|
|
@@ -97,7 +97,7 @@ function BucketRow({ label, bucket }: { label: string; bucket?: LimitBucket }) {
|
|
|
97
97
|
<div className="flex items-center justify-between">
|
|
98
98
|
<span className="text-xs font-medium text-text-primary">{label}</span>
|
|
99
99
|
{reset && (
|
|
100
|
-
<span className="text-[10px] text-text-subtle">↻ {reset}</span>
|
|
100
|
+
<span className="text-[10px] text-text-subtle" title="Resets in">↻ {reset}</span>
|
|
101
101
|
)}
|
|
102
102
|
</div>
|
|
103
103
|
<div className="flex items-center gap-2">
|
|
@@ -115,6 +115,28 @@ function BucketRow({ label, bucket }: { label: string; bucket?: LimitBucket }) {
|
|
|
115
115
|
);
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
function formatExpiry(expiresAt: number): string {
|
|
119
|
+
const diff = expiresAt - Date.now();
|
|
120
|
+
if (diff <= 0) return "expired";
|
|
121
|
+
const mins = Math.ceil(diff / 60_000);
|
|
122
|
+
const h = Math.floor(mins / 60);
|
|
123
|
+
const d = Math.floor(h / 24);
|
|
124
|
+
if (d > 0) return `${d}d ${h % 24}h`;
|
|
125
|
+
if (h > 0) return `${h}h ${mins % 60}m`;
|
|
126
|
+
return `${mins}m`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Derive a human-readable token status from account info */
|
|
130
|
+
function tokenStatus(info?: AccountInfo): { label: string; tip: string; color: string } {
|
|
131
|
+
if (!info) return { label: "unknown", tip: "No account info available", color: "text-text-subtle" };
|
|
132
|
+
if (!info.expiresAt) return { label: "key", tip: "API key (no expiry)", color: "text-text-subtle" };
|
|
133
|
+
const expired = info.expiresAt * 1000 < Date.now(); // expiresAt is seconds
|
|
134
|
+
if (expired && info.hasRefreshToken) return { label: "expired", tip: "Token expired but has refresh token — will auto-renew", color: "text-amber-500" };
|
|
135
|
+
if (expired) return { label: "expired", tip: "Token expired, no refresh token", color: "text-red-500" };
|
|
136
|
+
if (info.hasRefreshToken) return { label: "long-lived", tip: "OAuth token with refresh — long-lived", color: "text-green-500" };
|
|
137
|
+
return { label: "temp", tip: "Temporary token without refresh — will expire", color: "text-amber-500" };
|
|
138
|
+
}
|
|
139
|
+
|
|
118
140
|
function formatLastUpdated(ts: number | null | undefined): string | null {
|
|
119
141
|
if (!ts) return null;
|
|
120
142
|
const secs = Math.round((Date.now() - ts) / 1000);
|
|
@@ -129,13 +151,12 @@ function formatLastUpdated(ts: number | null | undefined): string | null {
|
|
|
129
151
|
return `${days}d ago`;
|
|
130
152
|
}
|
|
131
153
|
|
|
132
|
-
function AccountUsageCard({ entry, isActive, accountInfo, onToggle,
|
|
154
|
+
function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, onViewProfile, flash }: {
|
|
133
155
|
entry: AccountUsageEntry;
|
|
134
156
|
isActive: boolean;
|
|
135
157
|
accountInfo?: AccountInfo;
|
|
136
158
|
onToggle?: (id: string, status: string) => void;
|
|
137
|
-
|
|
138
|
-
verifyingId?: string | null;
|
|
159
|
+
onExport?: (id: string) => void;
|
|
139
160
|
onViewProfile?: (profile: OAuthProfileData) => void;
|
|
140
161
|
flash?: boolean;
|
|
141
162
|
}) {
|
|
@@ -144,7 +165,7 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onVerify, ve
|
|
|
144
165
|
const status = accountInfo?.status ?? entry.accountStatus;
|
|
145
166
|
|
|
146
167
|
return (
|
|
147
|
-
<div className={`rounded-md border p-2 space-y-1.5 transition-colors duration-500 ${flash ? "bg-primary/10 border-primary/40" : ""} ${isActive ? "border-primary/30 bg-primary/5" : "border-border/50"}`}>
|
|
168
|
+
<div className={`rounded-md border p-2 space-y-1.5 transition-colors duration-500 min-w-[200px] shrink-0 snap-start ${flash ? "bg-primary/10 border-primary/40" : ""} ${isActive ? "border-primary/30 bg-primary/5" : "border-border/50"}`}>
|
|
148
169
|
<div className="flex items-center gap-1.5">
|
|
149
170
|
<span className="text-xs font-medium truncate flex-1 min-w-0">
|
|
150
171
|
{entry.accountLabel ?? entry.accountId.slice(0, 8)}
|
|
@@ -163,14 +184,13 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onVerify, ve
|
|
|
163
184
|
<Eye className="size-3" />
|
|
164
185
|
</button>
|
|
165
186
|
)}
|
|
166
|
-
{
|
|
187
|
+
{onExport && entry.isOAuth && (
|
|
167
188
|
<button
|
|
168
|
-
className="p-1 rounded cursor-pointer text-text-subtle hover:text-
|
|
169
|
-
onClick={() =>
|
|
170
|
-
|
|
171
|
-
title="Verify token"
|
|
189
|
+
className="p-1 rounded cursor-pointer text-text-subtle hover:text-blue-500 hover:bg-surface-elevated transition-colors"
|
|
190
|
+
onClick={() => onExport(entry.accountId)}
|
|
191
|
+
title="Export this account"
|
|
172
192
|
>
|
|
173
|
-
|
|
193
|
+
<Download className="size-3" />
|
|
174
194
|
</button>
|
|
175
195
|
)}
|
|
176
196
|
{onToggle && (
|
|
@@ -195,11 +215,21 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onVerify, ve
|
|
|
195
215
|
{entry.isOAuth ? "No usage data yet" : "Usage tracking not available for API keys"}
|
|
196
216
|
</p>
|
|
197
217
|
)}
|
|
198
|
-
{
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
218
|
+
{/* Footer: updated · expires · type */}
|
|
219
|
+
{(() => {
|
|
220
|
+
const ts = tokenStatus(accountInfo);
|
|
221
|
+
return (
|
|
222
|
+
<div className="flex items-center gap-1.5 text-[9px] text-text-subtle flex-wrap">
|
|
223
|
+
{usage.lastFetchedAt && (
|
|
224
|
+
<span title="Last usage data update">↻ {formatLastUpdated(new Date(usage.lastFetchedAt).getTime())}</span>
|
|
225
|
+
)}
|
|
226
|
+
{accountInfo?.expiresAt && accountInfo.expiresAt * 1000 > Date.now() && (
|
|
227
|
+
<span title="Token expires in">⏱ {formatExpiry(accountInfo.expiresAt * 1000)}</span>
|
|
228
|
+
)}
|
|
229
|
+
<span className={ts.color} title={ts.tip}>© {ts.label}</span>
|
|
230
|
+
</div>
|
|
231
|
+
);
|
|
232
|
+
})()}
|
|
203
233
|
</div>
|
|
204
234
|
);
|
|
205
235
|
}
|
|
@@ -211,10 +241,26 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
211
241
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
212
242
|
const [refreshing, setRefreshing] = useState(false);
|
|
213
243
|
const [flashIds, setFlashIds] = useState<Set<string>>(new Set());
|
|
214
|
-
const [verifyingId, setVerifyingId] = useState<string | null>(null);
|
|
215
244
|
const [profileView, setProfileView] = useState<OAuthProfileData | null>(null);
|
|
245
|
+
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
246
|
+
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
247
|
+
const [showImportDialog, setShowImportDialog] = useState(false);
|
|
248
|
+
const [exportPreselect, setExportPreselect] = useState<string | null>(null);
|
|
249
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
250
|
+
const msgTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
216
251
|
const prevUsagesRef = useRef<AccountUsageEntry[]>([]);
|
|
217
252
|
|
|
253
|
+
function showMessage(msg: string) {
|
|
254
|
+
if (msgTimer.current) clearTimeout(msgTimer.current);
|
|
255
|
+
setMessage(msg);
|
|
256
|
+
msgTimer.current = setTimeout(() => setMessage(null), 4000);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function handleSuccess(msg?: string) {
|
|
260
|
+
loadAll();
|
|
261
|
+
if (msg) showMessage(msg);
|
|
262
|
+
}
|
|
263
|
+
|
|
218
264
|
async function loadAll() {
|
|
219
265
|
const isRefresh = allUsages.length > 0;
|
|
220
266
|
if (isRefresh) setRefreshing(true); else setInitialLoading(true);
|
|
@@ -277,10 +323,9 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
277
323
|
onReload?.();
|
|
278
324
|
}
|
|
279
325
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
setVerifyingId(null);
|
|
326
|
+
function openExportAll() {
|
|
327
|
+
setExportPreselect(null);
|
|
328
|
+
setShowExportDialog(true);
|
|
284
329
|
}
|
|
285
330
|
|
|
286
331
|
return (
|
|
@@ -312,8 +357,14 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
312
357
|
</div>
|
|
313
358
|
</div>
|
|
314
359
|
|
|
360
|
+
{message && (
|
|
361
|
+
<div className="text-[11px] p-1.5 rounded bg-green-500/10 text-green-600 text-center animate-in fade-in duration-200">
|
|
362
|
+
{message}
|
|
363
|
+
</div>
|
|
364
|
+
)}
|
|
365
|
+
|
|
315
366
|
{(hasMultipleAccounts || initialLoading) ? (
|
|
316
|
-
<div className="
|
|
367
|
+
<div className="flex gap-1.5 overflow-x-auto pb-1 -mx-3 px-3 snap-x snap-mandatory scrollbar-thin">
|
|
317
368
|
{initialLoading ? (
|
|
318
369
|
<p className="text-[10px] text-text-subtle">Loading...</p>
|
|
319
370
|
) : (
|
|
@@ -324,8 +375,7 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
324
375
|
isActive={entry.accountId === (activeAccountId ?? usage.activeAccountId)}
|
|
325
376
|
accountInfo={accountMap.get(entry.accountId)}
|
|
326
377
|
onToggle={handleToggle}
|
|
327
|
-
|
|
328
|
-
verifyingId={verifyingId}
|
|
378
|
+
onExport={(id) => { setExportPreselect(id); setShowExportDialog(true); }}
|
|
329
379
|
onViewProfile={setProfileView}
|
|
330
380
|
flash={flashIds.has(entry.accountId)}
|
|
331
381
|
/>
|
|
@@ -387,6 +437,24 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
387
437
|
</div>
|
|
388
438
|
</div>
|
|
389
439
|
)}
|
|
440
|
+
|
|
441
|
+
{/* Action buttons */}
|
|
442
|
+
<div className="border-t border-border pt-2 flex gap-1.5">
|
|
443
|
+
<button onClick={() => setShowAddDialog(true)} className="flex-1 flex items-center justify-center gap-1 rounded-md border border-border px-2 py-1 text-[11px] text-text-secondary hover:bg-surface-hover transition-colors cursor-pointer">
|
|
444
|
+
<Plus className="size-3" /> Add
|
|
445
|
+
</button>
|
|
446
|
+
<button onClick={openExportAll} className="flex-1 flex items-center justify-center gap-1 rounded-md border border-border px-2 py-1 text-[11px] text-text-secondary hover:bg-surface-hover transition-colors cursor-pointer">
|
|
447
|
+
<Download className="size-3" /> Export
|
|
448
|
+
</button>
|
|
449
|
+
<button onClick={() => setShowImportDialog(true)} className="flex-1 flex items-center justify-center gap-1 rounded-md border border-border px-2 py-1 text-[11px] text-text-secondary hover:bg-surface-hover transition-colors cursor-pointer">
|
|
450
|
+
<Upload className="size-3" /> Import
|
|
451
|
+
</button>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
{/* Account dialogs */}
|
|
455
|
+
<AddAccountDialog open={showAddDialog} onOpenChange={setShowAddDialog} onSuccess={handleSuccess} />
|
|
456
|
+
<ExportAccountsDialog open={showExportDialog} onOpenChange={(v) => { setShowExportDialog(v); if (!v) setExportPreselect(null); }} accounts={accounts} preselectId={exportPreselect} onMessage={showMessage} />
|
|
457
|
+
<ImportAccountsDialog open={showImportDialog} onOpenChange={setShowImportDialog} onSuccess={handleSuccess} />
|
|
390
458
|
</div>
|
|
391
459
|
);
|
|
392
460
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Moon, Sun, Monitor, Bell, BellOff, Check, ChevronRight, ArrowLeft,
|
|
4
|
-
Bot, BellRing,
|
|
4
|
+
Bot, BellRing, Keyboard, Globe,
|
|
5
5
|
} from "lucide-react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
7
7
|
import { Input } from "@/components/ui/input";
|
|
@@ -12,7 +12,6 @@ import { cn } from "@/lib/utils";
|
|
|
12
12
|
import { AISettingsSection } from "./ai-settings-section";
|
|
13
13
|
import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
|
|
14
14
|
import { TelegramSettingsSection } from "./telegram-settings-section";
|
|
15
|
-
import { AccountsSettingsSection } from "./accounts-settings-section";
|
|
16
15
|
import { ProxySettingsSection } from "./proxy-settings-section";
|
|
17
16
|
import { usePushNotification } from "@/hooks/use-push-notification";
|
|
18
17
|
|
|
@@ -26,12 +25,11 @@ const pushSupported = "PushManager" in window && "serviceWorker" in navigator;
|
|
|
26
25
|
const isIosNonPwa = /iPhone|iPad/.test(navigator.userAgent) &&
|
|
27
26
|
!window.matchMedia("(display-mode: standalone)").matches;
|
|
28
27
|
|
|
29
|
-
type SettingsCategory = "ai" | "notifications" | "
|
|
28
|
+
type SettingsCategory = "ai" | "notifications" | "proxy" | "shortcuts";
|
|
30
29
|
|
|
31
30
|
const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; icon: React.ElementType }[] = [
|
|
32
31
|
{ value: "ai", label: "AI Provider", subtitle: "Model, execution mode, limits", icon: Bot },
|
|
33
32
|
{ value: "notifications", label: "Notifications", subtitle: "Push & Telegram alerts", icon: BellRing },
|
|
34
|
-
{ value: "accounts", label: "Accounts", subtitle: "Claude accounts & rotation", icon: Users },
|
|
35
33
|
{ value: "proxy", label: "API Proxy", subtitle: "Expose accounts as Anthropic API", icon: Globe },
|
|
36
34
|
{ value: "shortcuts", label: "Keyboard Shortcuts", subtitle: "Customize key bindings", icon: Keyboard },
|
|
37
35
|
];
|
|
@@ -84,7 +82,6 @@ export function SettingsTab() {
|
|
|
84
82
|
<div className="p-3">
|
|
85
83
|
{activeCategory === "ai" && <AISettingsSection compact />}
|
|
86
84
|
{activeCategory === "notifications" && <NotificationsContent isSubscribed={isSubscribed} loading={loading} permission={permission} pushError={pushError} subscribe={subscribe} unsubscribe={unsubscribe} />}
|
|
87
|
-
{activeCategory === "accounts" && <AccountsSettingsSection />}
|
|
88
85
|
{activeCategory === "proxy" && <ProxySettingsSection />}
|
|
89
86
|
{activeCategory === "shortcuts" && <KeyboardShortcutsSection />}
|
|
90
87
|
</div>
|