@hienlh/ppm 0.12.11 → 0.12.12
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 +9 -0
- package/bun.lock +2062 -0
- package/bunfig.toml +2 -0
- package/dist/web/assets/ai-settings-section-NNWp6nw7.js +1 -0
- package/dist/web/assets/{api-settings-DAk7D-NP.js → api-settings-C3T95dWg.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DDuzYaUV.js +1 -0
- package/dist/web/assets/{audio-preview-DnQmf9fu.js → audio-preview-BkbgGtDH.js} +1 -1
- package/dist/web/assets/chat-tab-BZlP1qjX.js +12 -0
- package/dist/web/assets/chevron-up-BWBvMZkp.js +1 -0
- package/dist/web/assets/{code-editor-B-lU1fz3.js → code-editor-BtspASkW.js} +4 -4
- package/dist/web/assets/{conflict-editor-BYzf3LuW.js → conflict-editor-Dgsu6fmj.js} +1 -1
- package/dist/web/assets/{csv-preview-HMSavgBb.js → csv-preview-DcWCjQkZ.js} +1 -1
- package/dist/web/assets/{database-viewer-DjvnIn8p.js → database-viewer-C85RxdMV.js} +2 -2
- package/dist/web/assets/diff-viewer-2pPy97Tl.js +4 -0
- package/dist/web/assets/{esm-K1XIK4vc.js → esm-_CLpyLJ_.js} +1 -1
- package/dist/web/assets/{extension-store-3yZYn07W.js → extension-store-BZDZ9QRc.js} +1 -1
- package/dist/web/assets/{extension-webview-4xMREn_x.js → extension-webview-U1lMYZ0p.js} +1 -1
- package/dist/web/assets/{file-store-BrbCNyLm.js → file-store-4BpOJthN.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-BURAevTc.js +1 -0
- package/dist/web/assets/{image-preview-CkS2PVdQ.js → image-preview-BcT1SbY2.js} +1 -1
- package/dist/web/assets/index-BWSRKVZn.js +23 -0
- package/dist/web/assets/index-b6tIZImC.css +2 -0
- package/dist/web/assets/info-3K5VOQVL-tSD4Fpi3.js +1 -0
- package/dist/web/assets/{input-Dk49gO8E.js → input-2eDVjcRZ.js} +1 -1
- package/dist/web/assets/{keybindings-store-B-zET-0o.js → keybindings-store-BOG1yviy.js} +1 -1
- package/dist/web/assets/keybindings-store-BvdUoEC7.js +1 -0
- package/dist/web/assets/{markdown-renderer-Bj2B05Km.js → markdown-renderer-Dbam_-04.js} +3 -3
- package/dist/web/assets/packet-RMMSAZCW-DmDLZUrV.js +1 -0
- package/dist/web/assets/{pdf-preview-CCyw5cuH.js → pdf-preview-BmHVGx32.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-w03Pc9ZR.js +1 -0
- package/dist/web/assets/{port-forwarding-tab-Cebb5Eix.js → port-forwarding-tab-Dkq1upWC.js} +1 -1
- package/dist/web/assets/{postgres-viewer-BrOiliEv.js → postgres-viewer-BgBJAJ9q.js} +3 -3
- package/dist/web/assets/pre-compact-button-Dp7Hs49L.js +1 -0
- package/dist/web/assets/pre-compact-section-DnM5fGSR.js +1 -0
- package/dist/web/assets/radar-KQ55EAFF-C9XQvoey.js +1 -0
- package/dist/web/assets/{scroll-area-BEllam7_.js → scroll-area-CdxNNnN-.js} +1 -1
- package/dist/web/assets/{settings-store-BLLR7ed8.js → settings-store-CMAssqyb.js} +2 -2
- package/dist/web/assets/settings-tab-zYWKTq5z.js +1 -0
- package/dist/web/assets/{sql-query-editor-CVAnRFbi.js → sql-query-editor-b7zJ8XPp.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-OEVq_-Po.js → sqlite-viewer-4lLAz1es.js} +1 -1
- package/dist/web/assets/{tab-store-B3M9hjho.js → tab-store-DNBsLdPn.js} +1 -1
- package/dist/web/assets/{terminal-tab-MjmJaQyA.js → terminal-tab-BtnqkN1H.js} +1 -1
- package/dist/web/assets/treemap-KZPCXAKY-lmftxSky.js +1 -0
- package/dist/web/assets/{use-blob-url-e9uTXjv5.js → use-blob-url-QX-XajU8.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-BkZDwoVd.js → use-monaco-theme-D68oX3XU.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-Dx86tuVP.js → vendor-mermaid-sQS4C_iL.js} +2 -2
- package/dist/web/assets/{video-preview-B819qvlp.js → video-preview-CkOKvVLt.js} +1 -1
- package/dist/web/index.html +18 -18
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -0
- package/src/providers/claude-agent-sdk.ts +1 -135
- package/src/server/index.ts +2 -1
- package/src/server/routes/chat.ts +18 -0
- package/src/server/routes/git.ts +16 -0
- package/src/services/git.service.ts +34 -0
- package/src/services/jsonl-transcript-parser.ts +216 -0
- package/src/services/supervisor.ts +2 -1
- package/src/web/components/chat/message-list.tsx +41 -2
- package/src/web/components/chat/pre-compact-button.tsx +50 -0
- package/src/web/components/chat/pre-compact-section.tsx +69 -0
- package/src/web/components/editor/diff-viewer.tsx +21 -5
- package/dist/web/assets/ai-settings-section-QE6nBNgN.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-DvZbltvY.js +0 -1
- package/dist/web/assets/chat-tab-Cf6T3mGO.js +0 -12
- package/dist/web/assets/diff-viewer-CP2jcR5J.js +0 -4
- package/dist/web/assets/gitGraph-HDMCJU4V-BxhdxFgj.js +0 -1
- package/dist/web/assets/index-BTjuH4fn.css +0 -2
- package/dist/web/assets/index-FGlF8IWZ.js +0 -23
- package/dist/web/assets/info-3K5VOQVL-BwAZ2zd8.js +0 -1
- package/dist/web/assets/keybindings-store-DaBV6qhz.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-tx2n5Qry.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-D6S2MqVT.js +0 -1
- package/dist/web/assets/plus-51UQ45rf.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-BviZcL-b.js +0 -1
- package/dist/web/assets/settings-tab-D0XjupJm.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-CM54VdaB.js +0 -1
- /package/dist/web/assets/{api-client-Dvzcc_EO.js → api-client-DIhJ5qVW.js} +0 -0
- /package/dist/web/assets/{csv-parser--2WJNgS7.js → csv-parser-B5QW8pZ6.js} +0 -0
- /package/dist/web/assets/{dist-im4ynINo.js → dist-GtkSekuX.js} +0 -0
- /package/dist/web/assets/{katex-CKoArbIw.js → katex-C3cZrCvP.js} +0 -0
- /package/dist/web/assets/{lib-DQHnkzGy.js → lib-Bu71-TFS.js} +0 -0
- /package/dist/web/assets/{react-GqWghJ-L.js → react-DMIOAtcX.js} +0 -0
- /package/dist/web/assets/{refresh-cw-LlbZDJpO.js → refresh-cw-BjrAbUJe.js} +0 -0
- /package/dist/web/assets/{sql-completion-provider-C3cq9j99.js → sql-completion-provider-CULTsCqR.js} +0 -0
- /package/dist/web/assets/{table-Dq575bPF.js → table-tf7pRkME.js} +0 -0
- /package/dist/web/assets/{text-wrap-Cn6BNQfq.js → text-wrap-BV-R4Vvy.js} +0 -0
- /package/dist/web/assets/{trash-2-CJYoLw7Q.js → trash-2-DjQOpgUV.js} +0 -0
- /package/dist/web/assets/{utils-CTg5uAYR.js → utils-CQux7CsO.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-CU2c3f0A.js → vendor-xterm-K3_Xwigj.js} +0 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a Claude Code JSONL transcript file into ChatMessage[].
|
|
3
|
+
* Reusable across live SDK session history (claude-agent-sdk.ts) and
|
|
4
|
+
* pre-compact transcript loading (chat route /pre-compact-messages).
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
9
|
+
import type { ChatEvent, ChatMessage } from "../types/chat.ts";
|
|
10
|
+
|
|
11
|
+
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
12
|
+
const TEAMMATE_MSG_RE = /<teammate-message[^>]*>[\s\S]*?<\/teammate-message>/g;
|
|
13
|
+
|
|
14
|
+
/** Strip SDK teammate-message XML tags from assistant text */
|
|
15
|
+
export function stripTeammateXml(text: string): string {
|
|
16
|
+
if (!text.includes("<teammate-message")) return text;
|
|
17
|
+
return text.replace(TEAMMATE_MSG_RE, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Extract plain text from message payload */
|
|
21
|
+
export function extractText(message: unknown): string {
|
|
22
|
+
if (!message || typeof message !== "object") return "";
|
|
23
|
+
const msg = message as Record<string, unknown>;
|
|
24
|
+
if (typeof msg.content === "string") return msg.content;
|
|
25
|
+
if (Array.isArray(msg.content)) {
|
|
26
|
+
return (msg.content as Array<Record<string, unknown>>)
|
|
27
|
+
.filter((b) => b.type === "text" && typeof b.text === "string")
|
|
28
|
+
.map((b) => b.text as string)
|
|
29
|
+
.join("");
|
|
30
|
+
}
|
|
31
|
+
return "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Parse SDK SessionMessage into ChatMessage with events for tool_use blocks */
|
|
35
|
+
export function parseSessionMessage(
|
|
36
|
+
msg: { uuid: string; type: string; message: unknown; parent_tool_use_id?: string | null },
|
|
37
|
+
): ChatMessage {
|
|
38
|
+
const message = msg.message as Record<string, unknown> | undefined;
|
|
39
|
+
const role = msg.type as "user" | "assistant";
|
|
40
|
+
const parentId = (msg as any).parent_tool_use_id as string | undefined;
|
|
41
|
+
|
|
42
|
+
// Filter synthetic SDK-generated error messages (auth failures, rate limits, etc.)
|
|
43
|
+
const isSdkErrorMessage =
|
|
44
|
+
(msg as any).isApiErrorMessage === true ||
|
|
45
|
+
typeof (msg as any).error === "string" ||
|
|
46
|
+
(message && (message as any).model === "<synthetic>" &&
|
|
47
|
+
Array.isArray(message.content) &&
|
|
48
|
+
(message.content as Array<Record<string, unknown>>).some(
|
|
49
|
+
(b) => b.type === "text" && typeof b.text === "string" &&
|
|
50
|
+
/Failed to authenticate|API Error: 40[13]|hit your limit|rate.?limit/i.test(b.text as string),
|
|
51
|
+
));
|
|
52
|
+
if (isSdkErrorMessage) {
|
|
53
|
+
return {
|
|
54
|
+
id: msg.uuid,
|
|
55
|
+
role,
|
|
56
|
+
content: "",
|
|
57
|
+
timestamp: new Date().toISOString(),
|
|
58
|
+
sdkUuid: msg.uuid,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const events: ChatEvent[] = [];
|
|
63
|
+
let textContent = "";
|
|
64
|
+
|
|
65
|
+
if (message && Array.isArray(message.content)) {
|
|
66
|
+
for (const block of message.content as Array<Record<string, unknown>>) {
|
|
67
|
+
if (block.type === "text" && typeof block.text === "string") {
|
|
68
|
+
const cleaned = role === "assistant" ? stripTeammateXml(block.text) : block.text;
|
|
69
|
+
textContent += cleaned;
|
|
70
|
+
if (role === "assistant" && cleaned) {
|
|
71
|
+
events.push({ type: "text", content: cleaned, ...(parentId && { parentToolUseId: parentId }) });
|
|
72
|
+
}
|
|
73
|
+
} else if (block.type === "tool_use") {
|
|
74
|
+
events.push({
|
|
75
|
+
type: "tool_use",
|
|
76
|
+
tool: (block.name as string) ?? "unknown",
|
|
77
|
+
input: block.input ?? {},
|
|
78
|
+
toolUseId: block.id as string | undefined,
|
|
79
|
+
...(parentId && { parentToolUseId: parentId }),
|
|
80
|
+
});
|
|
81
|
+
} else if (block.type === "tool_result") {
|
|
82
|
+
const output = block.content ?? block.output ?? "";
|
|
83
|
+
events.push({
|
|
84
|
+
type: "tool_result",
|
|
85
|
+
output: typeof output === "string" ? output : JSON.stringify(output),
|
|
86
|
+
isError: !!(block as Record<string, unknown>).is_error,
|
|
87
|
+
toolUseId: block.tool_use_id as string | undefined,
|
|
88
|
+
...(parentId && { parentToolUseId: parentId }),
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
textContent = extractText(message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// SDK-generated user messages carry system text (tool_result blocks, teammate XML) —
|
|
97
|
+
// clear so they don't render as user bubbles.
|
|
98
|
+
if (role === "user" && (events.some((e) => e.type === "tool_result") || textContent.includes("<teammate-message"))) {
|
|
99
|
+
textContent = "";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
id: msg.uuid,
|
|
104
|
+
role,
|
|
105
|
+
content: textContent,
|
|
106
|
+
events: events.length > 0 ? events : undefined,
|
|
107
|
+
timestamp: new Date().toISOString(),
|
|
108
|
+
sdkUuid: msg.uuid,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Move events with parentToolUseId into their parent Agent/Task tool_use's children array.
|
|
114
|
+
* Mutates the array in-place.
|
|
115
|
+
*/
|
|
116
|
+
export function nestChildEvents(events: ChatEvent[]): void {
|
|
117
|
+
const parentMap = new Map<string, ChatEvent & { type: "tool_use" }>();
|
|
118
|
+
for (const ev of events) {
|
|
119
|
+
if (ev.type === "tool_use" && (ev.tool === "Agent" || ev.tool === "Task") && ev.toolUseId) {
|
|
120
|
+
parentMap.set(ev.toolUseId, ev);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (parentMap.size === 0) return;
|
|
124
|
+
|
|
125
|
+
const childIndices: number[] = [];
|
|
126
|
+
for (let i = 0; i < events.length; i++) {
|
|
127
|
+
const ev = events[i]!;
|
|
128
|
+
const pid = (ev as any).parentToolUseId as string | undefined;
|
|
129
|
+
if (!pid) continue;
|
|
130
|
+
const parent = parentMap.get(pid);
|
|
131
|
+
if (parent) {
|
|
132
|
+
if (!parent.children) parent.children = [];
|
|
133
|
+
parent.children.push(ev);
|
|
134
|
+
childIndices.push(i);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (let i = childIndices.length - 1; i >= 0; i--) {
|
|
138
|
+
events.splice(childIndices[i]!, 1);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Validate JSONL path — must be under ~/.claude/ (prevents arbitrary file reads).
|
|
144
|
+
* Throws Error with descriptive message. Returns resolved realpath on success.
|
|
145
|
+
*/
|
|
146
|
+
export function validateJsonlPath(inputPath: string): string {
|
|
147
|
+
if (!inputPath) throw new Error("jsonlPath is required");
|
|
148
|
+
// Reject obvious traversal attempts before resolution
|
|
149
|
+
if (inputPath.includes("\0")) throw new Error("Invalid path: denied");
|
|
150
|
+
if (!inputPath.endsWith(".jsonl")) throw new Error("Invalid path: must be a .jsonl file");
|
|
151
|
+
|
|
152
|
+
const resolved = resolve(inputPath);
|
|
153
|
+
if (!existsSync(resolved)) throw new Error("File not found");
|
|
154
|
+
|
|
155
|
+
let real: string;
|
|
156
|
+
try {
|
|
157
|
+
real = realpathSync(resolved);
|
|
158
|
+
} catch {
|
|
159
|
+
throw new Error("File not found");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const claudeDir = resolve(homedir(), ".claude") + "/";
|
|
163
|
+
if (!(real + "/").startsWith(claudeDir)) {
|
|
164
|
+
throw new Error("Access denied: path traversal detected");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const stat = statSync(real);
|
|
168
|
+
if (!stat.isFile()) throw new Error("Not a regular file");
|
|
169
|
+
if (stat.size > MAX_FILE_SIZE) {
|
|
170
|
+
throw new Error(`File too large: ${Math.round(stat.size / 1024 / 1024)}MB exceeds 50MB limit`);
|
|
171
|
+
}
|
|
172
|
+
return real;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Read a JSONL transcript file, parse entries, apply merge/nest pipeline, return ChatMessage[].
|
|
177
|
+
* Applies the same logic as ClaudeAgentSdkProvider.getMessages() but reads from file directly.
|
|
178
|
+
*/
|
|
179
|
+
export async function parseJsonlTranscript(filePath: string): Promise<ChatMessage[]> {
|
|
180
|
+
const text = await Bun.file(filePath).text();
|
|
181
|
+
const parsed: ChatMessage[] = [];
|
|
182
|
+
for (const line of text.split("\n")) {
|
|
183
|
+
const trimmed = line.trim();
|
|
184
|
+
if (!trimmed) continue;
|
|
185
|
+
let entry: any;
|
|
186
|
+
try {
|
|
187
|
+
entry = JSON.parse(trimmed);
|
|
188
|
+
} catch {
|
|
189
|
+
continue; // skip malformed lines defensively
|
|
190
|
+
}
|
|
191
|
+
if (entry.type !== "user" && entry.type !== "assistant") continue;
|
|
192
|
+
if (!entry.uuid || !entry.message) continue;
|
|
193
|
+
parsed.push(parseSessionMessage(entry));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Merge tool_result-only user messages into preceding assistant
|
|
197
|
+
const merged: ChatMessage[] = [];
|
|
198
|
+
for (const msg of parsed) {
|
|
199
|
+
if (msg.events?.length && msg.events.every((e) => e.type === "tool_result")) {
|
|
200
|
+
const lastAssistant = [...merged].reverse().find((m) => m.role === "assistant");
|
|
201
|
+
if (lastAssistant?.events) {
|
|
202
|
+
lastAssistant.events.push(...msg.events);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
merged.push(msg);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
for (const msg of merged) {
|
|
210
|
+
if (msg.events) nestChildEvents(msg.events);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return merged.filter(
|
|
214
|
+
(msg) => msg.content.trim().length > 0 || (msg.events && msg.events.length > 0),
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -949,7 +949,8 @@ if (process.argv.includes("__supervise__")) {
|
|
|
949
949
|
const idx = process.argv.indexOf("__supervise__");
|
|
950
950
|
const port = parseInt(process.argv[idx + 1] ?? "8080", 10);
|
|
951
951
|
const host = process.argv[idx + 2] ?? "0.0.0.0";
|
|
952
|
-
const
|
|
952
|
+
const profileRaw = process.argv[idx + 3];
|
|
953
|
+
const profile = profileRaw && profileRaw !== "_" && !profileRaw.startsWith("--") ? profileRaw : undefined;
|
|
953
954
|
const share = process.argv.includes("--share");
|
|
954
955
|
|
|
955
956
|
// Set DB profile for supervisor (needed to read config)
|
|
@@ -5,6 +5,10 @@ import type { ChatMessage, ChatEvent } from "../../../types/chat";
|
|
|
5
5
|
import type { SessionPhase } from "../../../types/api";
|
|
6
6
|
import type { BashPartialEntry } from "../../hooks/use-chat";
|
|
7
7
|
import { ToolCard } from "./tool-cards";
|
|
8
|
+
import { extractJsonlPath } from "./pre-compact-button";
|
|
9
|
+
const PreCompactSection = lazy(() =>
|
|
10
|
+
import("./pre-compact-section").then((m) => ({ default: m.PreCompactSection }))
|
|
11
|
+
);
|
|
8
12
|
const MarkdownRenderer = lazy(() =>
|
|
9
13
|
import("@/components/shared/markdown-renderer").then((m) => ({ default: m.MarkdownRenderer }))
|
|
10
14
|
);
|
|
@@ -310,11 +314,11 @@ const SYSTEM_TAG_NAMES = new Set(["task-notification", "environment_details"]);
|
|
|
310
314
|
|
|
311
315
|
/** User message bubble — full width, collapsible, with system tag badges */
|
|
312
316
|
function UserBubble({ content, projectName, onFork }: { content: string; projectName?: string; onFork?: () => void }) {
|
|
313
|
-
const { files, text, tags, command } = useMemo(() => {
|
|
317
|
+
const { files, text, tags, command, jsonlPath } = useMemo(() => {
|
|
314
318
|
const parsed = parseUserAttachments(content);
|
|
315
319
|
const { cleanText: noSysTags, tags } = extractSystemTags(parsed.text);
|
|
316
320
|
const { command, cleanText } = parseCommandTags(noSysTags);
|
|
317
|
-
return { files: parsed.files, text: cleanText, tags, command };
|
|
321
|
+
return { files: parsed.files, text: cleanText, tags, command, jsonlPath: extractJsonlPath(cleanText) };
|
|
318
322
|
}, [content]);
|
|
319
323
|
|
|
320
324
|
const isSystemContext = tags.some((t) => SYSTEM_TAG_NAMES.has(t.name));
|
|
@@ -399,6 +403,23 @@ function UserBubble({ content, projectName, onFork }: { content: string; project
|
|
|
399
403
|
{expanded ? <><ChevronUp className="size-3" />Show less</> : <><ChevronDown className="size-3" />Show more</>}
|
|
400
404
|
</button>
|
|
401
405
|
)}
|
|
406
|
+
{/* Expand compacted conversation: detect JSONL path in compact summary user message */}
|
|
407
|
+
{jsonlPath && (
|
|
408
|
+
<Suspense fallback={<div className="mt-2 animate-pulse h-10 bg-surface/50 rounded" />}>
|
|
409
|
+
<PreCompactSection
|
|
410
|
+
jsonlPath={jsonlPath}
|
|
411
|
+
projectName={projectName}
|
|
412
|
+
renderMessage={(msg, idx) => (
|
|
413
|
+
<MessageBubble
|
|
414
|
+
key={msg.id ?? `pc-${idx}`}
|
|
415
|
+
message={msg}
|
|
416
|
+
isStreaming={false}
|
|
417
|
+
projectName={projectName}
|
|
418
|
+
/>
|
|
419
|
+
)}
|
|
420
|
+
/>
|
|
421
|
+
</Suspense>
|
|
422
|
+
)}
|
|
402
423
|
{/* Fork/Rewind button — only for real user messages */}
|
|
403
424
|
{!isSystemContext && onFork && (
|
|
404
425
|
<button
|
|
@@ -788,9 +809,27 @@ function InterleavedEvents({ events, isStreaming, projectName, bashPartialOutput
|
|
|
788
809
|
}
|
|
789
810
|
if (group.kind === "text") {
|
|
790
811
|
const isLast = isStreaming && i === groups.length - 1;
|
|
812
|
+
const jsonlPath = extractJsonlPath(group.content);
|
|
791
813
|
return (
|
|
792
814
|
<div key={`text-${i}`} className="text-sm text-text-primary select-text">
|
|
793
815
|
<StreamingText content={group.content} animate={isLast} projectName={projectName} />
|
|
816
|
+
{jsonlPath && (
|
|
817
|
+
<Suspense fallback={<div className="mt-2 animate-pulse h-10 bg-surface/50 rounded" />}>
|
|
818
|
+
<PreCompactSection
|
|
819
|
+
jsonlPath={jsonlPath}
|
|
820
|
+
projectName={projectName}
|
|
821
|
+
renderMessage={(msg, idx) => (
|
|
822
|
+
<MessageBubble
|
|
823
|
+
key={msg.id ?? `pc-${idx}`}
|
|
824
|
+
message={msg}
|
|
825
|
+
isStreaming={false}
|
|
826
|
+
projectName={projectName}
|
|
827
|
+
bashPartialOutput={bashPartialOutput}
|
|
828
|
+
/>
|
|
829
|
+
)}
|
|
830
|
+
/>
|
|
831
|
+
</Suspense>
|
|
832
|
+
)}
|
|
794
833
|
</div>
|
|
795
834
|
);
|
|
796
835
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { AlertCircle, ChevronUp, History, Loader2 } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
/** Detects a JSONL transcript path in Claude's compact summary message text. */
|
|
4
|
+
const JSONL_PATH_RE = /read the full transcript at:\s*(\S+\.jsonl)/i;
|
|
5
|
+
|
|
6
|
+
export function extractJsonlPath(text: string): string | null {
|
|
7
|
+
const match = text.match(JSONL_PATH_RE);
|
|
8
|
+
return match?.[1]?.trim() ?? null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type PreCompactStatus = "idle" | "loading" | "loaded" | "error";
|
|
12
|
+
|
|
13
|
+
interface PreCompactButtonProps {
|
|
14
|
+
status: PreCompactStatus;
|
|
15
|
+
onLoad?: () => void;
|
|
16
|
+
count?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Button shown when Claude's compact summary is detected.
|
|
21
|
+
* Clicking triggers the pre-compact-messages fetch. Shows loading/loaded/error states.
|
|
22
|
+
* Responsive: full-width on mobile, inline on desktop. Min 44px touch target.
|
|
23
|
+
*/
|
|
24
|
+
export function PreCompactButton({ status, onLoad, count }: PreCompactButtonProps) {
|
|
25
|
+
const isBusy = status === "loading";
|
|
26
|
+
const isLoaded = status === "loaded";
|
|
27
|
+
const isError = status === "error";
|
|
28
|
+
|
|
29
|
+
const label = isBusy
|
|
30
|
+
? "Loading previous conversation..."
|
|
31
|
+
: isLoaded
|
|
32
|
+
? `Previous conversation loaded${count != null ? ` (${count})` : ""}`
|
|
33
|
+
: isError
|
|
34
|
+
? "Failed to load — retry"
|
|
35
|
+
: "Load previous conversation";
|
|
36
|
+
|
|
37
|
+
const Icon = isBusy ? Loader2 : isLoaded ? ChevronUp : isError ? AlertCircle : History;
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<button
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={onLoad}
|
|
43
|
+
disabled={isBusy || isLoaded}
|
|
44
|
+
className="mt-2 inline-flex items-center justify-center gap-2 rounded-md border border-border bg-surface/50 px-4 py-2.5 text-sm text-text-primary hover:bg-surface transition-colors disabled:opacity-70 disabled:cursor-default w-full md:w-auto min-h-[44px]"
|
|
45
|
+
>
|
|
46
|
+
<Icon className={`size-4 shrink-0 ${isBusy ? "animate-spin" : ""} ${isError ? "text-red-400" : ""}`} />
|
|
47
|
+
<span>{label}</span>
|
|
48
|
+
</button>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { ChevronDown, ChevronRight, History } from "lucide-react";
|
|
3
|
+
import { api, projectUrl } from "@/lib/api-client";
|
|
4
|
+
import type { ChatMessage } from "../../../types/chat";
|
|
5
|
+
import { PreCompactButton, type PreCompactStatus } from "./pre-compact-button";
|
|
6
|
+
|
|
7
|
+
interface PreCompactSectionProps {
|
|
8
|
+
jsonlPath: string;
|
|
9
|
+
projectName?: string;
|
|
10
|
+
/** Renders each loaded pre-compact message. Passed from parent to avoid circular imports. */
|
|
11
|
+
renderMessage: (msg: ChatMessage, idx: number) => React.ReactNode;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Orchestrates the "Load previous conversation" flow:
|
|
16
|
+
* 1. Shows button when idle/loading/error
|
|
17
|
+
* 2. On click: GET /api/project/:name/chat/pre-compact-messages?jsonlPath=...
|
|
18
|
+
* 3. Renders returned messages in a collapsible section
|
|
19
|
+
*/
|
|
20
|
+
export function PreCompactSection({ jsonlPath, projectName, renderMessage }: PreCompactSectionProps) {
|
|
21
|
+
const [status, setStatus] = useState<PreCompactStatus>("idle");
|
|
22
|
+
const [messages, setMessages] = useState<ChatMessage[] | null>(null);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
const [expanded, setExpanded] = useState(false);
|
|
25
|
+
|
|
26
|
+
const handleLoad = useCallback(async () => {
|
|
27
|
+
if (!projectName) { setError("No project context available"); setStatus("error"); return; }
|
|
28
|
+
setStatus("loading");
|
|
29
|
+
setError(null);
|
|
30
|
+
try {
|
|
31
|
+
const path = `${projectUrl(projectName)}/chat/pre-compact-messages?jsonlPath=${encodeURIComponent(jsonlPath)}`;
|
|
32
|
+
const data = await api.get<ChatMessage[]>(path);
|
|
33
|
+
setMessages(data);
|
|
34
|
+
setStatus("loaded");
|
|
35
|
+
setExpanded(true);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
setError(e instanceof Error ? e.message : "Unknown error");
|
|
38
|
+
setStatus("error");
|
|
39
|
+
}
|
|
40
|
+
}, [jsonlPath, projectName]);
|
|
41
|
+
|
|
42
|
+
if (status !== "loaded" || !messages) {
|
|
43
|
+
return (
|
|
44
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
45
|
+
<PreCompactButton status={status} onLoad={status === "loading" ? undefined : handleLoad} />
|
|
46
|
+
{error && <p className="text-xs text-red-400">{error}</p>}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="mt-2 rounded-lg border border-border/50 bg-surface/30 overflow-hidden">
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => setExpanded((v) => !v)}
|
|
56
|
+
className="flex items-center gap-2 w-full px-3 py-2.5 text-sm text-text-secondary hover:bg-surface/50 transition-colors min-h-[44px]"
|
|
57
|
+
>
|
|
58
|
+
{expanded ? <ChevronDown className="size-4" /> : <ChevronRight className="size-4" />}
|
|
59
|
+
<History className="size-4" />
|
|
60
|
+
<span>Previous conversation ({messages.length} messages)</span>
|
|
61
|
+
</button>
|
|
62
|
+
{expanded && (
|
|
63
|
+
<div className="border-t border-border/30 px-2 md:px-3 py-3 space-y-3 max-h-[60vh] overflow-y-auto">
|
|
64
|
+
{messages.map((msg, idx) => renderMessage(msg, idx))}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -38,6 +38,7 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
38
38
|
|
|
39
39
|
const [diffText, setDiffText] = useState<string | null>(null);
|
|
40
40
|
const [fileContents, setFileContents] = useState<{ original: string; modified: string } | null>(null);
|
|
41
|
+
const [fullFileDiff, setFullFileDiff] = useState<{ original: string; modified: string } | null>(null);
|
|
41
42
|
const [loading, setLoading] = useState(!isInline);
|
|
42
43
|
const [error, setError] = useState<string | null>(null);
|
|
43
44
|
const [expandMode, setExpandMode] = useState<"both" | "left" | "right">("both");
|
|
@@ -63,6 +64,9 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
63
64
|
if (!projectName) return;
|
|
64
65
|
setLoading(true);
|
|
65
66
|
setError(null);
|
|
67
|
+
setFullFileDiff(null);
|
|
68
|
+
setFileContents(null);
|
|
69
|
+
setDiffText(null);
|
|
66
70
|
|
|
67
71
|
if (file1 && file2) {
|
|
68
72
|
const params = new URLSearchParams({ file1, file2 });
|
|
@@ -75,12 +79,23 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
75
79
|
return;
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
|
|
82
|
+
// Single-file diff → fetch FULL file contents on both sides (VSCode-style).
|
|
83
|
+
// Monaco DiffEditor computes the diff itself, giving full-file view instead
|
|
84
|
+
// of just the changed hunks + 3 lines of context that `git diff` returns.
|
|
79
85
|
if (filePath) {
|
|
80
86
|
const params = new URLSearchParams({ file: filePath });
|
|
81
87
|
if (ref1) params.set("ref", ref1);
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
api
|
|
89
|
+
.get<{ original: string; modified: string }>(
|
|
90
|
+
`${projectUrl(projectName)}/git/file-full-diff?${params}`,
|
|
91
|
+
)
|
|
92
|
+
.then((data) => { setFullFileDiff(data); setLoading(false); })
|
|
93
|
+
.catch((err) => { setError(err instanceof Error ? err.message : "Failed to load diff"); setLoading(false); });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
let url: string;
|
|
98
|
+
if (ref1 || ref2) {
|
|
84
99
|
const params = new URLSearchParams();
|
|
85
100
|
if (ref1) params.set("ref1", ref1);
|
|
86
101
|
if (ref2) params.set("ref2", ref2);
|
|
@@ -98,9 +113,10 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
98
113
|
const { original, modified } = useMemo(() => {
|
|
99
114
|
if (isInline) return { original: inlineOriginal ?? "", modified: inlineModified ?? "" };
|
|
100
115
|
if (isFileCompare && fileContents) return fileContents;
|
|
116
|
+
if (fullFileDiff) return fullFileDiff;
|
|
101
117
|
if (!diffText) return { original: "", modified: "" };
|
|
102
118
|
return parseDiff(diffText);
|
|
103
|
-
}, [diffText, isInline, inlineOriginal, inlineModified, isFileCompare, fileContents]);
|
|
119
|
+
}, [diffText, isInline, inlineOriginal, inlineModified, isFileCompare, fileContents, fullFileDiff]);
|
|
104
120
|
|
|
105
121
|
const language = useMemo(() => {
|
|
106
122
|
const langFile = filePath ?? file2 ?? file1;
|
|
@@ -135,7 +151,7 @@ export function DiffViewer({ metadata }: DiffViewerProps) {
|
|
|
135
151
|
}
|
|
136
152
|
|
|
137
153
|
// Catch diffs with metadata-only changes (mode, rename) where parseDiff returns empty
|
|
138
|
-
if (!isInline && !isFileCompare && !original && !modified) {
|
|
154
|
+
if (!isInline && !isFileCompare && !fullFileDiff && !original && !modified) {
|
|
139
155
|
return (
|
|
140
156
|
<div className="flex flex-col items-center justify-center h-full gap-2 text-muted-foreground">
|
|
141
157
|
<FileCode className="size-8" />
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{o as e}from"./rolldown-runtime-FhOqtrmT.js";import{b as t,x as n}from"./vendor-markdown-0Mxgxy0L.js";import{D as r,_ as i,b as a,c as o,d as s,f as c,g as l,h as u,m as d,p as f,s as p,u as m,v as h,x as g,y as _}from"./vendor-ui-B-89Uj8i.js";import{t as v}from"./createLucideIcon-BjHrJDVb.js";import{i as y}from"./dist-D7KGU7Vl.js";import{n as b,r as x}from"./plus-51UQ45rf.js";import{t as S}from"./input-Dk49gO8E.js";import{t as C}from"./refresh-cw-LlbZDJpO.js";import{t as w}from"./trash-2-CJYoLw7Q.js";import{i as T,t as E}from"./api-client-Dvzcc_EO.js";import{n as D}from"./utils-CTg5uAYR.js";import{a as O,h as k}from"./api-settings-DAk7D-NP.js";var A=v(`bell-off`,[[`path`,{d:`M10.268 21a2 2 0 0 0 3.464 0`,key:`vwvbt9`}],[`path`,{d:`M17 17H4a1 1 0 0 1-.74-1.673C4.59 13.956 6 12.499 6 8a6 6 0 0 1 .258-1.742`,key:`178tsu`}],[`path`,{d:`m2 2 20 20`,key:`1ooewy`}],[`path`,{d:`M8.668 3.01A6 6 0 0 1 18 8c0 2.687.77 4.653 1.707 6.05`,key:`1hqiys`}]]),j=v(`bot`,[[`path`,{d:`M12 8V4H8`,key:`hb8ula`}],[`rect`,{width:`16`,height:`12`,x:`4`,y:`8`,rx:`2`,key:`enze0r`}],[`path`,{d:`M2 14h2`,key:`vft8re`}],[`path`,{d:`M20 14h2`,key:`4cs60a`}],[`path`,{d:`M15 13v2`,key:`1xurst`}],[`path`,{d:`M9 13v2`,key:`rq6x2g`}]]),M=v(`bug`,[[`path`,{d:`M12 20v-9`,key:`1qisl0`}],[`path`,{d:`M14 7a4 4 0 0 1 4 4v3a6 6 0 0 1-12 0v-3a4 4 0 0 1 4-4z`,key:`uouzyp`}],[`path`,{d:`M14.12 3.88 16 2`,key:`qol33r`}],[`path`,{d:`M21 21a4 4 0 0 0-3.81-4`,key:`1b0z45`}],[`path`,{d:`M21 5a4 4 0 0 1-3.55 3.97`,key:`5cxbf6`}],[`path`,{d:`M22 13h-4`,key:`1jl80f`}],[`path`,{d:`M3 21a4 4 0 0 1 3.81-4`,key:`1fjd4g`}],[`path`,{d:`M3 5a4 4 0 0 0 3.55 3.97`,key:`1d7oge`}],[`path`,{d:`M6 13H2`,key:`82j7cp`}],[`path`,{d:`m8 2 1.88 1.88`,key:`fmnt4t`}],[`path`,{d:`M9 7.13V6a3 3 0 1 1 6 0v1.13`,key:`1vgav8`}]]),N=v(`lock`,[[`rect`,{width:`18`,height:`11`,x:`3`,y:`11`,rx:`2`,ry:`2`,key:`1w4ew1`}],[`path`,{d:`M7 11V7a5 5 0 0 1 10 0v4`,key:`fwvmzm`}]]),P=v(`pencil`,[[`path`,{d:`M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z`,key:`1a8usu`}],[`path`,{d:`m15 5 4 4`,key:`1mk7zo`}]]),F=v(`rotate-ccw`,[[`path`,{d:`M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8`,key:`1357e3`}],[`path`,{d:`M3 3v5h5`,key:`1xhq8a`}]]),I=e(n(),1),L=t();function R({className:e,...t}){return(0,L.jsx)(r,{"data-slot":`label`,className:D(`flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50`,e),...t})}var z=I.forwardRef(({className:e,...t},n)=>(0,L.jsx)(p,{className:D(`peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input`,e),...t,ref:n,children:(0,L.jsx)(o,{className:D(`pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0`)})}));z.displayName=p.displayName;function B({...e}){return(0,L.jsx)(l,{"data-slot":`select`,...e})}function V({...e}){return(0,L.jsx)(a,{"data-slot":`select-value`,...e})}function H({className:e,size:t=`default`,children:n,...r}){return(0,L.jsxs)(_,{"data-slot":`select-trigger`,"data-size":t,className:D(`flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground`,e),...r,children:[n,(0,L.jsx)(s,{asChild:!0,children:(0,L.jsx)(x,{className:`size-4 opacity-50`})})]})}function U({className:e,children:t,position:n=`item-aligned`,align:r=`center`,...i}){return(0,L.jsx)(u,{children:(0,L.jsxs)(m,{"data-slot":`select-content`,className:D(`relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95`,n===`popper`&&`data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1`,e),position:n,align:r,...i,children:[(0,L.jsx)(G,{}),(0,L.jsx)(g,{className:D(`p-1`,n===`popper`&&`h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1`),children:t}),(0,L.jsx)(K,{})]})})}function W({className:e,children:t,...n}){return(0,L.jsxs)(c,{"data-slot":`select-item`,className:D(`relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2`,e),...n,children:[(0,L.jsx)(`span`,{"data-slot":`select-item-indicator`,className:`absolute right-2 flex size-3.5 items-center justify-center`,children:(0,L.jsx)(f,{children:(0,L.jsx)(y,{className:`size-4`})})}),(0,L.jsx)(d,{children:t})]})}function G({className:e,...t}){return(0,L.jsx)(h,{"data-slot":`select-scroll-up-button`,className:D(`flex cursor-default items-center justify-center py-1`,e),...t,children:(0,L.jsx)(b,{className:`size-4`})})}function K({className:e,...t}){return(0,L.jsx)(i,{"data-slot":`select-scroll-down-button`,className:D(`flex cursor-default items-center justify-center py-1`,e),...t,children:(0,L.jsx)(x,{className:`size-4`})})}var q={claude:`C`,cursor:`▶`,codex:`◆`,gemini:`G`};function J({value:e,onChange:t,projectName:n}){let[r,i]=(0,I.useState)([]),[a,o]=(0,I.useState)(!1),s=(0,I.useRef)(null),c=(0,I.useRef)(0);(0,I.useEffect)(()=>{n&&E.get(`${T(n)}/chat/providers`).then(i).catch(()=>{})},[n]),(0,I.useEffect)(()=>{if(!a)return;let e=e=>{s.current&&!s.current.contains(e.target)&&o(!1)};return document.addEventListener(`mousedown`,e),()=>document.removeEventListener(`mousedown`,e)},[a]),(0,I.useEffect)(()=>{a&&(c.current=Math.max(0,r.findIndex(t=>t.id===e)))},[a,e,r]);let l=(0,I.useCallback)(e=>{if(e.key===`Escape`){o(!1);return}if(e.key===`ArrowDown`||e.key===`ArrowUp`){e.preventDefault();let t=e.key===`ArrowDown`?1:-1;c.current=(c.current+t+r.length)%r.length,(s.current?.querySelector(`[data-idx="${c.current}"]`))?.focus()}if(e.key===`Enter`){e.preventDefault();let n=r[c.current];n&&(t(n.id),o(!1))}},[t,r]);if(r.length<=1)return null;let u=r.find(t=>t.id===e),d=q[e]||`?`;return(0,L.jsxs)(`div`,{className:`relative`,children:[(0,L.jsxs)(`button`,{type:`button`,onClick:e=>{e.stopPropagation(),o(e=>!e)},className:`inline-flex items-center gap-1 px-2 py-1 rounded-md text-[11px] text-text-subtle hover:text-text-primary hover:bg-surface-elevated transition-colors border border-transparent hover:border-border`,"aria-label":`AI Provider: ${u?.name??e}`,children:[(0,L.jsx)(`span`,{className:`inline-flex h-3.5 w-3.5 items-center justify-center rounded text-[9px] font-bold bg-surface-elevated shrink-0`,children:d}),(0,L.jsx)(`span`,{className:`max-w-[80px] truncate capitalize`,children:u?.name??e})]}),a&&(0,L.jsxs)(`div`,{ref:s,role:`listbox`,"aria-label":`AI Providers`,onKeyDown:l,onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation(),className:`absolute bottom-full left-0 mb-1 z-50 w-56 rounded-lg border border-border bg-surface shadow-lg`,children:[(0,L.jsx)(`div`,{className:`px-3 py-2 border-b border-border`,children:(0,L.jsx)(`span`,{className:`text-xs font-medium text-text-secondary`,children:`Provider`})}),(0,L.jsx)(`div`,{className:`py-1`,children:r.map((n,r)=>{let i=q[n.id]||`?`,a=n.id===e;return(0,L.jsxs)(`button`,{"data-idx":r,role:`option`,"aria-selected":a,tabIndex:0,onClick:()=>{t(n.id),o(!1)},className:`w-full flex items-center gap-3 px-3 py-2 text-left transition-colors hover:bg-surface-elevated focus:bg-surface-elevated focus:outline-none ${a?`bg-surface-elevated`:``}`,children:[(0,L.jsx)(`span`,{className:`inline-flex h-5 w-5 items-center justify-center rounded text-[11px] font-bold bg-surface-elevated text-text-subtle shrink-0`,children:i}),(0,L.jsx)(`span`,{className:`flex-1 text-sm font-medium text-text-primary capitalize`,children:n.name}),a&&(0,L.jsx)(y,{className:`size-4 shrink-0 text-primary`})]},n.id)})})]})]})}function Y({providerId:e}){return(0,L.jsx)(`span`,{className:`inline-flex h-4 w-4 items-center justify-center rounded text-[10px] font-bold bg-surface-elevated text-text-subtle shrink-0`,title:e,children:q[e]||`?`})}var X=[{value:`low`,label:`Low`},{value:`medium`,label:`Medium`},{value:`high`,label:`High`}],Z=[{value:`bypassPermissions`,label:`Bypass permissions (default)`},{value:`default`,label:`Ask before edits`},{value:`acceptEdits`,label:`Edit automatically`},{value:`plan`,label:`Plan mode`}],Q={claude:`Claude`,cursor:`Cursor`,codex:`Codex`,gemini:`Gemini`};function $({compact:e}={}){let[t,n]=(0,I.useState)(null),[r,i]=(0,I.useState)(``),[a,o]=(0,I.useState)([]),[s,c]=(0,I.useState)(!1),[l,u]=(0,I.useState)(!1),[d,f]=(0,I.useState)(null),[p,m]=(0,I.useState)(0);(0,I.useEffect)(()=>{O().then(e=>{n(e),i(e.default_provider??`claude`)}).catch(e=>f(e.message))},[]),(0,I.useEffect)(()=>{r&&(c(!0),E.get(`/api/settings/ai/providers/${r}/models`).then(o).catch(()=>o([])).finally(()=>c(!1)))},[r]);let h=t?Object.keys(t.providers).filter(e=>e!==`mock`).map(e=>({id:e,name:Q[e]??e})):[],g=t?.providers[r],_=g?.type===`agent-sdk`||!g?.type&&r===`claude`,v=async(e,i)=>{if(t){u(!0),f(null);try{n(await k({providers:{[r]:{[e]:i}}})),m(e=>e+1)}catch(e){f(e.message)}finally{u(!1)}}},y=e?`text-[11px]`:`text-sm`,b=e?`text-xs`:`text-sm`,x=e?`space-y-2`:`space-y-4`,C=e?`space-y-1.5`:`space-y-3`,w=e?`space-y-1`:`space-y-1.5`;if(!t)return(0,L.jsxs)(`div`,{className:C,children:[(0,L.jsx)(`h3`,{className:`${b} font-medium text-text-secondary`,children:`AI Settings`}),(0,L.jsx)(`p`,{className:`${y} text-text-subtle`,children:d?`Error: ${d}`:`Loading...`})]});let T=_?a:[{value:`__default__`,label:`Auto (default)`},...a];return(0,L.jsxs)(`div`,{className:x,children:[(0,L.jsx)(`h3`,{className:`${b} font-medium text-text-secondary`,children:`AI Settings`}),h.length>1&&(0,L.jsx)(`div`,{className:`flex gap-0.5 border-b border-border/50 -mx-1 px-1`,children:h.map(e=>(0,L.jsxs)(`button`,{onClick:()=>i(e.id),className:`flex items-center gap-1 px-2 py-1 text-[11px] rounded-t transition-colors ${r===e.id?`text-primary border-b-2 border-primary font-medium`:`text-text-subtle hover:text-text-secondary`}`,children:[(0,L.jsx)(Y,{providerId:e.id}),(0,L.jsx)(`span`,{className:`capitalize`,children:e.name})]},e.id))}),(0,L.jsxs)(`div`,{className:C,children:[a.length>0&&(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-model`,className:e?y:void 0,children:`Model`}),(0,L.jsxs)(B,{value:_?g?.model??a[0]?.value:g?.model||`__default__`,onValueChange:e=>v(`model`,e===`__default__`?void 0:e),disabled:s,children:[(0,L.jsx)(H,{id:`ai-model`,className:`w-full ${e?`h-7 text-[11px]`:``}`,children:(0,L.jsx)(V,{placeholder:s?`Loading models...`:`Select model`})}),(0,L.jsx)(U,{className:`max-h-[300px]`,children:T.map(e=>(0,L.jsx)(W,{value:e.value,children:e.label},e.value))})]})]}),_&&(0,L.jsxs)(L.Fragment,{children:[(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-base-url`,className:e?y:void 0,children:`Base URL`}),(0,L.jsx)(S,{id:`ai-base-url`,type:`url`,defaultValue:g?.base_url??``,placeholder:`https://api.anthropic.com (default)`,className:e?`h-7 text-[11px]`:void 0,onBlur:e=>{v(`base_url`,e.target.value.trim()||void 0)}},`baseurl-${r}-${p}`)]}),(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-api-key`,className:e?y:void 0,children:`API Key / Token`}),(0,L.jsx)(S,{id:`ai-api-key`,type:`password`,defaultValue:g?.api_key??``,placeholder:`sk-ant-... (optional, overrides accounts)`,className:e?`h-7 text-[11px] font-mono`:`font-mono`,onBlur:e=>{let t=e.target.value.trim();t.startsWith(`••••`)||v(`api_key`,t||void 0)}},`apikey-${r}-${p}`),(0,L.jsx)(`p`,{className:`${e?`text-[9px]`:`text-[11px]`} text-muted-foreground`,children:`Direct API key or OAuth token. Leave empty to use connected accounts.`})]}),(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-effort`,className:e?y:void 0,children:`Effort`}),(0,L.jsxs)(B,{value:g?.effort??`high`,onValueChange:e=>v(`effort`,e),children:[(0,L.jsx)(H,{id:`ai-effort`,className:`w-full ${e?`h-7 text-[11px]`:``}`,children:(0,L.jsx)(V,{})}),(0,L.jsx)(U,{children:X.map(e=>(0,L.jsx)(W,{value:e.value,children:e.label},e.value))})]})]}),(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-max-turns`,className:e?y:void 0,children:`Max Turns (1-500)`}),(0,L.jsx)(S,{id:`ai-max-turns`,type:`number`,min:1,max:500,defaultValue:g?.max_turns??100,className:e?`h-7 text-[11px]`:void 0,onBlur:e=>{let t=parseInt(e.target.value);isNaN(t)||v(`max_turns`,t)}},`turns-${r}-${p}`)]}),(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-budget`,className:e?y:void 0,children:`Max Budget (USD)`}),(0,L.jsx)(S,{id:`ai-budget`,type:`number`,step:.1,min:.01,max:50,defaultValue:g?.max_budget_usd??``,placeholder:`No limit`,className:e?`h-7 text-[11px]`:void 0,onBlur:e=>{let t=parseFloat(e.target.value);v(`max_budget_usd`,isNaN(t)?void 0:t)}},`budget-${r}-${p}`)]}),(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-thinking`,className:e?y:void 0,children:`Thinking Budget (tokens)`}),(0,L.jsx)(S,{id:`ai-thinking`,type:`number`,min:0,defaultValue:g?.thinking_budget_tokens??``,placeholder:`Disabled`,className:e?`h-7 text-[11px]`:void 0,onBlur:e=>{let t=parseInt(e.target.value);v(`thinking_budget_tokens`,isNaN(t)?void 0:t)}},`thinking-${r}-${p}`)]}),(0,L.jsxs)(`div`,{className:`flex items-center justify-between gap-2`,children:[(0,L.jsxs)(`div`,{children:[(0,L.jsx)(R,{htmlFor:`ai-agent-teams`,className:e?y:void 0,children:`Agent Teams`}),(0,L.jsx)(`p`,{className:`${e?`text-[9px]`:`text-[11px]`} text-muted-foreground`,children:`Experimental. Enables multi-agent collaboration with shared tasks and messaging. Uses ~7x more tokens.`})]}),(0,L.jsx)(z,{id:`ai-agent-teams`,checked:g?.agent_teams??!1,onCheckedChange:e=>v(`agent_teams`,e)})]}),g?.agent_teams&&(0,L.jsx)(ee,{compact:e})]}),(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-permission-mode`,className:e?y:void 0,children:`Default Permission Mode`}),(0,L.jsxs)(B,{value:g?.permission_mode??`bypassPermissions`,onValueChange:e=>v(`permission_mode`,e),children:[(0,L.jsx)(H,{id:`ai-permission-mode`,className:`w-full ${e?`h-7 text-[11px]`:``}`,children:(0,L.jsx)(V,{})}),(0,L.jsx)(U,{children:Z.map(e=>(0,L.jsx)(W,{value:e.value,children:e.label},e.value))})]})]}),(0,L.jsxs)(`div`,{className:w,children:[(0,L.jsx)(R,{htmlFor:`ai-system-prompt`,className:e?y:void 0,children:`Additional Instructions`}),(0,L.jsx)(`textarea`,{id:`ai-system-prompt`,rows:e?3:4,defaultValue:g?.system_prompt??``,placeholder:`Enter additional instructions for ${r}...`,className:`w-full rounded-md border border-input bg-background px-3 py-2 ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 ${e?`text-[11px]`:`text-sm`}`,onBlur:e=>{v(`system_prompt`,e.target.value.trim()||void 0)}},`sysprompt-${r}-${p}`)]})]}),l&&(0,L.jsx)(`p`,{className:`text-xs text-text-subtle`,children:`Saving...`}),d&&(0,L.jsx)(`p`,{className:`text-xs text-red-500`,children:d})]})}function ee({compact:e}){let[t,n]=(0,I.useState)([]),[r,i]=(0,I.useState)(!1),[a,o]=(0,I.useState)(null),s=(0,I.useCallback)(async()=>{i(!0);try{n(await E.get(`/api/teams`)??[])}catch{}i(!1)},[]);(0,I.useEffect)(()=>{s()},[s]);let c=async e=>{try{await E.del(`/api/teams/${encodeURIComponent(e)}`),n(t=>t.filter(t=>t.name!==e)),o(null)}catch{}};return t.length===0&&!r?null:(0,L.jsxs)(`div`,{className:`space-y-1.5`,children:[(0,L.jsxs)(`div`,{className:`flex items-center justify-between`,children:[(0,L.jsxs)(R,{className:e?`text-[11px]`:void 0,children:[`Teams (`,t.length,`)`]}),(0,L.jsx)(`button`,{onClick:s,className:`text-text-subtle hover:text-foreground p-1`,"aria-label":`Refresh teams`,children:(0,L.jsx)(C,{className:`size-3 ${r?`animate-spin`:``}`})})]}),t.map(e=>(0,L.jsxs)(`div`,{className:`flex items-center justify-between p-2 rounded bg-surface-elevated text-xs`,children:[(0,L.jsxs)(`div`,{className:`min-w-0`,children:[(0,L.jsx)(`div`,{className:`font-medium truncate`,children:e.name}),e.description&&(0,L.jsx)(`div`,{className:`text-text-subtle truncate`,children:e.description}),(0,L.jsxs)(`div`,{className:`text-text-subtle`,children:[e.members?.length??e.memberCount??0,` members`,e.createdAt?` · ${new Date(e.createdAt).toLocaleDateString()}`:``]})]}),a===e.name?(0,L.jsxs)(`div`,{className:`flex gap-1 shrink-0 ml-2`,children:[(0,L.jsx)(`button`,{onClick:()=>c(e.name),className:`px-2 py-1 bg-red-600 text-white rounded text-[10px]`,children:`Delete`}),(0,L.jsx)(`button`,{onClick:()=>o(null),className:`px-2 py-1 bg-zinc-600 text-white rounded text-[10px]`,children:`Cancel`})]}):(0,L.jsx)(`button`,{onClick:()=>o(e.name),className:`shrink-0 text-text-subtle hover:text-red-500 p-1 ml-2`,"aria-label":`Delete team ${e.name}`,children:(0,L.jsx)(w,{className:`size-3.5`})})]},e.name))]})}export{U as a,V as c,F as d,P as f,A as g,j as h,B as i,z as l,M as m,Y as n,W as o,N as p,J as r,H as s,$ as t,R as u};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{W as e}from"./vendor-mermaid-Dx86tuVP.js";export{e as createArchitectureServices};
|