@hienlh/ppm 0.8.54 → 0.8.55
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 +19 -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-BUOCxR2G.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-os78eUN8.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-DTwe0h8F.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-CSyOOmS2.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-CwYW3F4P.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-yMR7OUDx.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-B-BLLKiZ.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-DQWY7QvX.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-Ctv7NTI_.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-Daap0c_B.js +1 -0
- package/dist/web/assets/sqlite-viewer-DtNk76CE.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-JEpjt3RD.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 +56 -20
- 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;
|
|
@@ -129,13 +129,12 @@ function formatLastUpdated(ts: number | null | undefined): string | null {
|
|
|
129
129
|
return `${days}d ago`;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
function AccountUsageCard({ entry, isActive, accountInfo, onToggle,
|
|
132
|
+
function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, onViewProfile, flash }: {
|
|
133
133
|
entry: AccountUsageEntry;
|
|
134
134
|
isActive: boolean;
|
|
135
135
|
accountInfo?: AccountInfo;
|
|
136
136
|
onToggle?: (id: string, status: string) => void;
|
|
137
|
-
|
|
138
|
-
verifyingId?: string | null;
|
|
137
|
+
onExport?: (id: string) => void;
|
|
139
138
|
onViewProfile?: (profile: OAuthProfileData) => void;
|
|
140
139
|
flash?: boolean;
|
|
141
140
|
}) {
|
|
@@ -144,7 +143,7 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onVerify, ve
|
|
|
144
143
|
const status = accountInfo?.status ?? entry.accountStatus;
|
|
145
144
|
|
|
146
145
|
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"}`}>
|
|
146
|
+
<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
147
|
<div className="flex items-center gap-1.5">
|
|
149
148
|
<span className="text-xs font-medium truncate flex-1 min-w-0">
|
|
150
149
|
{entry.accountLabel ?? entry.accountId.slice(0, 8)}
|
|
@@ -163,14 +162,13 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onVerify, ve
|
|
|
163
162
|
<Eye className="size-3" />
|
|
164
163
|
</button>
|
|
165
164
|
)}
|
|
166
|
-
{
|
|
165
|
+
{onExport && entry.isOAuth && (
|
|
167
166
|
<button
|
|
168
|
-
className="p-1 rounded cursor-pointer text-text-subtle hover:text-
|
|
169
|
-
onClick={() =>
|
|
170
|
-
|
|
171
|
-
title="Verify token"
|
|
167
|
+
className="p-1 rounded cursor-pointer text-text-subtle hover:text-blue-500 hover:bg-surface-elevated transition-colors"
|
|
168
|
+
onClick={() => onExport(entry.accountId)}
|
|
169
|
+
title="Export this account"
|
|
172
170
|
>
|
|
173
|
-
|
|
171
|
+
<Download className="size-3" />
|
|
174
172
|
</button>
|
|
175
173
|
)}
|
|
176
174
|
{onToggle && (
|
|
@@ -211,10 +209,26 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
211
209
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
212
210
|
const [refreshing, setRefreshing] = useState(false);
|
|
213
211
|
const [flashIds, setFlashIds] = useState<Set<string>>(new Set());
|
|
214
|
-
const [verifyingId, setVerifyingId] = useState<string | null>(null);
|
|
215
212
|
const [profileView, setProfileView] = useState<OAuthProfileData | null>(null);
|
|
213
|
+
const [showAddDialog, setShowAddDialog] = useState(false);
|
|
214
|
+
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
215
|
+
const [showImportDialog, setShowImportDialog] = useState(false);
|
|
216
|
+
const [exportPreselect, setExportPreselect] = useState<string | null>(null);
|
|
217
|
+
const [message, setMessage] = useState<string | null>(null);
|
|
218
|
+
const msgTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
216
219
|
const prevUsagesRef = useRef<AccountUsageEntry[]>([]);
|
|
217
220
|
|
|
221
|
+
function showMessage(msg: string) {
|
|
222
|
+
if (msgTimer.current) clearTimeout(msgTimer.current);
|
|
223
|
+
setMessage(msg);
|
|
224
|
+
msgTimer.current = setTimeout(() => setMessage(null), 4000);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function handleSuccess(msg?: string) {
|
|
228
|
+
loadAll();
|
|
229
|
+
if (msg) showMessage(msg);
|
|
230
|
+
}
|
|
231
|
+
|
|
218
232
|
async function loadAll() {
|
|
219
233
|
const isRefresh = allUsages.length > 0;
|
|
220
234
|
if (isRefresh) setRefreshing(true); else setInitialLoading(true);
|
|
@@ -277,10 +291,9 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
277
291
|
onReload?.();
|
|
278
292
|
}
|
|
279
293
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
setVerifyingId(null);
|
|
294
|
+
function openExportAll() {
|
|
295
|
+
setExportPreselect(null);
|
|
296
|
+
setShowExportDialog(true);
|
|
284
297
|
}
|
|
285
298
|
|
|
286
299
|
return (
|
|
@@ -312,8 +325,14 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
312
325
|
</div>
|
|
313
326
|
</div>
|
|
314
327
|
|
|
328
|
+
{message && (
|
|
329
|
+
<div className="text-[11px] p-1.5 rounded bg-green-500/10 text-green-600 text-center animate-in fade-in duration-200">
|
|
330
|
+
{message}
|
|
331
|
+
</div>
|
|
332
|
+
)}
|
|
333
|
+
|
|
315
334
|
{(hasMultipleAccounts || initialLoading) ? (
|
|
316
|
-
<div className="
|
|
335
|
+
<div className="flex gap-1.5 overflow-x-auto pb-1 -mx-3 px-3 snap-x snap-mandatory scrollbar-thin">
|
|
317
336
|
{initialLoading ? (
|
|
318
337
|
<p className="text-[10px] text-text-subtle">Loading...</p>
|
|
319
338
|
) : (
|
|
@@ -324,8 +343,7 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
324
343
|
isActive={entry.accountId === (activeAccountId ?? usage.activeAccountId)}
|
|
325
344
|
accountInfo={accountMap.get(entry.accountId)}
|
|
326
345
|
onToggle={handleToggle}
|
|
327
|
-
|
|
328
|
-
verifyingId={verifyingId}
|
|
346
|
+
onExport={(id) => { setExportPreselect(id); setShowExportDialog(true); }}
|
|
329
347
|
onViewProfile={setProfileView}
|
|
330
348
|
flash={flashIds.has(entry.accountId)}
|
|
331
349
|
/>
|
|
@@ -387,6 +405,24 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
387
405
|
</div>
|
|
388
406
|
</div>
|
|
389
407
|
)}
|
|
408
|
+
|
|
409
|
+
{/* Action buttons */}
|
|
410
|
+
<div className="border-t border-border pt-2 flex gap-1.5">
|
|
411
|
+
<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">
|
|
412
|
+
<Plus className="size-3" /> Add
|
|
413
|
+
</button>
|
|
414
|
+
<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">
|
|
415
|
+
<Download className="size-3" /> Export
|
|
416
|
+
</button>
|
|
417
|
+
<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">
|
|
418
|
+
<Upload className="size-3" /> Import
|
|
419
|
+
</button>
|
|
420
|
+
</div>
|
|
421
|
+
|
|
422
|
+
{/* Account dialogs */}
|
|
423
|
+
<AddAccountDialog open={showAddDialog} onOpenChange={setShowAddDialog} onSuccess={handleSuccess} />
|
|
424
|
+
<ExportAccountsDialog open={showExportDialog} onOpenChange={(v) => { setShowExportDialog(v); if (!v) setExportPreselect(null); }} accounts={accounts} preselectId={exportPreselect} onMessage={showMessage} />
|
|
425
|
+
<ImportAccountsDialog open={showImportDialog} onOpenChange={setShowImportDialog} onSuccess={handleSuccess} />
|
|
390
426
|
</div>
|
|
391
427
|
);
|
|
392
428
|
}
|
|
@@ -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>
|