@hienlh/ppm 0.17.33 → 0.17.34
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 +5 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-resource-editor-CZc98en9.js → ai-resource-editor-QNfSU0Fi.js} +1 -1
- package/dist/web/assets/{audio-preview-XkBaDV-d.js → audio-preview-IokB77Cj.js} +1 -1
- package/dist/web/assets/{chat-tab-Cpxrbm1O.js → chat-tab-BZNEvS88.js} +3 -3
- package/dist/web/assets/{code-editor-DeqsPj8M.js → code-editor-B8YiBPOX.js} +2 -2
- package/dist/web/assets/{conflict-editor-CqORnyRu.js → conflict-editor-CY1o5kBO.js} +1 -1
- package/dist/web/assets/{csv-preview-i79R-2TA.js → csv-preview-AEg0GB-V.js} +1 -1
- package/dist/web/assets/{database-viewer-ClrtBc_A.js → database-viewer-C5Meg0s_.js} +1 -1
- package/dist/web/assets/{diff-viewer-CWUjtM3T.js → diff-viewer-BRRGvZnp.js} +1 -1
- package/dist/web/assets/{docx-preview-CFjtBdLf.js → docx-preview-DqTX7NDp.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-BSjgyGAL.js → edit-diff-preview-_CXUdwsF.js} +1 -1
- package/dist/web/assets/{extension-webview-DwRdXtK4.js → extension-webview-CQceYb7O.js} +1 -1
- package/dist/web/assets/{git-log-panel-DI_uaWVT.js → git-log-panel-D7QlAJqu.js} +1 -1
- package/dist/web/assets/{glide-data-grid-Q2kMqNeo.js → glide-data-grid-cGxkgluR.js} +1 -1
- package/dist/web/assets/group-chat-tab-B-eA5ytm.js +1 -0
- package/dist/web/assets/{image-preview-QyeYNiJt.js → image-preview-DRUzYptI.js} +1 -1
- package/dist/web/assets/{index-BI35sAVB.js → index-BuX7JkCt.js} +4 -4
- package/dist/web/assets/index-PX2pZ3bH.css +2 -0
- package/dist/web/assets/{keybindings-store-55cr_nFT.js → keybindings-store-B3plqyxF.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DoofKFOC.js → markdown-renderer-BKDbjc3g.js} +1 -1
- package/dist/web/assets/{markdown-renderer-ClDzjZ5T.js → markdown-renderer-C9Gy8tM2.js} +1 -1
- package/dist/web/assets/{notification-store-Bwzk7bz9.js → notification-store-jFDauzse.js} +1 -1
- package/dist/web/assets/{pdf-preview-Dd65pYcK.js → pdf-preview-CzHUqXJB.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BAeRjowr.js → postgres-viewer-BeantJZe.js} +1 -1
- package/dist/web/assets/{settings-tab-D_Vz5vjU.js → settings-tab-CqQHhqV1.js} +1 -1
- package/dist/web/assets/{sql-query-editor-RrxjEhFI.js → sql-query-editor-DqU9zfVy.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-DULpl_6h.js → sqlite-viewer-BH891DC0.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-DHOIu_5Y.js → system-monitor-tab-Bue4p4Vb.js} +1 -1
- package/dist/web/assets/{terminal-tab-B64jdELv.js → terminal-tab-BfNnKU71.js} +1 -1
- package/dist/web/assets/tool-cards-CSOwe7_i.js +7 -0
- package/dist/web/assets/use-blob-url-jSRmyXVg.js +1 -0
- package/dist/web/assets/{use-monaco-theme-Bf_8qog4.js → use-monaco-theme-5XjAzOzE.js} +1 -1
- package/dist/web/assets/{video-preview-C-ZB6w-Z.js → video-preview-DMqLOqmc.js} +1 -1
- package/dist/web/index.html +2 -2
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +4 -3
- package/src/server/routes/fs-browse.ts +13 -2
- package/src/services/jsonl-transcript-parser.ts +2 -1
- package/src/shared/image-extensions.ts +19 -0
- package/src/shared/tool-result-content.ts +70 -0
- package/src/web/components/chat/image-preview-geometry.ts +83 -0
- package/src/web/components/chat/image-preview-states.tsx +51 -0
- package/src/web/components/chat/tool-cards.tsx +27 -2
- package/src/web/components/chat/tool-image-preview.tsx +225 -0
- package/src/web/components/editor/audio-preview.tsx +1 -1
- package/src/web/components/editor/image-preview.tsx +1 -1
- package/src/web/components/editor/video-preview.tsx +1 -1
- package/src/web/{components/editor → hooks}/use-blob-url.ts +14 -3
- package/src/web/styles/globals.css +72 -0
- package/dist/web/assets/group-chat-tab-2NJcUl_4.js +0 -1
- package/dist/web/assets/index-BW2cOdzp.css +0 -2
- package/dist/web/assets/tool-cards-DT3R2C18.js +0 -7
- package/dist/web/assets/use-blob-url-kvtWmJtt.js +0 -1
- /package/dist/web/assets/{csv-parser-BSnYgPcx.js → csv-parser-BPS94T2h.js} +0 -0
- /package/dist/web/assets/{libesm-Dd8zbj1v.js → libesm-BS7D9de3.js} +0 -0
|
@@ -67,6 +67,9 @@ import type { ChatEvent } from "../../../types/chat";
|
|
|
67
67
|
import { useShallow } from "zustand/react/shallow";
|
|
68
68
|
import { useTabStore } from "@/stores/tab-store";
|
|
69
69
|
import { basename } from "@/lib/utils";
|
|
70
|
+
import { isImageExtension } from "../../../shared/image-extensions";
|
|
71
|
+
import { resultHasImagePlaceholder } from "../../../shared/tool-result-content";
|
|
72
|
+
import { ToolImagePreview } from "./tool-image-preview";
|
|
70
73
|
|
|
71
74
|
/** Extract tool name and input from a ChatEvent */
|
|
72
75
|
function extractToolInfo(tool: ChatEvent): { toolName: string; input: Record<string, unknown> } {
|
|
@@ -84,6 +87,18 @@ function extractToolInfo(tool: ChatEvent): { toolName: string; input: Record<str
|
|
|
84
87
|
return { toolName, input };
|
|
85
88
|
}
|
|
86
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Path of the image a Read call targeted, or null when it is not a displayable image read.
|
|
92
|
+
* Only absolute paths qualify, matching what useBlobUrl can resolve to the external
|
|
93
|
+
* raw-file endpoint — and what the Read tool always supplies.
|
|
94
|
+
*/
|
|
95
|
+
function imageReadPath(tool: ChatEvent): string | null {
|
|
96
|
+
if (tool.type !== "tool_use" || tool.tool !== "Read") return null;
|
|
97
|
+
const path = (tool.input as Record<string, unknown> | undefined)?.file_path;
|
|
98
|
+
if (typeof path !== "string" || !/^(\/|[A-Za-z]:[/\\])/.test(path)) return null;
|
|
99
|
+
return isImageExtension(path) ? path : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
87
102
|
/** Unified tool card: shows tool-specific summary + expandable details */
|
|
88
103
|
export function ToolCard({
|
|
89
104
|
tool,
|
|
@@ -103,7 +118,10 @@ export function ToolCard({
|
|
|
103
118
|
// a click — unless the edit failed (diff was never applied, so it's noise).
|
|
104
119
|
const t = tool.type === "tool_use" ? tool.tool : (tool as any).tool;
|
|
105
120
|
const failed = result?.type === "tool_result" && !!(result as any).isError;
|
|
106
|
-
|
|
121
|
+
if ((t === "Edit" || t === "MultiEdit") && !failed) return true;
|
|
122
|
+
// Same reasoning for reading an image: the thumbnail is the point of the card,
|
|
123
|
+
// so show it without requiring a click.
|
|
124
|
+
return !!imageReadPath(tool);
|
|
107
125
|
});
|
|
108
126
|
|
|
109
127
|
if (tool.type === "error") {
|
|
@@ -127,6 +145,10 @@ export function ToolCard({
|
|
|
127
145
|
// File-mutation tools show their change via the inline diff/content preview — the SDK's
|
|
128
146
|
// "file updated successfully" boilerplate is noise, so suppress it (but keep error output).
|
|
129
147
|
const isFileMutation = ["Edit", "MultiEdit", "Write", "NotebookEdit"].includes(toolName);
|
|
148
|
+
const imagePath = imageReadPath(tool);
|
|
149
|
+
// Hide the text output only when the result really carried an image. Extension is not
|
|
150
|
+
// enough: an SVG or an undecodable format comes back as text that must stay visible.
|
|
151
|
+
const resultIsImage = hasResult && resultHasImagePlaceholder(String((result as any).output ?? ""));
|
|
130
152
|
|
|
131
153
|
// Read partial output for streaming Bash/PowerShell tools
|
|
132
154
|
const toolUseId = tool.type === "tool_use" ? (tool as any).toolUseId as string | undefined : undefined;
|
|
@@ -194,7 +216,10 @@ export function ToolCard({
|
|
|
194
216
|
{hasChildren && (
|
|
195
217
|
<SubagentChildren events={children!} projectName={projectName} />
|
|
196
218
|
)}
|
|
197
|
-
{
|
|
219
|
+
{imagePath && (
|
|
220
|
+
<ToolImagePreview filePath={imagePath} projectName={projectName ?? ""} />
|
|
221
|
+
)}
|
|
222
|
+
{hasResult && !(isFileMutation && !isError) && !(resultIsImage && !isError) && (
|
|
198
223
|
<ToolResultView toolName={toolName} output={(result as any).output} />
|
|
199
224
|
)}
|
|
200
225
|
</div>
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import { useShallow } from "zustand/react/shallow";
|
|
3
|
+
import { useBlobUrl } from "@/hooks/use-blob-url";
|
|
4
|
+
import { useImageOverlay } from "@/stores/image-overlay-store";
|
|
5
|
+
import { useTabStore } from "@/stores/tab-store";
|
|
6
|
+
import { copyToClipboard } from "@/lib/clipboard";
|
|
7
|
+
import { basename } from "@/lib/utils";
|
|
8
|
+
import { ImagePreviewFailure, ImagePreviewSkeleton } from "./image-preview-states";
|
|
9
|
+
import {
|
|
10
|
+
cacheNatural,
|
|
11
|
+
dimensionsLabel,
|
|
12
|
+
formatBytes,
|
|
13
|
+
getCachedNatural,
|
|
14
|
+
previewLayout,
|
|
15
|
+
TILE_WIDTH,
|
|
16
|
+
type NaturalSize,
|
|
17
|
+
} from "./image-preview-geometry";
|
|
18
|
+
|
|
19
|
+
/** Ratio the skeleton assumes before the image has ever been measured. */
|
|
20
|
+
const FALLBACK_ASPECT = 4 / 5;
|
|
21
|
+
|
|
22
|
+
const LABEL = "text-[var(--img-meta-label)] @max-[470px]:hidden";
|
|
23
|
+
const VALUE = "truncate text-text @max-[470px]:text-text-2";
|
|
24
|
+
const GHOST =
|
|
25
|
+
"h-[30px] @max-[470px]:h-[36px] shrink-0 rounded-[7px] px-[9px] font-mono text-[11.5px] " +
|
|
26
|
+
"text-text-2 bg-[var(--img-ghost)] border border-[var(--img-ghost-border)] backdrop-blur-[6px] " +
|
|
27
|
+
"transition-colors can-hover:hover:bg-[var(--img-ghost-hover)] " +
|
|
28
|
+
"can-hover:hover:border-[var(--img-ghost-hover-border)] can-hover:hover:text-text";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The image a Read call opened, shown on a stage lit by a blurred copy of itself, beside
|
|
32
|
+
* the file facts needed to decide whether to open it. Fetched as a blob with an
|
|
33
|
+
* Authorization header, so no auth token ends up in an image URL.
|
|
34
|
+
*/
|
|
35
|
+
export function ToolImagePreview({
|
|
36
|
+
filePath,
|
|
37
|
+
projectName,
|
|
38
|
+
}: {
|
|
39
|
+
filePath: string;
|
|
40
|
+
projectName: string;
|
|
41
|
+
}) {
|
|
42
|
+
const [attempt, setAttempt] = useState(0);
|
|
43
|
+
const { blobUrl, blob, error, errorCode } = useBlobUrl(filePath, projectName, undefined, attempt);
|
|
44
|
+
const [natural, setNatural] = useState<NaturalSize | undefined>(() => getCachedNatural(filePath));
|
|
45
|
+
const [decodeFailed, setDecodeFailed] = useState(false);
|
|
46
|
+
const [copied, setCopied] = useState(false);
|
|
47
|
+
const openOverlay = useImageOverlay((s) => s.open);
|
|
48
|
+
const { openTab } = useTabStore(useShallow((s) => ({ openTab: s.openTab })));
|
|
49
|
+
const name = basename(filePath);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!copied) return;
|
|
53
|
+
const timer = setTimeout(() => setCopied(false), 1200);
|
|
54
|
+
return () => clearTimeout(timer);
|
|
55
|
+
}, [copied]);
|
|
56
|
+
|
|
57
|
+
const measure = (img: HTMLImageElement) => {
|
|
58
|
+
const size = { w: img.naturalWidth, h: img.naturalHeight };
|
|
59
|
+
if (!size.w || !size.h) return setDecodeFailed(true);
|
|
60
|
+
cacheNatural(filePath, size);
|
|
61
|
+
setNatural(size);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const retry = () => {
|
|
65
|
+
setDecodeFailed(false);
|
|
66
|
+
setAttempt((n) => n + 1);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (error || decodeFailed) {
|
|
70
|
+
return (
|
|
71
|
+
<div className="border-t border-border pt-1.5">
|
|
72
|
+
<ImagePreviewFailure code={decodeFailed ? undefined : errorCode ?? undefined} onRetry={retry} />
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const layout = natural ? previewLayout(natural, TILE_WIDTH) : null;
|
|
78
|
+
const skeletonAspect = layout?.aspect ?? FALLBACK_ASPECT;
|
|
79
|
+
|
|
80
|
+
const actions = (
|
|
81
|
+
<div className="flex flex-wrap gap-[6px]">
|
|
82
|
+
<button type="button" onClick={openInEditor} className={GHOST}>
|
|
83
|
+
<span className="@max-[470px]:hidden">open in editor</span>
|
|
84
|
+
<span className="hidden @max-[470px]:inline">open</span>
|
|
85
|
+
</button>
|
|
86
|
+
<button type="button" onClick={copyPath} className={GHOST}>
|
|
87
|
+
{copied ? "copied" : "copy path"}
|
|
88
|
+
</button>
|
|
89
|
+
<button type="button" onClick={openFull} className={GHOST}>
|
|
90
|
+
full size
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
function openInEditor() {
|
|
96
|
+
if (!projectName) return;
|
|
97
|
+
openTab({
|
|
98
|
+
type: "editor",
|
|
99
|
+
title: name,
|
|
100
|
+
metadata: { filePath, projectName },
|
|
101
|
+
projectId: projectName,
|
|
102
|
+
closable: true,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function copyPath() {
|
|
107
|
+
void copyToClipboard(filePath).then(() => setCopied(true));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function openFull() {
|
|
111
|
+
if (blobUrl) openOverlay(blobUrl, name);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Loading: the same box as the loaded state, so the transcript never shifts under the
|
|
115
|
+
// reader once the image resolves.
|
|
116
|
+
if (!blobUrl || !layout || !natural) {
|
|
117
|
+
return (
|
|
118
|
+
<div className="@container border-t border-border pt-1.5">
|
|
119
|
+
{blobUrl && (
|
|
120
|
+
<img
|
|
121
|
+
src={blobUrl}
|
|
122
|
+
alt=""
|
|
123
|
+
className="hidden"
|
|
124
|
+
onLoad={(e) => measure(e.currentTarget)}
|
|
125
|
+
onError={() => setDecodeFailed(true)}
|
|
126
|
+
/>
|
|
127
|
+
)}
|
|
128
|
+
<ImagePreviewSkeleton aspect={skeletonAspect} actions={actions} />
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const metadata = (
|
|
134
|
+
<>
|
|
135
|
+
<dt className={LABEL}>dimensions</dt>
|
|
136
|
+
<dd className={VALUE}>{dimensionsLabel(natural, layout.fit)}</dd>
|
|
137
|
+
{blob && (
|
|
138
|
+
<>
|
|
139
|
+
<dt className={LABEL}>size</dt>
|
|
140
|
+
<dd className={VALUE}>
|
|
141
|
+
{formatBytes(blob.size)}
|
|
142
|
+
{blob.type ? ` · ${blob.type}` : ""}
|
|
143
|
+
</dd>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
<dt className={LABEL}>path</dt>
|
|
147
|
+
{/* rtl puts the ellipsis at the head of the path, keeping the filename readable */}
|
|
148
|
+
<dd dir="rtl" className={`${VALUE} text-left`}>
|
|
149
|
+
{filePath}
|
|
150
|
+
</dd>
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const picture = (
|
|
155
|
+
<img
|
|
156
|
+
src={blobUrl}
|
|
157
|
+
alt={name}
|
|
158
|
+
onLoad={(e) => measure(e.currentTarget)}
|
|
159
|
+
onError={() => setDecodeFailed(true)}
|
|
160
|
+
className={
|
|
161
|
+
layout.fit === "cover"
|
|
162
|
+
? "size-full object-cover"
|
|
163
|
+
: layout.fit === "contain"
|
|
164
|
+
? "size-full object-contain"
|
|
165
|
+
: "max-h-full max-w-full"
|
|
166
|
+
}
|
|
167
|
+
/>
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<div className="@container border-t border-border pt-1.5">
|
|
172
|
+
<div className="relative overflow-hidden rounded-lg border border-border p-[14px] @max-[470px]:p-[11px]">
|
|
173
|
+
<img
|
|
174
|
+
aria-hidden="true"
|
|
175
|
+
alt=""
|
|
176
|
+
src={blobUrl}
|
|
177
|
+
className="absolute inset-0 size-full object-cover"
|
|
178
|
+
style={{ filter: "var(--img-backdrop)", transform: "scale(1.3)" }}
|
|
179
|
+
/>
|
|
180
|
+
<div className="absolute inset-0" style={{ background: "var(--img-veil)" }} />
|
|
181
|
+
|
|
182
|
+
{layout.variant === "band" ? (
|
|
183
|
+
<div className="relative grid gap-[11px]">
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={openFull}
|
|
187
|
+
title={`Open ${name}`}
|
|
188
|
+
className="img-checkerboard w-full cursor-zoom-in overflow-hidden rounded-lg border border-[var(--img-plate-border)]"
|
|
189
|
+
style={{ aspectRatio: layout.aspect }}
|
|
190
|
+
>
|
|
191
|
+
{picture}
|
|
192
|
+
</button>
|
|
193
|
+
<dl className="grid grid-cols-[auto_auto_1fr] gap-x-[18px] font-mono text-[11.5px] [&_dt]:hidden">
|
|
194
|
+
{metadata}
|
|
195
|
+
</dl>
|
|
196
|
+
{actions}
|
|
197
|
+
</div>
|
|
198
|
+
) : (
|
|
199
|
+
<div className="relative grid grid-cols-[132px_1fr] items-start gap-[14px] @max-[470px]:grid-cols-[64px_1fr] @max-[470px]:gap-[11px]">
|
|
200
|
+
<button
|
|
201
|
+
type="button"
|
|
202
|
+
onClick={openFull}
|
|
203
|
+
title={`Open ${name}`}
|
|
204
|
+
className={`cursor-zoom-in overflow-hidden rounded-lg border border-[var(--img-plate-border)] bg-[var(--img-plate)] shadow-[var(--img-plate-shadow)] ${
|
|
205
|
+
layout.fit === "cover" ? "" : "img-checkerboard flex items-center justify-center"
|
|
206
|
+
}`}
|
|
207
|
+
style={{ aspectRatio: layout.aspect }}
|
|
208
|
+
>
|
|
209
|
+
{picture}
|
|
210
|
+
</button>
|
|
211
|
+
<div className="flex min-w-0 flex-col gap-[9px]">
|
|
212
|
+
<p className="truncate text-[13px] font-semibold text-text @max-[470px]:hidden">
|
|
213
|
+
{name}
|
|
214
|
+
</p>
|
|
215
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-[14px] gap-y-[4px] font-mono text-[11.5px] @max-[470px]:grid-cols-[1fr]">
|
|
216
|
+
{metadata}
|
|
217
|
+
</dl>
|
|
218
|
+
{actions}
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
)}
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Loader2, FileWarning, Music } from "lucide-react";
|
|
2
|
-
import { useBlobUrl } from "
|
|
2
|
+
import { useBlobUrl } from "@/hooks/use-blob-url";
|
|
3
3
|
import { basename } from "@/lib/utils";
|
|
4
4
|
|
|
5
5
|
export function AudioPreview({ filePath, projectName }: { filePath: string; projectName: string }) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Loader2, FileWarning } from "lucide-react";
|
|
2
|
-
import { useBlobUrl } from "
|
|
2
|
+
import { useBlobUrl } from "@/hooks/use-blob-url";
|
|
3
3
|
|
|
4
4
|
export function ImagePreview({ filePath, projectName }: { filePath: string; projectName: string }) {
|
|
5
5
|
const { blobUrl, error } = useBlobUrl(filePath, projectName);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Loader2, FileWarning } from "lucide-react";
|
|
2
|
-
import { useBlobUrl } from "
|
|
2
|
+
import { useBlobUrl } from "@/hooks/use-blob-url";
|
|
3
3
|
|
|
4
4
|
export function VideoPreview({ filePath, projectName }: { filePath: string; projectName: string }) {
|
|
5
5
|
const { blobUrl, error } = useBlobUrl(filePath, projectName);
|
|
@@ -11,11 +11,16 @@ export function useBlobUrl(
|
|
|
11
11
|
refreshKey = 0,
|
|
12
12
|
) {
|
|
13
13
|
const [blobUrl, setBlobUrl] = useState<string | null>(null);
|
|
14
|
+
const [blob, setBlob] = useState<Blob | null>(null);
|
|
14
15
|
const [error, setError] = useState(false);
|
|
16
|
+
const [errorCode, setErrorCode] = useState<string | null>(null);
|
|
15
17
|
const urlRef = useRef<string | null>(null);
|
|
16
18
|
|
|
17
19
|
useEffect(() => {
|
|
18
20
|
let cancelled = false;
|
|
21
|
+
// Reset so a retry (bumped refreshKey) leaves the error state.
|
|
22
|
+
setError(false);
|
|
23
|
+
setErrorCode(null);
|
|
19
24
|
const isExternal = /^(\/|[A-Za-z]:[/\\])/.test(filePath);
|
|
20
25
|
const url = isExternal
|
|
21
26
|
? `/api/fs/raw?path=${encodeURIComponent(filePath)}`
|
|
@@ -23,7 +28,7 @@ export function useBlobUrl(
|
|
|
23
28
|
const token = getAuthToken();
|
|
24
29
|
fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {} })
|
|
25
30
|
.then((r) => {
|
|
26
|
-
if (!r.ok) throw new Error(
|
|
31
|
+
if (!r.ok) throw new Error(String(r.status));
|
|
27
32
|
return r.blob();
|
|
28
33
|
})
|
|
29
34
|
.then((blob) => {
|
|
@@ -34,8 +39,13 @@ export function useBlobUrl(
|
|
|
34
39
|
if (urlRef.current) URL.revokeObjectURL(urlRef.current);
|
|
35
40
|
urlRef.current = u;
|
|
36
41
|
setBlobUrl(u);
|
|
42
|
+
setBlob(final);
|
|
37
43
|
})
|
|
38
|
-
.catch(() => {
|
|
44
|
+
.catch((e: Error) => {
|
|
45
|
+
if (cancelled) return;
|
|
46
|
+
setError(true);
|
|
47
|
+
setErrorCode(/^\d+$/.test(e.message) ? e.message : "network");
|
|
48
|
+
});
|
|
39
49
|
return () => { cancelled = true; };
|
|
40
50
|
}, [filePath, projectName, mimeOverride, refreshKey]);
|
|
41
51
|
|
|
@@ -44,5 +54,6 @@ export function useBlobUrl(
|
|
|
44
54
|
if (urlRef.current) URL.revokeObjectURL(urlRef.current);
|
|
45
55
|
}, []);
|
|
46
56
|
|
|
47
|
-
|
|
57
|
+
// `blob` is exposed so callers can read size and MIME type without a second request.
|
|
58
|
+
return { blobUrl, blob, error, errorCode };
|
|
48
59
|
}
|
|
@@ -101,6 +101,78 @@
|
|
|
101
101
|
--blur: 16px;
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
+
/*
|
|
105
|
+
* Ambient image stage (chat Read-tool image results). The panel floats a tile on a
|
|
106
|
+
* blurred copy of the image itself, so its surfaces are translucent veils rather than
|
|
107
|
+
* theme tokens. Declared per mode because the veil must stay contrast-safe over both a
|
|
108
|
+
* bright poster and a dark screenshot. Dark lives in :root, matching the default above.
|
|
109
|
+
*/
|
|
110
|
+
:root {
|
|
111
|
+
--img-veil: linear-gradient(100deg, rgba(10, 16, 32, 0.6), rgba(10, 16, 32, 0.84));
|
|
112
|
+
--img-backdrop: blur(40px) saturate(1.6);
|
|
113
|
+
--img-plate: rgba(255, 255, 255, 0.06);
|
|
114
|
+
--img-plate-border: rgba(148, 178, 255, 0.22);
|
|
115
|
+
--img-plate-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), 0 16px 34px -20px rgba(0, 0, 0, 0.7);
|
|
116
|
+
--img-ghost: rgba(255, 255, 255, 0.08);
|
|
117
|
+
--img-ghost-border: rgba(255, 255, 255, 0.14);
|
|
118
|
+
--img-ghost-hover: rgba(255, 255, 255, 0.14);
|
|
119
|
+
--img-ghost-hover-border: rgba(148, 178, 255, 0.35);
|
|
120
|
+
--img-meta-label: #8b97b4;
|
|
121
|
+
--img-checker: rgba(255, 255, 255, 0.04);
|
|
122
|
+
/* Own values rather than a surface token: on flat light themes panel and panel-2 are
|
|
123
|
+
both #ffffff, which would make the skeleton invisible on the card. */
|
|
124
|
+
--img-skeleton: rgba(255, 255, 255, 0.09);
|
|
125
|
+
--img-skeleton-sweep: rgba(255, 255, 255, 0.14);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
html.light {
|
|
129
|
+
--img-veil: linear-gradient(100deg, rgba(255, 255, 255, 0.62), rgba(255, 255, 255, 0.84));
|
|
130
|
+
--img-backdrop: blur(40px) saturate(1.6) brightness(0.95);
|
|
131
|
+
--img-plate: rgba(255, 255, 255, 0.5);
|
|
132
|
+
--img-plate-border: rgba(255, 255, 255, 0.7);
|
|
133
|
+
--img-plate-shadow: 0 2px 4px rgba(15, 23, 42, 0.14), 0 16px 34px -20px rgba(15, 23, 42, 0.55);
|
|
134
|
+
--img-ghost: rgba(255, 255, 255, 0.7);
|
|
135
|
+
--img-ghost-border: rgba(255, 255, 255, 0.85);
|
|
136
|
+
--img-ghost-hover: rgba(255, 255, 255, 0.95);
|
|
137
|
+
--img-ghost-hover-border: #cbd5e1;
|
|
138
|
+
--img-meta-label: #7c8899;
|
|
139
|
+
--img-checker: rgba(15, 23, 42, 0.03);
|
|
140
|
+
--img-skeleton: rgba(15, 23, 42, 0.07);
|
|
141
|
+
--img-skeleton-sweep: rgba(255, 255, 255, 0.75);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* Backing for image tiles that letterbox: shows the transparent area without reading
|
|
145
|
+
as an empty box. Padding keeps the artwork off the tile edge. */
|
|
146
|
+
.img-checkerboard {
|
|
147
|
+
background-image:
|
|
148
|
+
linear-gradient(45deg, var(--img-checker) 25%, transparent 25%, transparent 75%, var(--img-checker) 75%),
|
|
149
|
+
linear-gradient(45deg, var(--img-checker) 25%, transparent 25%, transparent 75%, var(--img-checker) 75%);
|
|
150
|
+
background-size: 14px 14px;
|
|
151
|
+
background-position: 0 0, 7px 7px;
|
|
152
|
+
padding: 6px;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* Skeleton that occupies the loaded box exactly, so the row never grows after paint. */
|
|
156
|
+
.img-skeleton {
|
|
157
|
+
position: relative;
|
|
158
|
+
overflow: hidden;
|
|
159
|
+
background: var(--img-skeleton);
|
|
160
|
+
}
|
|
161
|
+
.img-skeleton::after {
|
|
162
|
+
content: "";
|
|
163
|
+
position: absolute;
|
|
164
|
+
inset: 0;
|
|
165
|
+
background: linear-gradient(100deg, transparent 20%, var(--img-skeleton-sweep) 50%, transparent 80%);
|
|
166
|
+
animation: img-skeleton-sweep 1.25s linear infinite;
|
|
167
|
+
}
|
|
168
|
+
@keyframes img-skeleton-sweep {
|
|
169
|
+
from { transform: translateX(-100%); }
|
|
170
|
+
to { transform: translateX(100%); }
|
|
171
|
+
}
|
|
172
|
+
@media (prefers-reduced-motion: reduce) {
|
|
173
|
+
.img-skeleton::after { animation: none; }
|
|
174
|
+
}
|
|
175
|
+
|
|
104
176
|
/* Smooth theme transitions (~250ms) */
|
|
105
177
|
html {
|
|
106
178
|
transition: background-color 0.25s ease-out, color 0.25s ease-out;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{b as t,y as n}from"./vendor-markdown-CKTlmXbz.js";import"./vendor-ui-DMRGWRuc.js";import{t as r}from"./createLucideIcon-ConhwJnb.js";import{g as i,p as a,t as o}from"./mobile-bottom-sheet-BwPBKZON.js";import{n as s}from"./use-is-mobile-BcuCjH19.js";import{t as c}from"./chevron-right-DeUue2aK.js";import{n as l,t as u}from"./tool-cards-DT3R2C18.js";import{a as d,c as f,n as p,o as m,t as h}from"./dialog-HHdNInEH.js";import"./api-client-DhXpjLdi.js";import"./vendor-mermaid-CYOBtZtD.js";import"./settings-store-DFMz_Oer.js";import{n as g}from"./utils-E0yyGxXt.js";import"./panel-store-D-RxcUgH.js";import{i as _}from"./project-store-C1jInVih.js";import"./tab-store-CNxCwdXw.js";import{Bt as v,C as y,Dt as b,G as x,It as S,J as C,K as w,Qt as T,S as E,Ut as ee,W as D,_ as O,b as k,g as te,h as A,jt as j,m as ne,p as M,q as N,v as re,x as P,y as F,zt as I}from"./index-BI35sAVB.js";import{t as L}from"./markdown-renderer-DoofKFOC.js";var R=r(`scroll-text`,[[`path`,{d:`M15 12h-5`,key:`r7krc0`}],[`path`,{d:`M15 8h-5`,key:`1khuty`}],[`path`,{d:`M19 17V5a2 2 0 0 0-2-2H4`,key:`zz82l3`}],[`path`,{d:`M8 21h12a2 2 0 0 0 2-2v-1a1 1 0 0 0-1-1H11a1 1 0 0 0-1 1v1a2 2 0 1 1-4 0V5a2 2 0 1 0-4 0v2a1 1 0 0 0 1 1h3`,key:`1ph1d7`}]]),z=r(`user-plus`,[[`path`,{d:`M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2`,key:`1yyitq`}],[`circle`,{cx:`9`,cy:`7`,r:`4`,key:`nufk8`}],[`line`,{x1:`19`,x2:`19`,y1:`8`,y2:`14`,key:`1bvyxn`}],[`line`,{x1:`22`,x2:`16`,y1:`11`,y2:`11`,key:`1shjgl`}]]),B=e(t(),1),V=8e3;function ie(e,t){let[n,r]=(0,B.useState)([]),[i,a]=(0,B.useState)([]),[o,s]=(0,B.useState)(`idle`),[c,u]=(0,B.useState)({}),[d,f]=(0,B.useState)(!1),[p,m]=(0,B.useState)(!1),[h,g]=(0,B.useState)(!1),[_,v]=(0,B.useState)(null),y=(0,B.useRef)(new Map),b=(0,B.useCallback)(e=>{u(t=>{if(!t[e])return t;let n={...t};return delete n[e],n})},[]),x=(0,B.useCallback)(e=>{let t;try{t=JSON.parse(e.data)}catch{return}switch(t.type){case`ping`:return;case`group_state`:m(!0),s(t.status),a(t.members),g(t.status===`active`);return;case`group_message`:b(t.message.fromMember),r(e=>e.some(e=>e.id===t.message.id)?e:[...e,t.message]);return;case`member_status`:a(e=>e.map(e=>e.id===t.memberId?{...e,status:t.status}:e));return;case`typing`:{u(e=>({...e,[t.member]:!0}));let e=y.current,n=e.get(t.member);n&&clearTimeout(n),e.set(t.member,setTimeout(()=>b(t.member),V));return}case`turn_done`:return;case`group_done`:g(!1),s(`idle`),u({});return;case`error`:v(t.message),g(!1);return}},[b]),{send:S}=l({url:e&&t?`/ws/project/${encodeURIComponent(t)}/group/${encodeURIComponent(e)}`:``,onMessage:x,autoConnect:!!e&&!!t});return(0,B.useEffect)(()=>{let t=!1;r([]),a([]),u({}),s(`idle`),g(!1),v(null),m(!1);for(let e of y.current.values())clearTimeout(e);if(y.current.clear(),e)return f(!0),A(e,{limit:200}).then(e=>{t||r(t=>{let n=new Set(e.messages.map(e=>e.id)),r=t.filter(e=>!n.has(e.id));return[...e.messages,...r]})}).catch(e=>{t||v(e instanceof Error?e.message:`Failed to load feed`)}).finally(()=>{t||f(!1)}),()=>{t=!0}},[e]),{messages:n,members:i,status:o,typing:c,loading:d,isConnected:p,isRunning:h,error:_,sendMessage:(0,B.useCallback)(t=>{let n=t.trim();!n||!e||(v(null),g(!0),p?S(JSON.stringify({type:`message`,content:n})):k(e,n).catch(e=>{v(e instanceof Error?e.message:`Failed to send message`),g(!1)}))},[e,p,S]),stop:(0,B.useCallback)(()=>{e&&(p?S(JSON.stringify({type:`stop`})):P(e).catch(()=>{}),s(`paused`),g(!1))},[e,p,S]),resume:(0,B.useCallback)(()=>{e&&F(e).then(()=>{s(`active`),g(!0)}).catch(e=>v(e instanceof Error?e.message:`Failed to resume`))},[e])}}var H=n();function U(e){let t=e.trim().split(/\s+/).filter(Boolean);return t.length===0?`?`:t.length===1?t[0].slice(0,2).toUpperCase():(t[0][0]+t[t.length-1][0]).toUpperCase()}function W(e){try{let t=e<0xe8d4a51000?e*1e3:e;return new Date(t).toLocaleTimeString([],{hour:`2-digit`,minute:`2-digit`})}catch{return``}}function G(e,t){return e.split(/(@[\p{L}\p{N}_]+)/gu).map((e,n)=>{if(e.startsWith(`@`)&&e.length>1){let r=t(e.slice(1));return(0,H.jsx)(`span`,{className:g(`font-semibold`,!r&&`text-primary`),style:r?{color:r}:void 0,children:e},n)}return e})}var ae=(0,B.memo)(function({message:e,colorFor:t,isLeader:n,onViewFull:r}){let i=e.fromMember===`user`,a=i?`You`:e.fromMember,o=i?`var(--color-primary)`:t(e.fromMember)??`var(--accent)`,s=!i&&n(e.fromMember),c=e.summary??``,l=e.toMember&&e.toMember!==`all`?e.toMember:null,u=(0,H.jsxs)(`div`,{className:g(`group relative flex gap-2.5 px-3 py-2 transition-colors hover:bg-surface-elevated/50`,i&&`bg-primary/5`),style:{boxShadow:`inset 3px 0 0 0 ${o}`},children:[e.fullSessionRef&&(0,H.jsx)(`button`,{type:`button`,onClick:()=>r(e),title:`View full transcript (AI's raw thinking / debug)`,"aria-label":`View full transcript`,className:`absolute right-1.5 top-1.5 hidden items-center rounded p-1 text-text-subtle/60 hover:bg-surface-elevated hover:text-text-secondary can-hover:group-hover:inline-flex`,children:(0,H.jsx)(R,{className:`size-3.5`})}),(0,H.jsx)(`div`,{className:`mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-full text-[11px] font-semibold text-white ring-2 ring-background`,style:{backgroundColor:o},"aria-hidden":!0,children:U(a)}),(0,H.jsxs)(`div`,{className:`min-w-0 flex-1 can-hover:pr-6`,children:[(0,H.jsxs)(`div`,{className:`flex flex-wrap items-center gap-x-2 gap-y-0.5`,children:[(0,H.jsx)(`span`,{className:`text-sm font-semibold`,style:{color:i?`var(--color-foreground)`:o},children:a}),s&&(0,H.jsx)(`span`,{className:`rounded bg-surface-elevated px-1.5 py-px text-[10px] font-medium text-text-secondary`,children:`leader`}),l&&(0,H.jsxs)(`span`,{className:`text-[11px] text-text-subtle`,children:[`→ `,l]}),(0,H.jsx)(`span`,{className:`text-[11px] text-text-subtle`,children:W(e.createdAt)})]}),(0,H.jsx)(`div`,{className:`mt-0.5 whitespace-pre-wrap break-words text-sm leading-relaxed text-text-secondary`,children:G(c,t)})]})]});return e.fullSessionRef?(0,H.jsxs)(D,{children:[(0,H.jsx)(N,{asChild:!0,children:u}),(0,H.jsx)(x,{children:(0,H.jsxs)(w,{onClick:()=>r(e),children:[(0,H.jsx)(R,{className:`size-4`}),` View full transcript`]})})]}):u}),K={idle:`bg-text-subtle`,working:`bg-primary animate-pulse`,done:`bg-emerald-500`,error:`bg-destructive`},q={idle:`Idle`,working:`Working`,done:`Done`,error:`Error`},J=(0,B.memo)(function({members:e,liveStatus:t,typing:n,busy:r,onAdd:i,onEdit:o,onRemove:s}){return(0,H.jsxs)(`div`,{className:`flex flex-col gap-0.5 p-2`,children:[e.map(e=>{let i=n[e.name],c=t.get(e.id)??e.status;return(0,H.jsxs)(`div`,{className:`group flex items-center gap-2 rounded-md px-2 py-1.5`,children:[(0,H.jsx)(`span`,{className:`flex size-6 shrink-0 items-center justify-center rounded-full text-[10px] font-semibold text-white ring-2 ring-background`,style:{backgroundColor:e.color??`var(--accent)`},"aria-hidden":!0,children:e.name.slice(0,1).toUpperCase()}),(0,H.jsxs)(`div`,{className:`min-w-0 flex-1`,children:[(0,H.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[(0,H.jsx)(`span`,{className:`truncate text-xs font-medium`,style:{color:e.color??`var(--color-foreground)`},children:e.name}),e.role===`leader`&&(0,H.jsx)(`span`,{className:`rounded-sm bg-accent-wash px-1 text-[9px] font-semibold uppercase text-primary`,children:`lead`})]}),(0,H.jsx)(`span`,{className:`block truncate text-[10px] text-text-subtle`,children:i?`typing…`:e.persona||q[c]})]}),(0,H.jsxs)(`div`,{className:`flex shrink-0 items-center gap-0.5 can-hover:opacity-0 can-hover:group-hover:opacity-100`,children:[(0,H.jsx)(`button`,{type:`button`,onClick:()=>o(e),disabled:r,"aria-label":`Edit ${e.name}`,className:`flex size-8 items-center justify-center rounded-md text-text-subtle hover:bg-surface-elevated hover:text-foreground active:bg-surface-elevated disabled:opacity-40`,children:(0,H.jsx)(a,{className:`size-3.5`})}),(0,H.jsx)(`button`,{type:`button`,onClick:()=>s(e),disabled:r,"aria-label":`Remove ${e.name}`,className:`flex size-8 items-center justify-center rounded-md text-text-subtle hover:bg-surface-elevated hover:text-destructive active:bg-surface-elevated disabled:opacity-40`,children:(0,H.jsx)(j,{className:`size-3.5`})})]}),(0,H.jsx)(`span`,{className:g(`size-2 shrink-0 rounded-full`,K[c]),title:q[c],"aria-hidden":!0})]},e.id)}),(0,H.jsxs)(`button`,{type:`button`,onClick:i,disabled:r,className:`mt-1 flex items-center gap-2 rounded-md px-2 py-2 text-xs font-medium text-primary hover:bg-surface-elevated active:bg-surface-elevated disabled:opacity-40`,children:[(0,H.jsx)(z,{className:`size-4`}),` Add member`]})]})}),Y=`w-full min-h-[40px] rounded-md border border-border bg-background px-3 py-2 text-sm text-foreground placeholder:text-text-subtle focus:outline-none focus:ring-1 focus:ring-primary`;function oe({open:e,member:t,projectName:n,providerId:r=`claude`,onSubmit:i,onClose:a}){let c=s(),[l,u]=(0,B.useState)(t?.name??``),[g,_]=(0,B.useState)(t?.persona??``),[v,y]=(0,B.useState)(t?.model??null),[b,x]=(0,B.useState)(t?.role===`leader`),[S,C]=(0,B.useState)(!1),[w,E]=(0,B.useState)(null),ee=(0,B.useCallback)(async()=>{let e=l.trim();if(!e){E(`Name is required`);return}C(!0),E(null);try{await i({name:e,persona:g.trim()||null,model:v,role:b?`leader`:`member`}),a()}catch(e){E(e instanceof Error?e.message:`Failed to save member`),C(!1)}},[l,g,v,b,i,a]),D=t?.role===`leader`,O=(0,H.jsxs)(`div`,{className:`flex flex-col gap-3 px-1 py-1`,children:[(0,H.jsx)(`input`,{className:Y,value:l,onChange:e=>u(e.target.value),placeholder:`Member name`,autoFocus:!0}),(0,H.jsx)(`input`,{className:Y,value:g,onChange:e=>_(e.target.value),placeholder:`Persona (optional)`}),(0,H.jsx)(M,{value:v,onChange:y,projectName:n,providerId:r}),(0,H.jsxs)(`label`,{className:X(D),children:[(0,H.jsx)(`input`,{type:`checkbox`,className:`size-4 accent-[var(--color-primary)]`,checked:b,disabled:D,onChange:e=>x(e.target.checked)}),(0,H.jsx)(`span`,{children:D?`Leader (reassign from another member)`:`Set as leader`})]}),w&&(0,H.jsx)(`p`,{className:`text-xs text-destructive`,children:w}),(0,H.jsxs)(`div`,{className:`flex justify-end gap-2 pt-1`,children:[(0,H.jsx)(f,{variant:`outline`,onClick:a,disabled:S,children:`Cancel`}),(0,H.jsxs)(f,{onClick:ee,disabled:S,children:[S&&(0,H.jsx)(T,{className:`mr-1.5 size-4 animate-spin`}),t?`Save`:`Add member`]})]})]});if(!e)return null;let k=t?`Edit member`:`Add member`;return c?(0,H.jsx)(o,{open:!0,onClose:a,className:`flex max-h-[90vh] flex-col`,children:(0,H.jsxs)(`div`,{className:`flex min-h-0 flex-col px-4 pb-2`,children:[(0,H.jsx)(`h2`,{className:`mb-2 text-base font-semibold text-foreground`,children:k}),O]})}):(0,H.jsx)(h,{open:!0,onOpenChange:e=>{e||a()},children:(0,H.jsxs)(p,{className:`flex flex-col sm:max-w-md`,children:[(0,H.jsx)(d,{children:(0,H.jsx)(m,{children:k})}),O]})})}function X(e){return`flex items-center gap-2 text-sm text-text-secondary ${e?`opacity-60`:``}`}function se({messages:e,config:t,projectName:n}){return(0,H.jsxs)(`div`,{className:`flex flex-col gap-3`,children:[t&&(0,H.jsx)(Z,{config:t}),e.map(e=>(0,H.jsxs)(`div`,{className:`flex flex-col gap-1.5`,children:[(0,H.jsx)(`span`,{className:`text-[10px] font-semibold uppercase tracking-wide text-text-subtle`,children:e.role===`user`?`User`:e.role===`assistant`?`Assistant`:e.role}),(0,H.jsx)(ce,{message:e,projectName:n})]},e.id)),e.length===0&&(0,H.jsx)(`p`,{className:`py-6 text-center text-sm text-text-subtle`,children:`Transcript is empty.`})]})}function ce({message:e,projectName:t}){let n=e.events??[];if(n.length===0)return e.content.trim()?(0,H.jsx)(L,{content:e.content}):null;let r=new Map;for(let e of n)e.type===`tool_result`&&e.toolUseId&&r.set(e.toolUseId,e);return(0,H.jsx)(`div`,{className:`flex flex-col gap-2`,children:n.map((e,n)=>{if(e.type===`text`)return e.content.trim()?(0,H.jsx)(L,{content:e.content},n):null;if(e.type===`thinking`)return(0,H.jsx)(le,{content:e.content},n);if(e.type===`tool_use`){let i=e.toolUseId?r.get(e.toolUseId):void 0;return(0,H.jsx)(u,{tool:e,result:i,completed:!!i,projectName:t},n)}return null})})}function le({content:e}){let[t,n]=(0,B.useState)(!1);return(0,H.jsxs)(`div`,{className:`rounded-md border border-border bg-surface-elevated/40`,children:[(0,H.jsxs)(`button`,{type:`button`,onClick:()=>n(e=>!e),className:`flex w-full items-center gap-1.5 px-2.5 py-1.5 text-xs text-text-subtle hover:text-text-secondary`,children:[(0,H.jsx)(c,{className:g(`size-3.5 transition-transform`,t&&`rotate-90`)}),(0,H.jsx)(i,{className:`size-3.5`}),(0,H.jsx)(`span`,{children:`Thinking`})]}),t&&(0,H.jsx)(`div`,{className:`border-t border-border px-3 py-2 text-xs leading-relaxed text-text-secondary`,children:(0,H.jsx)(L,{content:e})})]})}function Z({config:e}){let[t,n]=(0,B.useState)(!1),r=[[`Model`,e.model],[`Permission`,e.permissionMode],[`CWD`,e.cwd],[`Git branch`,e.gitBranch],[`CLI version`,e.version]].filter(([,e])=>e);return r.length===0?null:(0,H.jsxs)(`div`,{className:`rounded-md border border-border bg-surface-elevated/40`,children:[(0,H.jsxs)(`button`,{type:`button`,onClick:()=>n(e=>!e),className:`flex w-full items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-text-secondary hover:text-foreground`,children:[(0,H.jsx)(c,{className:g(`size-3.5 transition-transform`,t&&`rotate-90`)}),(0,H.jsx)(I,{className:`size-3.5`}),(0,H.jsx)(`span`,{children:`Input config`})]}),t&&(0,H.jsx)(`dl`,{className:`grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 border-t border-border px-3 py-2 text-xs`,children:r.map(([e,t])=>(0,H.jsxs)(`div`,{className:`contents`,children:[(0,H.jsx)(`dt`,{className:`text-text-subtle`,children:e}),(0,H.jsx)(`dd`,{className:`break-all font-mono text-text-secondary`,children:t})]},e))})]})}function ue({groupId:e,message:t,onClose:n}){let r=s(),[i,a]=(0,B.useState)(null),[c,l]=(0,B.useState)(!1),[u,f]=(0,B.useState)(null),g=t?.fullSessionRef??null;if((0,B.useEffect)(()=>{if(!t||!g)return;let n=!1;return a(null),f(null),l(!0),O(e,g).then(e=>{n||a(e)}).catch(e=>{n||f(e instanceof Error?e.message:`Failed to load transcript`)}).finally(()=>{n||l(!1)}),()=>{n=!0}},[e,g,t]),!t)return null;let _=(0,H.jsxs)(`div`,{className:`min-h-0 flex-1 overflow-y-auto`,children:[c&&(0,H.jsx)(`div`,{className:`flex items-center justify-center py-10`,children:(0,H.jsx)(T,{className:`size-5 animate-spin text-primary`})}),u&&(0,H.jsxs)(`div`,{className:`px-1 py-4 text-sm text-text-secondary`,children:[(0,H.jsx)(`p`,{className:`mb-2 font-medium text-foreground`,children:`Full transcript unavailable`}),(0,H.jsx)(`p`,{className:`text-xs text-text-subtle`,children:u}),t.summary&&(0,H.jsx)(`pre`,{className:`mt-4 whitespace-pre-wrap break-words rounded-md bg-surface-elevated p-3 text-xs leading-relaxed text-text-secondary`,children:t.summary})]}),!c&&!u&&i!=null&&(0,H.jsx)(`div`,{className:`px-1 py-1`,children:(0,H.jsx)(se,{messages:i.messages,config:i.config})})]}),v=`${t.fromMember} — full transcript`;return r?(0,H.jsx)(o,{open:!0,onClose:n,className:`max-h-[85vh] flex flex-col`,children:(0,H.jsxs)(`div`,{className:`flex flex-col min-h-0 px-4 pb-2`,children:[(0,H.jsx)(`h2`,{className:`mb-2 text-sm font-semibold text-foreground`,children:v}),_]})}):(0,H.jsx)(h,{open:!0,onOpenChange:e=>{e||n()},children:(0,H.jsxs)(p,{className:`flex max-h-[80vh] flex-col sm:max-w-2xl`,children:[(0,H.jsx)(d,{children:(0,H.jsx)(m,{className:`text-sm`,children:v})}),_]})})}function Q({metadata:e}){let t=e?.groupId??null,{activeProject:n}=_(C(e=>({activeProject:e.activeProject}))),r=e?.projectName??n?.name??``,i=s(),{messages:a,members:c,status:l,typing:u,loading:d,isRunning:p,error:m,sendMessage:h,stop:x,resume:w}=ie(t,r),[D,O]=(0,B.useState)(``),[k,A]=(0,B.useState)(null),[j,M]=(0,B.useState)(!1),N=(0,B.useRef)(null),[P,F]=(0,B.useState)([]),[I,L]=(0,B.useState)(null),[R,z]=(0,B.useState)(!1),[V,U]=(0,B.useState)(null),[W,G]=(0,B.useState)(null),K=(0,B.useCallback)(async()=>{if(t)try{let e=await te(t);F(e.members),L(e.maxTurns)}catch{}},[t]),q=(0,B.useCallback)(async e=>{if(!t)return;let n=Math.max(1,Math.min(50,Math.floor(e)||1));L(n);try{await y(t,{maxTurns:n})}catch{K()}},[t,K]);(0,B.useEffect)(()=>{F([]),K()},[t,K]),(0,B.useEffect)(()=>{N.current?.scrollIntoView({block:`end`})},[a.length,u]);let Y=(0,B.useMemo)(()=>new Map(c.map(e=>[e.id,e.status])),[c]),X=P.length?P:c,se=(0,B.useMemo)(()=>{let e=new Map(X.map(e=>[e.name,e.color]));return t=>e.get(t)??null},[X]),ce=(0,B.useMemo)(()=>{let e=new Set(X.filter(e=>e.role===`leader`).map(e=>e.name));return t=>e.has(t)},[X]),le=(0,B.useCallback)(async e=>{t&&(W?.member?await E(t,W.member.id,e):await ne(t,e),await K())},[t,W,K]),Z=(0,B.useCallback)(async e=>{if(t){z(!0),U(null);try{await re(t,e.id),await K()}catch(e){U(e instanceof Error?e.message:`Failed to remove member`)}finally{z(!1)}}},[t,K]),Q=(0,B.useCallback)(()=>{let e=D.trim();e&&(h(e),O(``))},[D,h]),$=Object.keys(u).filter(e=>u[e]);return t?(0,H.jsxs)(`div`,{className:`flex h-full min-h-0`,children:[(0,H.jsxs)(`div`,{className:`flex min-h-0 flex-1 flex-col`,children:[(0,H.jsxs)(`div`,{className:`flex h-11 shrink-0 items-center justify-between border-b border-border px-3`,children:[(0,H.jsxs)(`div`,{className:`flex items-center gap-2 text-sm font-medium text-foreground`,children:[(0,H.jsx)(`span`,{className:g(`size-2 rounded-full`,l===`active`?`bg-primary animate-pulse`:l===`paused`?`bg-amber-500`:`bg-text-subtle`)}),(0,H.jsx)(`span`,{className:`capitalize`,children:l})]}),(0,H.jsxs)(`div`,{className:`flex items-center gap-1.5`,children:[p?(0,H.jsxs)(f,{size:`sm`,variant:`outline`,onClick:x,className:`h-8`,children:[(0,H.jsx)(S,{className:`mr-1 size-3.5`}),` Stop`]}):l===`paused`?(0,H.jsxs)(f,{size:`sm`,variant:`outline`,onClick:w,className:`h-8`,children:[(0,H.jsx)(ee,{className:`mr-1 size-3.5`}),` Resume`]}):null,(0,H.jsx)(`button`,{type:`button`,onClick:()=>M(e=>!e),className:`flex size-8 items-center justify-center rounded-md text-text-subtle hover:bg-surface-elevated hover:text-foreground`,"aria-label":`Toggle roster`,children:(0,H.jsx)(b,{className:`size-4`})})]})]}),(0,H.jsx)(`div`,{className:`min-h-0 flex-1 overflow-y-auto`,children:d?(0,H.jsx)(`div`,{className:`flex items-center justify-center py-10`,children:(0,H.jsx)(T,{className:`size-5 animate-spin text-primary`})}):a.length===0?(0,H.jsx)(`div`,{className:`flex h-full items-center justify-center p-4 text-center text-sm text-text-subtle`,children:`Send a message to start the group.`}):(0,H.jsxs)(`div`,{className:`flex flex-col py-2`,children:[a.map(e=>(0,H.jsx)(ae,{message:e,colorFor:se,isLeader:ce,onViewFull:A},e.id)),$.length>0&&(0,H.jsxs)(`div`,{className:`px-3 py-1.5 text-xs italic text-text-subtle`,children:[$.join(`, `),` `,$.length===1?`is`:`are`,` typing…`]}),(0,H.jsx)(`div`,{ref:N})]})}),m&&(0,H.jsx)(`div`,{className:`shrink-0 border-t border-border bg-destructive/10 px-3 py-2 text-xs text-destructive`,children:m}),(0,H.jsx)(`div`,{className:`shrink-0 border-t border-border p-2`,children:(0,H.jsxs)(`div`,{className:`flex items-end gap-2`,children:[(0,H.jsx)(`textarea`,{value:D,onChange:e=>O(e.target.value),onKeyDown:e=>{e.key===`Enter`&&!e.shiftKey&&!i&&(e.preventDefault(),Q())},rows:1,placeholder:`Message the group…`,className:`max-h-32 min-h-[44px] flex-1 resize-none rounded-md border border-border bg-background px-3 py-2.5 text-sm text-foreground placeholder:text-text-subtle focus:outline-none focus:ring-1 focus:ring-primary`}),(0,H.jsx)(f,{onClick:Q,disabled:!D.trim(),className:`size-11 shrink-0 p-0`,"aria-label":`Send`,children:(0,H.jsx)(v,{className:`size-4`})})]})})]}),i?(0,H.jsxs)(o,{open:j,onClose:()=>M(!1),className:`flex max-h-[72vh] flex-col`,children:[(0,H.jsx)(`div`,{className:`flex h-11 shrink-0 items-center px-4 text-sm font-medium text-foreground`,children:`Members`}),(0,H.jsxs)(`div`,{className:`min-h-0 overflow-y-auto`,children:[I!==null&&(0,H.jsxs)(`label`,{className:`flex items-center gap-2 px-4 py-2 text-xs text-text-secondary`,children:[(0,H.jsx)(`span`,{children:`Reply cap`}),(0,H.jsx)(`input`,{type:`number`,min:1,max:50,value:I,onChange:e=>q(Number(e.target.value)),className:`w-14 rounded border border-border bg-background px-1.5 py-1 text-xs text-foreground`}),(0,H.jsx)(`span`,{className:`text-text-subtle`,children:`turns/msg`})]}),V&&(0,H.jsx)(`p`,{className:`px-3 py-1 text-xs text-destructive`,children:V}),(0,H.jsx)(J,{members:P,liveStatus:Y,typing:u,busy:R,onAdd:()=>G({member:null}),onEdit:e=>G({member:e}),onRemove:Z})]})]}):j&&(0,H.jsxs)(`div`,{className:`flex w-60 shrink-0 flex-col border-l border-border bg-panel`,children:[(0,H.jsx)(`div`,{className:`flex h-11 shrink-0 items-center border-b border-border px-3 text-sm font-medium text-foreground`,children:`Members`}),(0,H.jsxs)(`div`,{className:`min-h-0 flex-1 overflow-y-auto`,children:[I!==null&&(0,H.jsxs)(`label`,{className:`flex items-center gap-2 px-3 py-2 text-xs text-text-secondary`,children:[(0,H.jsx)(`span`,{children:`Reply cap`}),(0,H.jsx)(`input`,{type:`number`,min:1,max:50,value:I,onChange:e=>q(Number(e.target.value)),className:`w-14 rounded border border-border bg-background px-1.5 py-1 text-xs text-foreground`}),(0,H.jsx)(`span`,{className:`text-text-subtle`,children:`turns/msg`})]}),V&&(0,H.jsx)(`p`,{className:`px-3 py-1 text-xs text-destructive`,children:V}),(0,H.jsx)(J,{members:P,liveStatus:Y,typing:u,busy:R,onAdd:()=>G({member:null}),onEdit:e=>G({member:e}),onRemove:Z})]})]}),W&&(0,H.jsx)(oe,{open:!0,member:W.member,projectName:r,onSubmit:le,onClose:()=>G(null)},W.member?.id??`add`),(0,H.jsx)(ue,{groupId:t,message:k,onClose:()=>A(null)})]}):(0,H.jsx)(`div`,{className:`flex h-full items-center justify-center p-4 text-sm text-text-subtle`,children:`No group selected.`})}export{Q as GroupChatTab};
|