@hienlh/ppm 0.8.94 → 0.8.95
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 +48 -0
- package/dist/web/assets/api-settings-Bid0NHuI.js +1 -0
- package/dist/web/assets/{browser-tab-DJLH0eDY.js → browser-tab-DNiBGn5p.js} +1 -1
- package/dist/web/assets/chat-tab-w7jsIjfo.js +8 -0
- package/dist/web/assets/{code-editor-CaGdx-lS.js → code-editor-COwuo1MZ.js} +1 -1
- package/dist/web/assets/{database-viewer-i4Ddk6mO.js → database-viewer-CL3kXoYN.js} +1 -1
- package/dist/web/assets/{diff-viewer-DQDS7yjv.js → diff-viewer-BCuKcGH5.js} +1 -1
- package/dist/web/assets/{git-graph-DUs-TN1u.js → git-graph-CmQb8T0E.js} +1 -1
- package/dist/web/assets/{index-Dm6RN1A1.js → index-BlDA3VoN.js} +11 -11
- package/dist/web/assets/index-CqhIj4Ko.css +2 -0
- package/dist/web/assets/keybindings-store-D44LPqNY.js +1 -0
- package/dist/web/assets/{markdown-renderer-L1NgC2Rw.js → markdown-renderer-PdMYiBSA.js} +1 -1
- package/dist/web/assets/{postgres-viewer-_uDispGW.js → postgres-viewer-9yUy5BZB.js} +1 -1
- package/dist/web/assets/{settings-tab-Bp4041i6.js → settings-tab-DSF87yix.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-GW-QCjHn.js → sqlite-viewer-BaloRTBe.js} +1 -1
- package/dist/web/assets/{terminal-tab-E4cWujj4.js → terminal-tab-CM6G6XMO.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-zABXAAla.js → use-monaco-theme-C8rXfYU9.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/docs/streaming-input-guide.md +267 -0
- package/package.json +1 -1
- package/snapshot-state.md +1526 -0
- package/src/providers/claude-agent-sdk.ts +78 -2
- 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/account.service.ts +2 -2
- package/src/services/claude-usage.service.ts +2 -7
- 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/mcp-config.service.ts +15 -6
- package/src/services/supervisor.ts +22 -26
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +2 -0
- package/src/web/app.tsx +3 -2
- package/src/web/components/chat/chat-history-bar.tsx +39 -8
- package/src/web/components/chat/chat-tab.tsx +15 -10
- package/src/web/components/chat/message-list.tsx +7 -3
- package/src/web/components/chat/session-picker.tsx +1 -0
- package/src/web/components/chat/usage-badge.tsx +58 -8
- package/src/web/components/layout/upgrade-banner.tsx +15 -5
- 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/test-session-ops.mjs +444 -0
- package/test-tokens.mjs +212 -0
- package/.claude.bak/agent-memory/tester/MEMORY.md +0 -3
- package/.claude.bak/agent-memory/tester/project-ppm-test-conventions.md +0 -32
- package/dist/web/assets/api-settings-Dh4oFOpX.js +0 -1
- package/dist/web/assets/chat-tab-C8HFXqGS.js +0 -8
- package/dist/web/assets/index-DhtLEnPD.css +0 -2
- package/dist/web/assets/keybindings-store-qVLDZz97.js +0 -1
|
@@ -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({
|
|
@@ -90,13 +90,17 @@ export function MessageList({
|
|
|
90
90
|
<div className="relative flex-1 overflow-hidden flex flex-col min-h-0">
|
|
91
91
|
<StickToBottom className="flex-1 overflow-y-auto overflow-x-hidden" resize="smooth" initial="instant">
|
|
92
92
|
<StickToBottom.Content className="p-4 space-y-4">
|
|
93
|
-
{filtered.map((msg) => (
|
|
93
|
+
{filtered.map((msg, idx) => (
|
|
94
94
|
<MessageBubble
|
|
95
95
|
key={msg.id}
|
|
96
96
|
message={msg}
|
|
97
97
|
isStreaming={isStreaming && msg.id.startsWith("streaming-")}
|
|
98
98
|
projectName={projectName}
|
|
99
|
-
onFork={msg.role === "user" && onFork ? () =>
|
|
99
|
+
onFork={msg.role === "user" && onFork ? () => {
|
|
100
|
+
// Pass the previous message ID so the fork includes history up to (but not including) this user message
|
|
101
|
+
const prevMsg = idx > 0 ? filtered[idx - 1] : undefined;
|
|
102
|
+
onFork(msg.content, prevMsg?.id);
|
|
103
|
+
} : undefined}
|
|
100
104
|
/>
|
|
101
105
|
))}
|
|
102
106
|
|
|
@@ -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(
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useRef } from "react";
|
|
2
|
-
import { Activity, RefreshCw, Eye, Download, Upload, Plus, X, Settings } from "lucide-react";
|
|
2
|
+
import { Activity, RefreshCw, Eye, Download, Upload, Plus, X, Settings, Trash2 } from "lucide-react";
|
|
3
3
|
import { Switch } from "@/components/ui/switch";
|
|
4
4
|
import type { UsageInfo, LimitBucket } from "../../../types/chat";
|
|
5
5
|
import {
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
getActiveAccount,
|
|
8
8
|
getAllAccountUsages,
|
|
9
9
|
patchAccount,
|
|
10
|
+
deleteAccount,
|
|
10
11
|
type AccountInfo,
|
|
11
12
|
type AccountUsageEntry,
|
|
12
13
|
type OAuthProfileData,
|
|
@@ -152,11 +153,12 @@ function formatLastUpdated(ts: number | null | undefined): string | null {
|
|
|
152
153
|
return `${days}d ago`;
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, onViewProfile, flash }: {
|
|
156
|
+
function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onDelete, onExport, onViewProfile, flash }: {
|
|
156
157
|
entry: AccountUsageEntry;
|
|
157
158
|
isActive: boolean;
|
|
158
159
|
accountInfo?: AccountInfo;
|
|
159
160
|
onToggle?: (id: string, status: string) => void;
|
|
161
|
+
onDelete?: (id: string, display: string) => void;
|
|
160
162
|
onExport?: (id: string) => void;
|
|
161
163
|
onViewProfile?: (profile: OAuthProfileData) => void;
|
|
162
164
|
flash?: boolean;
|
|
@@ -164,19 +166,24 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
|
|
|
164
166
|
const { usage } = entry;
|
|
165
167
|
const hasBuckets = usage.session || usage.weekly || usage.weeklyOpus || usage.weeklySonnet;
|
|
166
168
|
const status = accountInfo?.status ?? entry.accountStatus;
|
|
169
|
+
// Expired: has expiresAt in the past AND no refresh token to auto-renew
|
|
170
|
+
const isExpired = !!(accountInfo && !accountInfo.hasRefreshToken && accountInfo.expiresAt && accountInfo.expiresAt < Math.floor(Date.now() / 1000));
|
|
167
171
|
|
|
168
172
|
return (
|
|
169
|
-
<div className={`rounded-md border p-2 space-y-1.5 transition-colors duration-500 min-w-[200px] shrink-0 snap-start ${flash ? "bg-primary/10 border-primary/40" : ""} ${isActive ? "border-primary/30 bg-primary/5" : "border-border/50"}`}>
|
|
173
|
+
<div className={`rounded-md border p-2 space-y-1.5 transition-colors duration-500 min-w-[200px] shrink-0 snap-start ${isExpired ? "opacity-50" : ""} ${flash ? "bg-primary/10 border-primary/40" : ""} ${isActive ? "border-primary/30 bg-primary/5" : "border-border/50"}`}>
|
|
170
174
|
<div className="flex items-center gap-1.5">
|
|
171
175
|
<span className="text-xs font-medium truncate flex-1 min-w-0">
|
|
172
176
|
{entry.accountLabel ?? entry.accountId.slice(0, 8)}
|
|
173
177
|
</span>
|
|
174
|
-
{
|
|
178
|
+
{isExpired && (
|
|
179
|
+
<span className="text-[9px] text-red-500 shrink-0 font-medium">Expired</span>
|
|
180
|
+
)}
|
|
181
|
+
{!entry.isOAuth && !isExpired && (
|
|
175
182
|
<span className="text-[9px] text-text-subtle shrink-0">API key</span>
|
|
176
183
|
)}
|
|
177
184
|
{/* Account controls */}
|
|
178
185
|
<div className="flex items-center gap-0.5 shrink-0">
|
|
179
|
-
{onViewProfile && accountInfo?.profileData && (
|
|
186
|
+
{!isExpired && onViewProfile && accountInfo?.profileData && (
|
|
180
187
|
<button
|
|
181
188
|
className="p-1 rounded cursor-pointer text-text-subtle hover:text-foreground hover:bg-surface-elevated transition-colors"
|
|
182
189
|
onClick={() => onViewProfile(accountInfo.profileData!)}
|
|
@@ -185,7 +192,7 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
|
|
|
185
192
|
<Eye className="size-3" />
|
|
186
193
|
</button>
|
|
187
194
|
)}
|
|
188
|
-
{onExport && entry.isOAuth && (
|
|
195
|
+
{!isExpired && onExport && entry.isOAuth && (
|
|
189
196
|
<button
|
|
190
197
|
className="p-1 rounded cursor-pointer text-text-subtle hover:text-blue-500 hover:bg-surface-elevated transition-colors"
|
|
191
198
|
onClick={() => onExport(entry.accountId)}
|
|
@@ -194,7 +201,7 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
|
|
|
194
201
|
<Download className="size-3" />
|
|
195
202
|
</button>
|
|
196
203
|
)}
|
|
197
|
-
{onToggle && (
|
|
204
|
+
{!isExpired && onToggle && (
|
|
198
205
|
<Switch
|
|
199
206
|
checked={status !== "disabled"}
|
|
200
207
|
onCheckedChange={() => onToggle(entry.accountId, status)}
|
|
@@ -202,6 +209,15 @@ function AccountUsageCard({ entry, isActive, accountInfo, onToggle, onExport, on
|
|
|
202
209
|
className="scale-[0.6] cursor-pointer"
|
|
203
210
|
/>
|
|
204
211
|
)}
|
|
212
|
+
{onDelete && (
|
|
213
|
+
<button
|
|
214
|
+
className="p-1 rounded cursor-pointer text-text-subtle hover:text-red-500 hover:bg-surface-elevated transition-colors"
|
|
215
|
+
onClick={() => onDelete(entry.accountId, entry.accountLabel ?? entry.accountId.slice(0, 8))}
|
|
216
|
+
title="Remove account"
|
|
217
|
+
>
|
|
218
|
+
<Trash2 className="size-3" />
|
|
219
|
+
</button>
|
|
220
|
+
)}
|
|
205
221
|
</div>
|
|
206
222
|
</div>
|
|
207
223
|
{hasBuckets ? (
|
|
@@ -247,6 +263,7 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
247
263
|
const [showExportDialog, setShowExportDialog] = useState(false);
|
|
248
264
|
const [showImportDialog, setShowImportDialog] = useState(false);
|
|
249
265
|
const [showRotationSettings, setShowRotationSettings] = useState(false);
|
|
266
|
+
const [deleteTarget, setDeleteTarget] = useState<{ id: string; display: string } | null>(null);
|
|
250
267
|
const [exportPreselect, setExportPreselect] = useState<string | null>(null);
|
|
251
268
|
const [message, setMessage] = useState<string | null>(null);
|
|
252
269
|
const msgTimer = useRef<ReturnType<typeof setTimeout>>(undefined);
|
|
@@ -325,13 +342,26 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
325
342
|
onReload?.();
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
async function confirmDeleteAccount() {
|
|
346
|
+
if (!deleteTarget) return;
|
|
347
|
+
try {
|
|
348
|
+
await deleteAccount(deleteTarget.id);
|
|
349
|
+
showMessage(`Account "${deleteTarget.display}" removed.`);
|
|
350
|
+
loadAll();
|
|
351
|
+
onReload?.();
|
|
352
|
+
} catch (e) {
|
|
353
|
+
showMessage(`Failed to remove: ${(e as Error).message}`);
|
|
354
|
+
}
|
|
355
|
+
setDeleteTarget(null);
|
|
356
|
+
}
|
|
357
|
+
|
|
328
358
|
function openExportAll() {
|
|
329
359
|
setExportPreselect(null);
|
|
330
360
|
setShowExportDialog(true);
|
|
331
361
|
}
|
|
332
362
|
|
|
333
363
|
return (
|
|
334
|
-
<div className="border-b border-border bg-surface px-3 py-2.5 space-y-2.5 max-h-[350px] overflow-y-auto">
|
|
364
|
+
<div className="relative border-b border-border bg-surface px-3 py-2.5 space-y-2.5 max-h-[350px] overflow-y-auto">
|
|
335
365
|
<div className="flex items-center justify-between">
|
|
336
366
|
<div className="flex items-center gap-2">
|
|
337
367
|
<span className="text-xs font-semibold text-text-primary">Usage & Accounts</span>
|
|
@@ -384,6 +414,7 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
384
414
|
isActive={entry.accountId === (activeAccountId ?? usage.activeAccountId)}
|
|
385
415
|
accountInfo={accountMap.get(entry.accountId)}
|
|
386
416
|
onToggle={handleToggle}
|
|
417
|
+
onDelete={(id, display) => setDeleteTarget({ id, display })}
|
|
387
418
|
onExport={(id) => { setExportPreselect(id); setShowExportDialog(true); }}
|
|
388
419
|
onViewProfile={setProfileView}
|
|
389
420
|
flash={flashIds.has(entry.accountId)}
|
|
@@ -460,6 +491,25 @@ export function UsageDetailPanel({ usage, visible, onClose, onReload, loading, l
|
|
|
460
491
|
</button>
|
|
461
492
|
</div>
|
|
462
493
|
|
|
494
|
+
{/* Delete confirmation overlay */}
|
|
495
|
+
{deleteTarget && (
|
|
496
|
+
<div className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-sm rounded-md">
|
|
497
|
+
<div className="bg-surface border border-border rounded-lg shadow-lg p-4 mx-4 max-w-[280px] w-full space-y-3">
|
|
498
|
+
<p className="text-xs text-text-primary text-center">
|
|
499
|
+
Remove <strong className="text-foreground">{deleteTarget.display}</strong>?
|
|
500
|
+
</p>
|
|
501
|
+
<div className="flex gap-2">
|
|
502
|
+
<button onClick={() => setDeleteTarget(null)} className="flex-1 px-3 py-1.5 rounded-md text-xs border border-border text-text-secondary hover:bg-surface-hover cursor-pointer transition-colors">
|
|
503
|
+
Cancel
|
|
504
|
+
</button>
|
|
505
|
+
<button onClick={confirmDeleteAccount} className="flex-1 px-3 py-1.5 rounded-md text-xs bg-red-500 text-white hover:bg-red-600 cursor-pointer transition-colors">
|
|
506
|
+
Remove
|
|
507
|
+
</button>
|
|
508
|
+
</div>
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
)}
|
|
512
|
+
|
|
463
513
|
{/* Account dialogs */}
|
|
464
514
|
<AddAccountDialog open={showAddDialog} onOpenChange={setShowAddDialog} onSuccess={handleSuccess} />
|
|
465
515
|
<ExportAccountsDialog open={showExportDialog} onOpenChange={(v) => { setShowExportDialog(v); if (!v) setExportPreselect(null); }} accounts={accounts} preselectId={exportPreselect} onMessage={showMessage} />
|
|
@@ -12,7 +12,11 @@ interface UpgradeStatus {
|
|
|
12
12
|
installMethod: string;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
interface UpgradeBannerProps {
|
|
16
|
+
onVisibilityChange?: (visible: boolean) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function UpgradeBanner({ onVisibilityChange }: UpgradeBannerProps) {
|
|
16
20
|
const [availableVersion, setAvailableVersion] = useState<string | null>(null);
|
|
17
21
|
const [upgrading, setUpgrading] = useState(false);
|
|
18
22
|
const [dismissed, setDismissed] = useState(false);
|
|
@@ -61,10 +65,16 @@ export function UpgradeBanner() {
|
|
|
61
65
|
setDismissed(true);
|
|
62
66
|
}, [availableVersion]);
|
|
63
67
|
|
|
64
|
-
|
|
68
|
+
const visible = !!availableVersion && !dismissed;
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
onVisibilityChange?.(visible);
|
|
72
|
+
}, [visible, onVisibilityChange]);
|
|
73
|
+
|
|
74
|
+
if (!visible) return null;
|
|
65
75
|
|
|
66
76
|
return (
|
|
67
|
-
<div className="w-full bg-blue-600 dark:bg-blue-700 text-white px-3 py-
|
|
77
|
+
<div className="w-full bg-blue-600 dark:bg-blue-700 text-white px-3 py-1 flex items-center justify-between gap-2 z-50 text-sm shrink-0">
|
|
68
78
|
{upgrading ? (
|
|
69
79
|
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
70
80
|
<Loader2 className="size-4 animate-spin shrink-0" />
|
|
@@ -83,13 +93,13 @@ export function UpgradeBanner() {
|
|
|
83
93
|
<div className="flex items-center gap-1 shrink-0">
|
|
84
94
|
<button
|
|
85
95
|
onClick={handleUpgrade}
|
|
86
|
-
className="bg-white text-blue-600 font-medium rounded-full px-3 min-h-[
|
|
96
|
+
className="bg-white text-blue-600 font-medium rounded-full px-3 py-0.5 text-xs min-h-[28px] min-w-[28px] flex items-center justify-center hover:bg-blue-50 active:bg-blue-100 transition-colors"
|
|
87
97
|
>
|
|
88
98
|
Upgrade
|
|
89
99
|
</button>
|
|
90
100
|
<button
|
|
91
101
|
onClick={handleDismiss}
|
|
92
|
-
className="min-h-[
|
|
102
|
+
className="min-h-[28px] min-w-[28px] flex items-center justify-center rounded-full hover:bg-blue-500 active:bg-blue-800 transition-colors"
|
|
93
103
|
aria-label="Dismiss upgrade notification"
|
|
94
104
|
>
|
|
95
105
|
<X className="size-4" />
|
|
@@ -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,
|