@hienlh/ppm 0.8.90 → 0.8.91
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 +14 -0
- package/dist/web/assets/{browser-tab-SHBc1OCK.js → browser-tab-Bt91e0v_.js} +1 -1
- package/dist/web/assets/chat-tab-BY1ovPns.js +8 -0
- package/dist/web/assets/{code-editor-BomcTYQ4.js → code-editor-CAHcH0N-.js} +1 -1
- package/dist/web/assets/{database-viewer-B47ck-1v.js → database-viewer-DzEoA-r6.js} +1 -1
- package/dist/web/assets/{diff-viewer-Dw2v2RU2.js → diff-viewer-Co7JUnvw.js} +1 -1
- package/dist/web/assets/{git-graph-Co7fcau-.js → git-graph-B139k04F.js} +1 -1
- package/dist/web/assets/{index-CQu4iIvy.js → index-CXJneRo7.js} +10 -10
- package/dist/web/assets/keybindings-store-C8WA_lZu.js +1 -0
- package/dist/web/assets/{markdown-renderer-C0n-Ucfa.js → markdown-renderer-C6phS0NU.js} +1 -1
- package/dist/web/assets/{postgres-viewer-D4vyH--N.js → postgres-viewer-Bb1N6-J2.js} +1 -1
- package/dist/web/assets/{settings-tab-CisRYqMl.js → settings-tab-CZp_PyJ9.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-D3t4nApY.js → sqlite-viewer-Bzgj_M05.js} +1 -1
- package/dist/web/assets/{terminal-tab-DFz3Bd_N.js → terminal-tab-Bi4qWzTP.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-Dopv6S2i.js → use-monaco-theme-D7s2hmIL.js} +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/providers/claude-agent-sdk.ts +59 -1
- package/src/providers/cli-provider-base.ts +6 -0
- package/src/server/routes/chat.ts +31 -10
- package/src/server/routes/settings.ts +27 -0
- package/src/server/ws/chat.ts +7 -1
- package/src/services/cloud-ws.service.ts +1 -0
- package/src/services/cloud.service.ts +1 -0
- package/src/services/db.service.ts +8 -0
- package/src/services/supervisor.ts +3 -0
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +2 -0
- package/src/web/components/chat/chat-history-bar.tsx +21 -7
- package/src/web/components/chat/chat-tab.tsx +4 -1
- package/src/web/components/chat/message-list.tsx +2 -2
- package/src/web/components/chat/session-picker.tsx +1 -0
- package/src/web/components/settings/change-password-section.tsx +128 -0
- package/src/web/components/settings/settings-tab.tsx +4 -0
- package/src/web/hooks/use-chat.ts +17 -0
- package/dist/web/assets/chat-tab-dssvQaJN.js +0 -8
- package/dist/web/assets/keybindings-store-OkhvRBpn.js +0 -1
|
@@ -8,7 +8,7 @@ import { renameSession as sdkRenameSession } from "@anthropic-ai/claude-agent-sd
|
|
|
8
8
|
import { listSlashItems } from "../../services/slash-items.service.ts";
|
|
9
9
|
import { getCachedUsage, refreshUsageNow } from "../../services/claude-usage.service.ts";
|
|
10
10
|
import { getSessionLog } from "../../services/session-log.service.ts";
|
|
11
|
-
import { getSessionMapping, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession } from "../../services/db.service.ts";
|
|
11
|
+
import { getSessionMapping, setSessionMapping, setSessionTitle, getPinnedSessionIds, pinSession, unpinSession, deleteSessionMapping, deleteSessionTitle } from "../../services/db.service.ts";
|
|
12
12
|
import { ok, err } from "../../types/api.ts";
|
|
13
13
|
|
|
14
14
|
type Env = { Variables: { projectPath: string; projectName: string } };
|
|
@@ -125,7 +125,13 @@ chatRoutes.delete("/sessions/:id", async (c) => {
|
|
|
125
125
|
try {
|
|
126
126
|
const id = c.req.param("id");
|
|
127
127
|
const providerId = c.req.query("providerId") ?? "claude";
|
|
128
|
+
const sdkId = getSessionMapping(id) ?? id;
|
|
129
|
+
// Provider-specific cleanup (JSONL, process, etc.)
|
|
128
130
|
await chatService.deleteSession(providerId, id);
|
|
131
|
+
// Shared DB cleanup
|
|
132
|
+
deleteSessionMapping(id);
|
|
133
|
+
deleteSessionTitle(sdkId);
|
|
134
|
+
unpinSession(sdkId);
|
|
129
135
|
return c.json(ok({ deleted: id }));
|
|
130
136
|
} catch (e) {
|
|
131
137
|
return c.json(err((e as Error).message), 404);
|
|
@@ -184,16 +190,31 @@ chatRoutes.post("/sessions/:id/fork", async (c) => {
|
|
|
184
190
|
const projectName = c.get("projectName");
|
|
185
191
|
const projectPath = c.get("projectPath");
|
|
186
192
|
const providerId = c.req.query("providerId") ?? "claude";
|
|
187
|
-
|
|
188
|
-
const session = await chatService.createSession(providerId, {
|
|
189
|
-
projectName,
|
|
190
|
-
projectPath,
|
|
191
|
-
title: "Forked Chat",
|
|
192
|
-
});
|
|
193
|
-
// Store fork source so WS handler knows to use forkSession on first message
|
|
193
|
+
const body = await c.req.json<{ messageId?: string }>().catch(() => ({} as { messageId?: string }));
|
|
194
194
|
const provider = providerRegistry.get(providerId);
|
|
195
|
-
provider
|
|
196
|
-
|
|
195
|
+
if (!provider) return c.json(err("Provider not found"), 404);
|
|
196
|
+
|
|
197
|
+
if (body.messageId && provider.forkAtMessage) {
|
|
198
|
+
// Mid-fork: SDK fork first, then create PPM session only on success
|
|
199
|
+
const result = await provider.forkAtMessage(sourceId, body.messageId, {
|
|
200
|
+
title: "Forked Chat", dir: projectPath,
|
|
201
|
+
});
|
|
202
|
+
const session = await chatService.createSession(providerId, {
|
|
203
|
+
projectName, projectPath, title: "Forked Chat",
|
|
204
|
+
});
|
|
205
|
+
setSessionMapping(session.id, result.sessionId);
|
|
206
|
+
provider.markAsResumed?.(session.id);
|
|
207
|
+
return c.json(ok({ ...session, forkedFrom: sourceId }), 201);
|
|
208
|
+
} else if (provider.setForkSource) {
|
|
209
|
+
// Deferred fork from end (full history copy on first msg)
|
|
210
|
+
const session = await chatService.createSession(providerId, {
|
|
211
|
+
projectName, projectPath, title: "Forked Chat",
|
|
212
|
+
});
|
|
213
|
+
provider.setForkSource(session.id, sourceId);
|
|
214
|
+
return c.json(ok({ ...session, forkedFrom: sourceId }), 201);
|
|
215
|
+
} else {
|
|
216
|
+
return c.json(err("Provider does not support forking"), 400);
|
|
217
|
+
}
|
|
197
218
|
} catch (e) {
|
|
198
219
|
return c.json(err((e as Error).message), 500);
|
|
199
220
|
}
|
|
@@ -252,6 +252,33 @@ settingsRoutes.post("/telegram/test", async (c) => {
|
|
|
252
252
|
}
|
|
253
253
|
});
|
|
254
254
|
|
|
255
|
+
// ── Auth / Password ──────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
/** PUT /settings/auth/password — change the access password (token) */
|
|
258
|
+
settingsRoutes.put("/auth/password", async (c) => {
|
|
259
|
+
try {
|
|
260
|
+
const { password, confirm } = await c.req.json<{ password: string; confirm: string }>();
|
|
261
|
+
if (typeof password !== "string" || !password.trim()) {
|
|
262
|
+
return c.json(err("Password is required"), 400);
|
|
263
|
+
}
|
|
264
|
+
if (password !== confirm) {
|
|
265
|
+
return c.json(err("Passwords do not match"), 400);
|
|
266
|
+
}
|
|
267
|
+
const trimmed = password.trim();
|
|
268
|
+
if (trimmed.length < 4) {
|
|
269
|
+
return c.json(err("Password must be at least 4 characters"), 400);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const auth = configService.get("auth");
|
|
273
|
+
configService.set("auth", { ...auth, token: trimmed });
|
|
274
|
+
configService.save();
|
|
275
|
+
|
|
276
|
+
return c.json(ok({ token: trimmed }));
|
|
277
|
+
} catch (e) {
|
|
278
|
+
return c.json(err((e as Error).message), 400);
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
255
282
|
// ── Proxy ────────────────────────────────────────────────────────────
|
|
256
283
|
|
|
257
284
|
/** GET /settings/proxy — proxy status */
|
package/src/server/ws/chat.ts
CHANGED
|
@@ -218,8 +218,14 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
218
218
|
continue;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// System events → transition connecting → thinking
|
|
221
|
+
// System events → transition connecting → thinking, forward compact events
|
|
222
222
|
if (evType === "system") {
|
|
223
|
+
const sub = (ev as any).subtype;
|
|
224
|
+
if (sub === "compacting") {
|
|
225
|
+
broadcast(sessionId, { type: "compact_status", status: "compacting" });
|
|
226
|
+
} else if (sub === "compact_done") {
|
|
227
|
+
broadcast(sessionId, { type: "compact_status", status: "done" });
|
|
228
|
+
}
|
|
223
229
|
if (!firstEventReceived) {
|
|
224
230
|
if (heartbeat) clearInterval(heartbeat);
|
|
225
231
|
setPhase(sessionId, "thinking");
|
|
@@ -424,6 +424,14 @@ export function getPinnedSessionIds(): Set<string> {
|
|
|
424
424
|
return new Set(rows.map((r) => r.session_id));
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
export function deleteSessionMapping(ppmId: string): void {
|
|
428
|
+
getDb().query("DELETE FROM session_map WHERE ppm_id = ?").run(ppmId);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function deleteSessionTitle(sessionId: string): void {
|
|
432
|
+
getDb().query("DELETE FROM session_titles WHERE session_id = ?").run(sessionId);
|
|
433
|
+
}
|
|
434
|
+
|
|
427
435
|
// ---------------------------------------------------------------------------
|
|
428
436
|
// Push subscription helpers
|
|
429
437
|
// ---------------------------------------------------------------------------
|
|
@@ -506,6 +506,8 @@ async function connectCloud(opts: { port: number }, serverArgs: string[], logFd:
|
|
|
506
506
|
secretKey: device.secret_key,
|
|
507
507
|
heartbeatFn: () => {
|
|
508
508
|
const status = readStatus();
|
|
509
|
+
// Re-read device file each heartbeat to pick up name changes
|
|
510
|
+
const currentDevice = getCloudDevice();
|
|
509
511
|
return {
|
|
510
512
|
type: "heartbeat" as const,
|
|
511
513
|
tunnelUrl,
|
|
@@ -515,6 +517,7 @@ async function connectCloud(opts: { port: number }, serverArgs: string[], logFd:
|
|
|
515
517
|
availableVersion: (status.availableVersion as string) || null,
|
|
516
518
|
serverPid: serverChild?.pid ?? null,
|
|
517
519
|
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
520
|
+
deviceName: currentDevice?.name ?? device.name,
|
|
518
521
|
timestamp: new Date().toISOString(),
|
|
519
522
|
};
|
|
520
523
|
},
|
package/src/types/api.ts
CHANGED
|
@@ -44,4 +44,5 @@ export type ChatWsServerMessage =
|
|
|
44
44
|
| { type: "session_state"; sessionId: string; phase: SessionPhase; pendingApproval: { requestId: string; tool: string; input: unknown } | null; sessionTitle: string | null }
|
|
45
45
|
| { type: "turn_events"; events: unknown[] }
|
|
46
46
|
| { type: "title_updated"; title: string }
|
|
47
|
+
| { type: "compact_status"; status: "compacting" | "done" }
|
|
47
48
|
| { type: "ping" };
|
package/src/types/chat.ts
CHANGED
|
@@ -29,6 +29,8 @@ export interface AIProvider {
|
|
|
29
29
|
listSessionsByDir?(dir: string): Promise<SessionInfo[]>;
|
|
30
30
|
ensureProjectPath?(sessionId: string, path: string): void;
|
|
31
31
|
setForkSource?(sessionId: string, sourceSessionId: string): void;
|
|
32
|
+
forkAtMessage?(sessionId: string, messageId: string, opts?: { title?: string; dir?: string }): Promise<{ sessionId: string }>;
|
|
33
|
+
markAsResumed?(sessionId: string): void;
|
|
32
34
|
isAvailable?(): Promise<boolean>;
|
|
33
35
|
listModels?(): Promise<ModelOption[]>;
|
|
34
36
|
}
|
|
@@ -16,6 +16,7 @@ interface ChatHistoryBarProps {
|
|
|
16
16
|
projectName: string;
|
|
17
17
|
usageInfo: UsageInfo;
|
|
18
18
|
contextWindowPct?: number | null;
|
|
19
|
+
compactStatus?: "compacting" | null;
|
|
19
20
|
usageLoading?: boolean;
|
|
20
21
|
refreshUsage?: () => void;
|
|
21
22
|
lastFetchedAt?: string | null;
|
|
@@ -79,7 +80,7 @@ function DebugCopyButton({ sessionId, projectName }: { sessionId: string; projec
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
export function ChatHistoryBar({
|
|
82
|
-
projectName, usageInfo, contextWindowPct, usageLoading, refreshUsage, lastFetchedAt,
|
|
83
|
+
projectName, usageInfo, contextWindowPct, compactStatus, usageLoading, refreshUsage, lastFetchedAt,
|
|
83
84
|
sessionId, providerId, onSelectSession, onBugReport, isConnected, onReconnect,
|
|
84
85
|
}: ChatHistoryBarProps) {
|
|
85
86
|
const [activePanel, setActivePanel] = useState<PanelType>(null);
|
|
@@ -240,14 +241,27 @@ export function ChatHistoryBar({
|
|
|
240
241
|
<span className={pctColor(contextWindowPct)}>Ctx:{contextWindowPct}%</span>
|
|
241
242
|
</>
|
|
242
243
|
)}
|
|
244
|
+
{compactStatus === "compacting" && (
|
|
245
|
+
<>
|
|
246
|
+
<span className="text-text-subtle">·</span>
|
|
247
|
+
<span className="text-blue-400 animate-pulse">compacting...</span>
|
|
248
|
+
</>
|
|
249
|
+
)}
|
|
243
250
|
</button>
|
|
244
251
|
) : (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
252
|
+
<>
|
|
253
|
+
{contextWindowPct != null && (
|
|
254
|
+
<span className={`flex items-center gap-1 px-1.5 py-0.5 text-[11px] font-medium tabular-nums ${pctColor(contextWindowPct)}`}>
|
|
255
|
+
<Activity className="size-3" />
|
|
256
|
+
<span>Ctx:{contextWindowPct}%</span>
|
|
257
|
+
</span>
|
|
258
|
+
)}
|
|
259
|
+
{compactStatus === "compacting" && (
|
|
260
|
+
<span className="text-[11px] px-1.5 py-0.5 text-blue-400 animate-pulse">
|
|
261
|
+
compacting...
|
|
262
|
+
</span>
|
|
263
|
+
)}
|
|
264
|
+
</>
|
|
251
265
|
)}
|
|
252
266
|
|
|
253
267
|
{/* Spacer */}
|
|
@@ -89,6 +89,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
89
89
|
connectingElapsed,
|
|
90
90
|
pendingApproval,
|
|
91
91
|
contextWindowPct,
|
|
92
|
+
compactStatus,
|
|
92
93
|
sessionTitle,
|
|
93
94
|
migratedSessionId,
|
|
94
95
|
sendMessage,
|
|
@@ -162,12 +163,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
162
163
|
}, [tabId, updateTab]);
|
|
163
164
|
|
|
164
165
|
/** Fork current session and open new tab with the forked session, resending userMessage */
|
|
165
|
-
const handleFork = useCallback(async (userMessage: string) => {
|
|
166
|
+
const handleFork = useCallback(async (userMessage: string, messageId?: string) => {
|
|
166
167
|
if (!sessionId || !projectName) return;
|
|
167
168
|
try {
|
|
168
169
|
const { api, projectUrl } = await import("@/lib/api-client");
|
|
169
170
|
const forked = await api.post<{ id: string; forkedFrom: string }>(
|
|
170
171
|
`${projectUrl(projectName)}/chat/sessions/${sessionId}/fork?providerId=${providerId}`,
|
|
172
|
+
{ messageId },
|
|
171
173
|
);
|
|
172
174
|
// Open new chat tab with forked session — it will send userMessage on connect
|
|
173
175
|
useTabStore.getState().openTab({
|
|
@@ -350,6 +352,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
350
352
|
projectName={projectName}
|
|
351
353
|
usageInfo={usageInfo}
|
|
352
354
|
contextWindowPct={contextWindowPct}
|
|
355
|
+
compactStatus={compactStatus}
|
|
353
356
|
usageLoading={usageLoading}
|
|
354
357
|
refreshUsage={refreshUsage}
|
|
355
358
|
lastFetchedAt={lastFetchedAt}
|
|
@@ -43,7 +43,7 @@ interface MessageListProps {
|
|
|
43
43
|
connectingElapsed?: number;
|
|
44
44
|
projectName?: string;
|
|
45
45
|
/** Called when user clicks Fork/Rewind — opens new forked chat tab */
|
|
46
|
-
onFork?: (userMessage: string) => void;
|
|
46
|
+
onFork?: (userMessage: string, messageId?: string) => void;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
49
|
export function MessageList({
|
|
@@ -96,7 +96,7 @@ export function MessageList({
|
|
|
96
96
|
message={msg}
|
|
97
97
|
isStreaming={isStreaming && msg.id.startsWith("streaming-")}
|
|
98
98
|
projectName={projectName}
|
|
99
|
-
onFork={msg.role === "user" && onFork ? () => onFork(msg.content) : undefined}
|
|
99
|
+
onFork={msg.role === "user" && onFork ? () => onFork(msg.content, msg.id) : undefined}
|
|
100
100
|
/>
|
|
101
101
|
))}
|
|
102
102
|
|
|
@@ -47,6 +47,7 @@ export function SessionPicker({
|
|
|
47
47
|
|
|
48
48
|
const handleDelete = async (e: React.MouseEvent, session: SessionInfo) => {
|
|
49
49
|
e.stopPropagation();
|
|
50
|
+
if (!window.confirm("Delete this session? This cannot be undone.")) return;
|
|
50
51
|
try {
|
|
51
52
|
if (!projectName) return;
|
|
52
53
|
await api.del(
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { useState, useCallback } from "react";
|
|
2
|
+
import { KeyRound, Check, Eye, EyeOff } from "lucide-react";
|
|
3
|
+
import { Button } from "@/components/ui/button";
|
|
4
|
+
import { Input } from "@/components/ui/input";
|
|
5
|
+
import { api } from "@/lib/api-client";
|
|
6
|
+
import { setAuthToken } from "@/lib/api-client";
|
|
7
|
+
|
|
8
|
+
export function ChangePasswordSection() {
|
|
9
|
+
const [open, setOpen] = useState(false);
|
|
10
|
+
const [password, setPassword] = useState("");
|
|
11
|
+
const [confirm, setConfirm] = useState("");
|
|
12
|
+
const [showPw, setShowPw] = useState(false);
|
|
13
|
+
const [saving, setSaving] = useState(false);
|
|
14
|
+
const [saved, setSaved] = useState(false);
|
|
15
|
+
const [error, setError] = useState<string | null>(null);
|
|
16
|
+
|
|
17
|
+
const mismatch = confirm.length > 0 && password !== confirm;
|
|
18
|
+
const canSubmit = password.trim().length >= 4 && password === confirm && !saving;
|
|
19
|
+
|
|
20
|
+
const handleSubmit = useCallback(async () => {
|
|
21
|
+
if (!canSubmit) return;
|
|
22
|
+
setSaving(true);
|
|
23
|
+
setError(null);
|
|
24
|
+
try {
|
|
25
|
+
const { token } = await api.put<{ token: string }>("/api/settings/auth/password", {
|
|
26
|
+
password: password.trim(),
|
|
27
|
+
confirm: confirm.trim(),
|
|
28
|
+
});
|
|
29
|
+
// Update localStorage so current session stays authenticated
|
|
30
|
+
setAuthToken(token);
|
|
31
|
+
setSaved(true);
|
|
32
|
+
setPassword("");
|
|
33
|
+
setConfirm("");
|
|
34
|
+
setTimeout(() => {
|
|
35
|
+
setSaved(false);
|
|
36
|
+
setOpen(false);
|
|
37
|
+
}, 1500);
|
|
38
|
+
} catch (e) {
|
|
39
|
+
setError(e instanceof Error ? e.message : "Failed to change password");
|
|
40
|
+
} finally {
|
|
41
|
+
setSaving(false);
|
|
42
|
+
}
|
|
43
|
+
}, [canSubmit, password, confirm]);
|
|
44
|
+
|
|
45
|
+
if (!open) {
|
|
46
|
+
return (
|
|
47
|
+
<section className="space-y-2">
|
|
48
|
+
<h3 className="text-xs font-medium text-muted-foreground">Security</h3>
|
|
49
|
+
<Button
|
|
50
|
+
variant="outline"
|
|
51
|
+
size="sm"
|
|
52
|
+
className="h-8 text-xs gap-1.5 cursor-pointer"
|
|
53
|
+
onClick={() => setOpen(true)}
|
|
54
|
+
>
|
|
55
|
+
<KeyRound className="size-3.5" />
|
|
56
|
+
Change Password
|
|
57
|
+
</Button>
|
|
58
|
+
</section>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<section className="space-y-2">
|
|
64
|
+
<h3 className="text-xs font-medium text-muted-foreground">Change Password</h3>
|
|
65
|
+
<div className="space-y-2">
|
|
66
|
+
<div className="relative">
|
|
67
|
+
<Input
|
|
68
|
+
type={showPw ? "text" : "password"}
|
|
69
|
+
placeholder="New password"
|
|
70
|
+
value={password}
|
|
71
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
72
|
+
className="h-8 text-xs pr-8"
|
|
73
|
+
autoFocus
|
|
74
|
+
/>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground cursor-pointer"
|
|
78
|
+
onClick={() => setShowPw(!showPw)}
|
|
79
|
+
tabIndex={-1}
|
|
80
|
+
>
|
|
81
|
+
{showPw ? <EyeOff className="size-3.5" /> : <Eye className="size-3.5" />}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
<Input
|
|
85
|
+
type={showPw ? "text" : "password"}
|
|
86
|
+
placeholder="Confirm password"
|
|
87
|
+
value={confirm}
|
|
88
|
+
onChange={(e) => setConfirm(e.target.value)}
|
|
89
|
+
onKeyDown={(e) => { if (e.key === "Enter") handleSubmit(); }}
|
|
90
|
+
className="h-8 text-xs"
|
|
91
|
+
/>
|
|
92
|
+
{mismatch && (
|
|
93
|
+
<p className="text-[11px] text-destructive">Passwords do not match</p>
|
|
94
|
+
)}
|
|
95
|
+
{error && (
|
|
96
|
+
<p className="text-[11px] text-destructive">{error}</p>
|
|
97
|
+
)}
|
|
98
|
+
<div className="flex gap-1.5">
|
|
99
|
+
<Button
|
|
100
|
+
variant="outline"
|
|
101
|
+
size="sm"
|
|
102
|
+
className="h-8 text-xs flex-1 cursor-pointer"
|
|
103
|
+
onClick={() => {
|
|
104
|
+
setOpen(false);
|
|
105
|
+
setPassword("");
|
|
106
|
+
setConfirm("");
|
|
107
|
+
setError(null);
|
|
108
|
+
}}
|
|
109
|
+
>
|
|
110
|
+
Cancel
|
|
111
|
+
</Button>
|
|
112
|
+
<Button
|
|
113
|
+
variant={saved ? "default" : "outline"}
|
|
114
|
+
size="sm"
|
|
115
|
+
className="h-8 text-xs flex-1 cursor-pointer"
|
|
116
|
+
disabled={!canSubmit}
|
|
117
|
+
onClick={handleSubmit}
|
|
118
|
+
>
|
|
119
|
+
{saving ? "..." : saved ? <Check className="size-3.5" /> : "Save"}
|
|
120
|
+
</Button>
|
|
121
|
+
</div>
|
|
122
|
+
<p className="text-[11px] text-muted-foreground">
|
|
123
|
+
Min 4 characters. You'll stay logged in on this device.
|
|
124
|
+
</p>
|
|
125
|
+
</div>
|
|
126
|
+
</section>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
@@ -14,6 +14,7 @@ import { KeyboardShortcutsSection } from "./keyboard-shortcuts-section";
|
|
|
14
14
|
import { TelegramSettingsSection } from "./telegram-settings-section";
|
|
15
15
|
import { ProxySettingsSection } from "./proxy-settings-section";
|
|
16
16
|
import { McpSettingsSection } from "./mcp-settings-section";
|
|
17
|
+
import { ChangePasswordSection } from "./change-password-section";
|
|
17
18
|
import { usePushNotification } from "@/hooks/use-push-notification";
|
|
18
19
|
|
|
19
20
|
const THEME_OPTIONS: { value: Theme; label: string; icon: React.ElementType }[] = [
|
|
@@ -128,6 +129,9 @@ export function SettingsTab() {
|
|
|
128
129
|
</p>
|
|
129
130
|
</section>
|
|
130
131
|
|
|
132
|
+
{/* Security: Change Password */}
|
|
133
|
+
<ChangePasswordSection />
|
|
134
|
+
|
|
131
135
|
{/* Quick: Theme */}
|
|
132
136
|
<section className="space-y-2">
|
|
133
137
|
<h3 className="text-xs font-medium text-muted-foreground">Theme</h3>
|
|
@@ -22,6 +22,7 @@ interface UseChatReturn {
|
|
|
22
22
|
connectingElapsed: number;
|
|
23
23
|
pendingApproval: ApprovalRequest | null;
|
|
24
24
|
contextWindowPct: number | null;
|
|
25
|
+
compactStatus: "compacting" | null;
|
|
25
26
|
sessionTitle: string | null;
|
|
26
27
|
/** When CLI provider assigns a different session ID, this holds the new ID */
|
|
27
28
|
migratedSessionId: string | null;
|
|
@@ -51,6 +52,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
51
52
|
const [connectingElapsed, setConnectingElapsed] = useState(0);
|
|
52
53
|
const [pendingApproval, setPendingApproval] = useState<ApprovalRequest | null>(null);
|
|
53
54
|
const [contextWindowPct, setContextWindowPct] = useState<number | null>(null);
|
|
55
|
+
const [compactStatus, setCompactStatus] = useState<"compacting" | null>(null);
|
|
54
56
|
const [sessionTitle, setSessionTitle] = useState<string | null>(null);
|
|
55
57
|
const [isConnected, setIsConnected] = useState(false);
|
|
56
58
|
const [migratedSessionId, setMigratedSessionId] = useState<string | null>(null);
|
|
@@ -270,6 +272,19 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
270
272
|
return;
|
|
271
273
|
}
|
|
272
274
|
|
|
275
|
+
// Handle compact status events
|
|
276
|
+
if ((data as any).type === "compact_status") {
|
|
277
|
+
const status = (data as any).status;
|
|
278
|
+
if (status === "compacting") {
|
|
279
|
+
setCompactStatus("compacting");
|
|
280
|
+
} else if (status === "done") {
|
|
281
|
+
setCompactStatus(null);
|
|
282
|
+
// Refresh messages to show compacted history
|
|
283
|
+
refetchRef.current?.();
|
|
284
|
+
}
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
273
288
|
// Handle phase transitions from BE
|
|
274
289
|
if ((data as any).type === "phase_changed") {
|
|
275
290
|
const p = (data as any).phase as SessionPhase;
|
|
@@ -362,6 +377,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
362
377
|
setPhase("idle");
|
|
363
378
|
phaseRef.current = "idle";
|
|
364
379
|
setPendingApproval(null);
|
|
380
|
+
setCompactStatus(null);
|
|
365
381
|
streamingContentRef.current = "";
|
|
366
382
|
streamingEventsRef.current = [];
|
|
367
383
|
setIsConnected(false);
|
|
@@ -550,6 +566,7 @@ export function useChat(sessionId: string | null, providerId = "claude", project
|
|
|
550
566
|
connectingElapsed,
|
|
551
567
|
pendingApproval,
|
|
552
568
|
contextWindowPct,
|
|
569
|
+
compactStatus,
|
|
553
570
|
sessionTitle,
|
|
554
571
|
migratedSessionId,
|
|
555
572
|
sendMessage,
|