@hienlh/ppm 0.9.0-beta.8 → 0.9.1
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 +238 -0
- package/bun.lock +17 -0
- package/dist/web/assets/api-settings-BUvk6Saw.js +1 -0
- package/dist/web/assets/arrow-up-BYhx9ckd.js +1 -0
- package/dist/web/assets/browser-tab-CrkhFCaw.js +1 -0
- package/dist/web/assets/chat-tab-C6jpiwh7.js +8 -0
- package/dist/web/assets/chevron-right-5HgK6l7K.js +1 -0
- package/dist/web/assets/code-editor-CBIPzlP2.js +2 -0
- package/dist/web/assets/columns-2-cEVJHYd7.js +1 -0
- package/dist/web/assets/createLucideIcon-PuMiQgHl.js +1 -0
- package/dist/web/assets/{csv-preview-DLqYtXxt.js → csv-preview-ncSOnJSC.js} +2 -2
- package/dist/web/assets/database-viewer-BqOJR_zi.js +1 -0
- package/dist/web/assets/diff-viewer-CcLyp4eY.js +4 -0
- package/dist/web/assets/{dist-CALwEtco.js → dist-DIV6WgAG.js} +1 -1
- package/dist/web/assets/{dist-DGDPTxs1.js → dist-ovWkrgO-.js} +1 -1
- package/dist/web/assets/extension-webview-NiZ7Ybvv.js +3 -0
- package/dist/web/assets/git-graph-CoTvMrIo.js +1 -0
- package/dist/web/assets/index-C8byznLO.js +37 -0
- package/dist/web/assets/index-KwC2YrG4.css +2 -0
- package/dist/web/assets/jsx-runtime-kMwlnEGE.js +1 -0
- package/dist/web/assets/keybindings-store-DPYzBe_M.js +1 -0
- package/dist/web/assets/{markdown-renderer-DklUd_Gv.js → markdown-renderer-DPLdR9xc.js} +4 -4
- package/dist/web/assets/postgres-viewer-BeiK4lCa.js +1 -0
- package/dist/web/assets/settings-tab-D3AvU4lu.js +1 -0
- package/dist/web/assets/sqlite-viewer-nA2sD4Yv.js +1 -0
- package/dist/web/assets/tab-store-BOgTrqRr.js +1 -0
- package/dist/web/assets/table-DFevCOMd.js +1 -0
- package/dist/web/assets/tag-CXMT0QB6.js +1 -0
- package/dist/web/assets/{terminal-tab-CqRuiIFn.js → terminal-tab-BBi0pEji.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-Dcz3aLAE.js → use-monaco-theme-B5pG2d1w.js} +1 -1
- package/dist/web/index.html +8 -8
- package/dist/web/monacoeditorwork/css.worker.bundle.js +122 -122
- package/dist/web/monacoeditorwork/editor.worker.bundle.js +78 -78
- package/dist/web/monacoeditorwork/html.worker.bundle.js +110 -110
- package/dist/web/monacoeditorwork/json.worker.bundle.js +108 -108
- package/dist/web/monacoeditorwork/ts.worker.bundle.js +81 -81
- package/dist/web/sw.js +1 -1
- package/docs/code-standards.md +128 -1
- package/docs/codebase-summary.md +79 -12
- package/docs/extension-development-guide.md +532 -0
- package/docs/project-changelog.md +51 -1
- package/docs/project-roadmap.md +9 -3
- package/docs/streaming-input-guide.md +267 -0
- package/docs/system-architecture.md +432 -3
- package/package.json +6 -3
- package/packages/ext-database/package.json +41 -0
- package/packages/ext-database/src/connection-tree.ts +142 -0
- package/packages/ext-database/src/extension.ts +346 -0
- package/packages/ext-database/src/query-panel.ts +120 -0
- package/packages/ext-database/src/table-viewer-panel.ts +410 -0
- package/packages/ext-database/tsconfig.json +8 -0
- package/packages/vscode-compat/package.json +16 -0
- package/packages/vscode-compat/src/commands.ts +39 -0
- package/packages/vscode-compat/src/context.ts +65 -0
- package/packages/vscode-compat/src/disposable.ts +21 -0
- package/packages/vscode-compat/src/env.ts +20 -0
- package/packages/vscode-compat/src/event-emitter.ts +28 -0
- package/packages/vscode-compat/src/index.ts +93 -0
- package/packages/vscode-compat/src/not-supported.ts +15 -0
- package/packages/vscode-compat/src/types.ts +167 -0
- package/packages/vscode-compat/src/uri.ts +65 -0
- package/packages/vscode-compat/src/window.ts +229 -0
- package/packages/vscode-compat/src/workspace.ts +76 -0
- package/packages/vscode-compat/tsconfig.json +10 -0
- package/snapshot-state.md +1526 -0
- package/src/cli/commands/autostart.ts +1 -1
- package/src/cli/commands/ext-cmd.ts +121 -0
- package/src/cli/commands/restart.ts +9 -1
- package/src/cli/commands/status.ts +19 -0
- package/src/index.ts +5 -3
- package/src/providers/claude-agent-sdk.ts +221 -17
- package/src/providers/cli-provider-base.ts +6 -0
- package/src/server/index.ts +55 -155
- package/src/server/routes/chat.ts +81 -11
- package/src/server/routes/extensions.ts +81 -0
- package/src/server/routes/project-scoped.ts +2 -0
- package/src/server/routes/settings.ts +27 -0
- package/src/server/routes/workspace.ts +35 -0
- package/src/server/ws/chat.ts +9 -3
- package/src/server/ws/extensions.ts +175 -0
- package/src/services/account-selector.service.ts +14 -5
- package/src/services/account.service.ts +20 -15
- package/src/services/claude-usage.service.ts +29 -24
- package/src/services/cloud-ws.service.ts +228 -0
- package/src/services/cloud.service.ts +11 -6
- package/src/services/contribution-registry.ts +110 -0
- package/src/services/db.service.ts +181 -4
- package/src/services/extension-host-worker.ts +160 -0
- package/src/services/extension-installer.ts +112 -0
- package/src/services/extension-manifest.ts +65 -0
- package/src/services/extension-rpc-handlers.ts +235 -0
- package/src/services/extension-rpc.ts +105 -0
- package/src/services/extension.service.ts +228 -0
- package/src/services/mcp-config.service.ts +15 -6
- package/src/services/supervisor.ts +271 -25
- package/src/types/api.ts +1 -0
- package/src/types/chat.ts +4 -0
- package/src/types/extension-messages.ts +64 -0
- package/src/types/extension.ts +131 -0
- package/src/web/app.tsx +69 -48
- package/src/web/components/chat/account-rotation-settings.tsx +163 -0
- package/src/web/components/chat/chat-history-bar.tsx +106 -10
- package/src/web/components/chat/chat-tab.tsx +15 -10
- package/src/web/components/chat/chat-welcome.tsx +148 -0
- package/src/web/components/chat/message-list.tsx +19 -6
- package/src/web/components/chat/session-picker.tsx +80 -32
- package/src/web/components/chat/usage-badge.tsx +68 -8
- package/src/web/components/editor/editor-breadcrumb.tsx +20 -29
- package/src/web/components/extensions/extension-inputbox.tsx +92 -0
- package/src/web/components/extensions/extension-quickpick.tsx +194 -0
- package/src/web/components/extensions/extension-tree-view.tsx +240 -0
- package/src/web/components/extensions/extension-webview.tsx +83 -0
- package/src/web/components/layout/command-palette.tsx +22 -2
- package/src/web/components/layout/editor-panel.tsx +163 -18
- package/src/web/components/layout/mobile-nav.tsx +2 -1
- package/src/web/components/layout/sidebar.tsx +21 -3
- package/src/web/components/layout/status-bar.tsx +64 -0
- package/src/web/components/layout/tab-bar.tsx +2 -0
- package/src/web/components/layout/tab-content.tsx +5 -0
- 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/extension-manager-section.tsx +214 -0
- package/src/web/components/settings/settings-tab.tsx +9 -2
- package/src/web/components/shared/connection-lost-overlay.tsx +89 -0
- package/src/web/hooks/use-chat.ts +28 -0
- package/src/web/hooks/use-extension-ws.ts +181 -0
- package/src/web/hooks/use-global-keybindings.ts +18 -2
- package/src/web/hooks/use-server-reload.ts +9 -0
- package/src/web/hooks/use-url-sync.ts +173 -21
- package/src/web/stores/connection-store.ts +39 -0
- package/src/web/stores/extension-store.ts +204 -0
- package/src/web/stores/panel-store.ts +63 -9
- package/src/web/stores/panel-utils.ts +145 -3
- package/src/web/stores/settings-store.ts +7 -2
- package/src/web/stores/tab-store.ts +2 -1
- package/test-session-ops.mjs +444 -0
- package/test-tokens.mjs +212 -0
- package/tsconfig.json +3 -1
- package/dist/web/assets/api-settings-D21InCnR.js +0 -1
- package/dist/web/assets/arrow-up--LjUXLEt.js +0 -1
- package/dist/web/assets/browser-tab-BEe89aSD.js +0 -1
- package/dist/web/assets/chat-tab-9lqvWozA.js +0 -7
- package/dist/web/assets/chevron-right-CHnjJt4E.js +0 -1
- package/dist/web/assets/code-editor-COAIZx-B.js +0 -2
- package/dist/web/assets/columns-2-DbesTfa7.js +0 -1
- package/dist/web/assets/database-viewer-aRR9n_Ui.js +0 -1
- package/dist/web/assets/diff-viewer-C4KMvpHr.js +0 -4
- package/dist/web/assets/dist-CVTST7Gc.js +0 -1
- package/dist/web/assets/git-graph-CfJjl4E3.js +0 -1
- package/dist/web/assets/index-Db8uky1a.css +0 -2
- package/dist/web/assets/index-DxZuwBDe.js +0 -37
- package/dist/web/assets/jsx-runtime-BRW_vwa9.js +0 -1
- package/dist/web/assets/keybindings-store-_uWVCZMv.js +0 -1
- package/dist/web/assets/postgres-viewer-DEAvAyaX.js +0 -1
- package/dist/web/assets/settings-tab-BQedc-No.js +0 -1
- package/dist/web/assets/sqlite-viewer-BPA5idzT.js +0 -1
- package/dist/web/assets/tab-store-DhK6EpBT.js +0 -1
- package/dist/web/assets/table-CQVQM2SB.js +0 -1
- package/dist/web/assets/tag-Q2dZiSPX.js +0 -1
package/src/web/app.tsx
CHANGED
|
@@ -10,12 +10,18 @@ import { ProjectBottomSheet } from "@/components/layout/project-bottom-sheet";
|
|
|
10
10
|
import { LoginScreen } from "@/components/auth/login-screen";
|
|
11
11
|
import { useProjectStore, resolveOrder } from "@/stores/project-store";
|
|
12
12
|
import { useTabStore } from "@/stores/tab-store";
|
|
13
|
+
import { usePanelStore } from "@/stores/panel-store";
|
|
14
|
+
import {
|
|
15
|
+
fetchWorkspaceFromServer,
|
|
16
|
+
resolveWorkspaceConflict,
|
|
17
|
+
savePanelLayout,
|
|
18
|
+
} from "@/stores/panel-utils";
|
|
13
19
|
import {
|
|
14
20
|
useSettingsStore,
|
|
15
21
|
applyThemeClass,
|
|
16
22
|
} from "@/stores/settings-store";
|
|
17
23
|
import { getAuthToken } from "@/lib/api-client";
|
|
18
|
-
import { useUrlSync, parseUrlState } from "@/hooks/use-url-sync";
|
|
24
|
+
import { useUrlSync, parseUrlState, autoOpenFromUrl } from "@/hooks/use-url-sync";
|
|
19
25
|
import { useGlobalKeybindings } from "@/hooks/use-global-keybindings";
|
|
20
26
|
import { useNotificationBadge } from "@/hooks/use-notification-badge";
|
|
21
27
|
import { useServerReload } from "@/hooks/use-server-reload";
|
|
@@ -24,12 +30,18 @@ import { BugReportPopup } from "@/components/shared/bug-report-popup";
|
|
|
24
30
|
import { UpgradeBanner } from "@/components/layout/upgrade-banner";
|
|
25
31
|
import { ImageOverlay } from "@/components/shared/image-overlay";
|
|
26
32
|
import { DiagramOverlay } from "@/components/shared/diagram-overlay";
|
|
33
|
+
import { ConnectionLostOverlay } from "@/components/shared/connection-lost-overlay";
|
|
34
|
+
import { StatusBar } from "@/components/layout/status-bar";
|
|
35
|
+
import { ExtensionQuickPick } from "@/components/extensions/extension-quickpick";
|
|
36
|
+
import { ExtensionInputBox } from "@/components/extensions/extension-inputbox";
|
|
37
|
+
import { useExtensionWs } from "@/hooks/use-extension-ws";
|
|
27
38
|
import { cn } from "@/lib/utils";
|
|
28
39
|
|
|
29
40
|
type AuthState = "checking" | "authenticated" | "unauthenticated";
|
|
30
41
|
|
|
31
42
|
export function App() {
|
|
32
43
|
const [authState, setAuthState] = useState<AuthState>("checking");
|
|
44
|
+
const [upgradeBannerVisible, setUpgradeBannerVisible] = useState(false);
|
|
33
45
|
const [drawerOpen, setDrawerOpen] = useState(false);
|
|
34
46
|
const [drawerTab, setDrawerTab] = useState<"explorer" | "git" | "settings" | undefined>();
|
|
35
47
|
const [projectSheetOpen, setProjectSheetOpen] = useState(false);
|
|
@@ -109,6 +121,9 @@ export function App() {
|
|
|
109
121
|
// Auto-reload when server restarts (clears SW cache first)
|
|
110
122
|
useServerReload();
|
|
111
123
|
|
|
124
|
+
// Extension WS bridge — connects to /ws/extensions for UI updates (only after auth)
|
|
125
|
+
useExtensionWs(authState === "authenticated");
|
|
126
|
+
|
|
112
127
|
// Warn before closing browser tab (prevents accidental Ctrl+W)
|
|
113
128
|
useEffect(() => {
|
|
114
129
|
if (authState !== "authenticated") return;
|
|
@@ -127,56 +142,52 @@ export function App() {
|
|
|
127
142
|
});
|
|
128
143
|
}, [authState]);
|
|
129
144
|
|
|
130
|
-
// Fetch projects after auth, then restore
|
|
145
|
+
// Fetch projects after auth, then restore workspace + URL
|
|
131
146
|
useEffect(() => {
|
|
132
147
|
if (authState !== "authenticated") return;
|
|
133
148
|
|
|
134
|
-
fetchProjects().then(() => {
|
|
135
|
-
const
|
|
149
|
+
fetchProjects().then(async () => {
|
|
150
|
+
const urlState = initialUrlRef.current;
|
|
136
151
|
const { projects, customOrder } = useProjectStore.getState();
|
|
137
152
|
if (projects.length === 0) return;
|
|
138
153
|
|
|
139
154
|
// URL project takes priority, then fall back to first sorted project
|
|
140
|
-
let target =
|
|
155
|
+
let target = urlState.projectName
|
|
156
|
+
? projects.find((p) => p.name === urlState.projectName)
|
|
157
|
+
: undefined;
|
|
141
158
|
if (!target) {
|
|
142
159
|
target = resolveOrder(projects, customOrder)[0];
|
|
143
160
|
}
|
|
144
|
-
if (target)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
161
|
+
if (!target) return;
|
|
162
|
+
|
|
163
|
+
useProjectStore.getState().setActiveProject(target);
|
|
164
|
+
|
|
165
|
+
// Fetch server workspace + compare with localStorage (latest-wins)
|
|
166
|
+
const serverLayout = await fetchWorkspaceFromServer(target.name);
|
|
167
|
+
if (serverLayout) {
|
|
168
|
+
const localRaw = localStorage.getItem(`ppm-panels-${target.name}`);
|
|
169
|
+
const localLayout = localRaw ? JSON.parse(localRaw) : null;
|
|
170
|
+
const resolved = resolveWorkspaceConflict(localLayout, serverLayout);
|
|
171
|
+
if (resolved && resolved === serverLayout) {
|
|
172
|
+
// Server wins — overwrite localStorage and reload panels
|
|
173
|
+
savePanelLayout(target.name, resolved);
|
|
174
|
+
usePanelStore.getState().reloadProject(target.name);
|
|
153
175
|
}
|
|
154
176
|
}
|
|
155
177
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
setActiveTab(existing.id);
|
|
165
|
-
} else {
|
|
166
|
-
openTab({
|
|
167
|
-
type: "chat",
|
|
168
|
-
title: "Chat",
|
|
169
|
-
projectId: target?.name ?? null,
|
|
170
|
-
closable: true,
|
|
171
|
-
metadata: { sessionId: openChat },
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
// Clean up query param
|
|
178
|
+
// Auto-open target tab from URL (type-based)
|
|
179
|
+
queueMicrotask(() => {
|
|
180
|
+
if (urlState.tabType) {
|
|
181
|
+
autoOpenFromUrl(urlState.tabType, urlState.tabIdentifier, target!.name);
|
|
182
|
+
}
|
|
183
|
+
// Legacy: ?openChat= query param
|
|
184
|
+
if (urlState.openChat) {
|
|
185
|
+
autoOpenFromUrl("chat", urlState.openChat, target!.name);
|
|
175
186
|
const url = new URL(window.location.href);
|
|
176
187
|
url.searchParams.delete("openChat");
|
|
177
188
|
window.history.replaceState(null, "", url.pathname);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
180
191
|
});
|
|
181
192
|
}, [authState, fetchProjects]);
|
|
182
193
|
|
|
@@ -226,11 +237,11 @@ export function App() {
|
|
|
226
237
|
<TooltipProvider>
|
|
227
238
|
<div className="h-dvh flex flex-col bg-background text-foreground overflow-hidden relative">
|
|
228
239
|
{/* Upgrade banner — shown when new version available */}
|
|
229
|
-
<UpgradeBanner />
|
|
240
|
+
<UpgradeBanner onVisibilityChange={setUpgradeBannerVisible} />
|
|
230
241
|
|
|
231
242
|
{/* Mobile device name badge — floating top-left */}
|
|
232
243
|
{deviceName && (
|
|
233
|
-
<div className="md:hidden fixed
|
|
244
|
+
<div className={cn("md:hidden fixed left-0 z-50 px-2 py-0.5 bg-primary/80 text-primary-foreground text-[10px] font-medium rounded-br transition-[top]", upgradeBannerVisible ? "top-7" : "top-0")}>
|
|
234
245
|
{deviceName}
|
|
235
246
|
</div>
|
|
236
247
|
)}
|
|
@@ -244,17 +255,20 @@ export function App() {
|
|
|
244
255
|
<Sidebar />
|
|
245
256
|
|
|
246
257
|
{/* Content area — keep-alive per project */}
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
+
<div className="flex-1 flex flex-col overflow-hidden">
|
|
259
|
+
{[...mountedProjects].map((projectName) => (
|
|
260
|
+
<div
|
|
261
|
+
key={projectName}
|
|
262
|
+
className={cn(
|
|
263
|
+
"flex-1 overflow-hidden pb-12 md:pb-0",
|
|
264
|
+
activeProjectName !== projectName && "hidden",
|
|
265
|
+
)}
|
|
266
|
+
>
|
|
267
|
+
<PanelLayout projectName={projectName} />
|
|
268
|
+
</div>
|
|
269
|
+
))}
|
|
270
|
+
<StatusBar />
|
|
271
|
+
</div>
|
|
258
272
|
</div>
|
|
259
273
|
|
|
260
274
|
{/* Mobile bottom nav */}
|
|
@@ -288,6 +302,13 @@ export function App() {
|
|
|
288
302
|
{/* Global diagram lightbox (mermaid) */}
|
|
289
303
|
<DiagramOverlay />
|
|
290
304
|
|
|
305
|
+
{/* Extension modals (QuickPick, InputBox) */}
|
|
306
|
+
<ExtensionQuickPick />
|
|
307
|
+
<ExtensionInputBox />
|
|
308
|
+
|
|
309
|
+
{/* Connection lost overlay — shown when API unreachable for >15s */}
|
|
310
|
+
<ConnectionLostOverlay />
|
|
311
|
+
|
|
291
312
|
{/* Toast notifications */}
|
|
292
313
|
<Toaster
|
|
293
314
|
position="bottom-left"
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { useState, useEffect, useSyncExternalStore } from "react";
|
|
2
|
+
import { Settings, X } from "lucide-react";
|
|
3
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
|
|
4
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
5
|
+
import { cn } from "@/lib/utils";
|
|
6
|
+
import {
|
|
7
|
+
getAccountSettings,
|
|
8
|
+
updateAccountSettings,
|
|
9
|
+
type AccountSettings,
|
|
10
|
+
} from "../../lib/api-settings";
|
|
11
|
+
|
|
12
|
+
interface AccountRotationSettingsProps {
|
|
13
|
+
open: boolean;
|
|
14
|
+
onOpenChange: (open: boolean) => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const mdQuery = typeof window !== "undefined" ? window.matchMedia("(min-width: 768px)") : null;
|
|
18
|
+
function subscribeMedia(cb: () => void) {
|
|
19
|
+
mdQuery?.addEventListener("change", cb);
|
|
20
|
+
return () => mdQuery?.removeEventListener("change", cb);
|
|
21
|
+
}
|
|
22
|
+
function getIsDesktop() {
|
|
23
|
+
return mdQuery?.matches ?? true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SettingsContent() {
|
|
27
|
+
const [settings, setSettings] = useState<AccountSettings | null>(null);
|
|
28
|
+
const [loading, setLoading] = useState(true);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
setLoading(true);
|
|
32
|
+
getAccountSettings()
|
|
33
|
+
.then(setSettings)
|
|
34
|
+
.finally(() => setLoading(false));
|
|
35
|
+
}, []);
|
|
36
|
+
|
|
37
|
+
if (loading) {
|
|
38
|
+
return <p className="text-xs text-text-subtle py-4 text-center">Loading...</p>;
|
|
39
|
+
}
|
|
40
|
+
if (!settings) {
|
|
41
|
+
return <p className="text-xs text-text-subtle py-4 text-center">Failed to load settings</p>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="space-y-4">
|
|
46
|
+
{/* Strategy */}
|
|
47
|
+
<div className="space-y-1.5">
|
|
48
|
+
<label className="text-xs font-medium text-text-primary">Rotation Strategy</label>
|
|
49
|
+
<Select
|
|
50
|
+
value={settings.strategy}
|
|
51
|
+
onValueChange={async (v) => {
|
|
52
|
+
const updated = await updateAccountSettings({ strategy: v as AccountSettings["strategy"] });
|
|
53
|
+
setSettings(updated);
|
|
54
|
+
}}
|
|
55
|
+
>
|
|
56
|
+
<SelectTrigger className="w-full h-9 text-xs">
|
|
57
|
+
<SelectValue />
|
|
58
|
+
</SelectTrigger>
|
|
59
|
+
<SelectContent>
|
|
60
|
+
<SelectItem value="round-robin">Round-robin</SelectItem>
|
|
61
|
+
<SelectItem value="fill-first">Fill-first</SelectItem>
|
|
62
|
+
<SelectItem value="lowest-usage">Lowest usage</SelectItem>
|
|
63
|
+
</SelectContent>
|
|
64
|
+
</Select>
|
|
65
|
+
<p className="text-[10px] text-text-subtle">
|
|
66
|
+
{settings.strategy === "round-robin" && "Cycles through accounts evenly"}
|
|
67
|
+
{settings.strategy === "fill-first" && "Uses one account until its limit, then moves on"}
|
|
68
|
+
{settings.strategy === "lowest-usage" && "Picks the account with the lowest current usage"}
|
|
69
|
+
</p>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
{/* Max Retry */}
|
|
73
|
+
<div className="space-y-1.5">
|
|
74
|
+
<label className="text-xs font-medium text-text-primary">Max Retry</label>
|
|
75
|
+
<input
|
|
76
|
+
type="number"
|
|
77
|
+
min={0}
|
|
78
|
+
value={settings.maxRetry}
|
|
79
|
+
className="w-full h-9 text-xs border rounded-md px-3 bg-background"
|
|
80
|
+
onChange={async (e) => {
|
|
81
|
+
const v = parseInt(e.target.value, 10);
|
|
82
|
+
if (!isNaN(v) && v >= 0) {
|
|
83
|
+
const updated = await updateAccountSettings({ maxRetry: v });
|
|
84
|
+
setSettings(updated);
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
/>
|
|
88
|
+
<p className="text-[10px] text-text-subtle">
|
|
89
|
+
How many accounts to try on failure. 0 = try all available accounts.
|
|
90
|
+
</p>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{/* Active accounts */}
|
|
94
|
+
<div className="flex items-center justify-between text-xs border-t border-border pt-3">
|
|
95
|
+
<span className="text-text-subtle">Active accounts</span>
|
|
96
|
+
<span className="font-medium text-text-primary">{settings.activeCount}</span>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function AccountRotationSettings({ open, onOpenChange }: AccountRotationSettingsProps) {
|
|
103
|
+
const isDesktop = useSyncExternalStore(subscribeMedia, getIsDesktop);
|
|
104
|
+
|
|
105
|
+
if (!open) return null;
|
|
106
|
+
|
|
107
|
+
// Desktop: Dialog
|
|
108
|
+
if (isDesktop) {
|
|
109
|
+
return (
|
|
110
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
111
|
+
<DialogContent className="sm:max-w-sm">
|
|
112
|
+
<DialogHeader>
|
|
113
|
+
<DialogTitle className="text-sm flex items-center gap-2">
|
|
114
|
+
<Settings className="size-4" /> Rotation & Retry
|
|
115
|
+
</DialogTitle>
|
|
116
|
+
</DialogHeader>
|
|
117
|
+
<SettingsContent />
|
|
118
|
+
</DialogContent>
|
|
119
|
+
</Dialog>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Mobile: Bottom sheet
|
|
124
|
+
return (
|
|
125
|
+
<>
|
|
126
|
+
<div
|
|
127
|
+
className="fixed inset-0 z-50 transition-opacity duration-200 opacity-100"
|
|
128
|
+
onClick={() => onOpenChange(false)}
|
|
129
|
+
style={{ backgroundColor: "rgba(0,0,0,0.5)" }}
|
|
130
|
+
/>
|
|
131
|
+
<div
|
|
132
|
+
className={cn(
|
|
133
|
+
"fixed bottom-0 left-0 right-0 z-50 bg-background rounded-t-2xl border-t border-border shadow-2xl",
|
|
134
|
+
"transition-transform duration-300 ease-out max-h-[85vh] overflow-y-auto",
|
|
135
|
+
"translate-y-0",
|
|
136
|
+
)}
|
|
137
|
+
>
|
|
138
|
+
{/* Drag handle */}
|
|
139
|
+
<div className="flex justify-center pt-3 pb-1">
|
|
140
|
+
<div className="w-10 h-1 rounded-full bg-border" />
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
{/* Header */}
|
|
144
|
+
<div className="flex items-center justify-between px-4 py-2 border-b border-border">
|
|
145
|
+
<span className="text-sm font-semibold flex items-center gap-2">
|
|
146
|
+
<Settings className="size-4" /> Rotation & Retry
|
|
147
|
+
</span>
|
|
148
|
+
<button
|
|
149
|
+
onClick={() => onOpenChange(false)}
|
|
150
|
+
className="flex items-center justify-center size-7 rounded-md hover:bg-surface-elevated transition-colors"
|
|
151
|
+
>
|
|
152
|
+
<X className="size-4" />
|
|
153
|
+
</button>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Content */}
|
|
157
|
+
<div className="px-4 py-4 pb-8">
|
|
158
|
+
<SettingsContent />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
</>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
|
-
import { History, Settings2, Loader2, RefreshCw, Search, Pencil, Check, X, BellOff } from "lucide-react";
|
|
2
|
+
import { History, Settings2, Loader2, MessageSquare, RefreshCw, Search, Pencil, Check, X, BellOff, Bug, ClipboardCheck, Pin, PinOff, Trash2 } from "lucide-react";
|
|
3
3
|
import { Activity } from "lucide-react";
|
|
4
4
|
import { api, projectUrl } from "@/lib/api-client";
|
|
5
5
|
import { useTabStore } from "@/stores/tab-store";
|
|
@@ -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;
|
|
@@ -50,8 +51,36 @@ function pctColor(pct: number): string {
|
|
|
50
51
|
return "text-green-500";
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
function DebugCopyButton({ sessionId, projectName }: { sessionId: string; projectName: string }) {
|
|
55
|
+
const [copied, setCopied] = useState(false);
|
|
56
|
+
return (
|
|
57
|
+
<button
|
|
58
|
+
onClick={async () => {
|
|
59
|
+
try {
|
|
60
|
+
const data = await api.get<{ ppmSessionId: string; sdkSessionId: string; jsonlPath: string | null; projectPath: string }>(
|
|
61
|
+
`${projectUrl(projectName)}/chat/sessions/${sessionId}/debug?project=${encodeURIComponent(projectName)}`,
|
|
62
|
+
);
|
|
63
|
+
const info = [
|
|
64
|
+
`PPM Session: ${data.ppmSessionId}`,
|
|
65
|
+
`SDK Session: ${data.sdkSessionId}`,
|
|
66
|
+
data.jsonlPath ? `JSONL: ${data.jsonlPath}` : `JSONL: not found`,
|
|
67
|
+
data.projectPath ? `Project: ${data.projectPath}` : null,
|
|
68
|
+
].filter(Boolean).join("\n");
|
|
69
|
+
await navigator.clipboard.writeText(info);
|
|
70
|
+
setCopied(true);
|
|
71
|
+
setTimeout(() => setCopied(false), 1500);
|
|
72
|
+
} catch { /* silent */ }
|
|
73
|
+
}}
|
|
74
|
+
className={`p-1 rounded transition-colors ${copied ? "text-green-500 bg-green-500/10" : "text-text-subtle hover:text-text-secondary hover:bg-surface-elevated"}`}
|
|
75
|
+
title={copied ? "Copied!" : "Copy session debug info"}
|
|
76
|
+
>
|
|
77
|
+
{copied ? <ClipboardCheck className="size-3" /> : <Bug className="size-3" />}
|
|
78
|
+
</button>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
export function ChatHistoryBar({
|
|
54
|
-
projectName, usageInfo, contextWindowPct, usageLoading, refreshUsage, lastFetchedAt,
|
|
83
|
+
projectName, usageInfo, contextWindowPct, compactStatus, usageLoading, refreshUsage, lastFetchedAt,
|
|
55
84
|
sessionId, providerId, onSelectSession, onBugReport, isConnected, onReconnect,
|
|
56
85
|
}: ChatHistoryBarProps) {
|
|
57
86
|
const [activePanel, setActivePanel] = useState<PanelType>(null);
|
|
@@ -123,6 +152,37 @@ export function ChatHistoryBar({
|
|
|
123
152
|
|
|
124
153
|
const cancelEditing = useCallback(() => setEditingId(null), []);
|
|
125
154
|
|
|
155
|
+
const togglePin = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
|
|
156
|
+
e.stopPropagation();
|
|
157
|
+
if (!projectName) return;
|
|
158
|
+
const url = `${projectUrl(projectName)}/chat/sessions/${session.id}/pin`;
|
|
159
|
+
try {
|
|
160
|
+
if (session.pinned) {
|
|
161
|
+
await api.del(url);
|
|
162
|
+
} else {
|
|
163
|
+
await api.put(url);
|
|
164
|
+
}
|
|
165
|
+
setSessions((prev) => {
|
|
166
|
+
const updated = prev.map((s) => s.id === session.id ? { ...s, pinned: !s.pinned } : s);
|
|
167
|
+
return updated.sort((a, b) => {
|
|
168
|
+
if (a.pinned && !b.pinned) return -1;
|
|
169
|
+
if (!a.pinned && b.pinned) return 1;
|
|
170
|
+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
} catch { /* silent */ }
|
|
174
|
+
}, [projectName]);
|
|
175
|
+
|
|
176
|
+
const deleteSession = useCallback(async (e: React.MouseEvent, session: SessionInfo) => {
|
|
177
|
+
e.stopPropagation();
|
|
178
|
+
if (!projectName) return;
|
|
179
|
+
if (!window.confirm("Delete this session? This cannot be undone.")) return;
|
|
180
|
+
try {
|
|
181
|
+
await api.del(`${projectUrl(projectName)}/chat/sessions/${session.id}?providerId=${session.providerId}`);
|
|
182
|
+
setSessions((prev) => prev.filter((s) => s.id !== session.id));
|
|
183
|
+
} catch { /* silent */ }
|
|
184
|
+
}, [projectName]);
|
|
185
|
+
|
|
126
186
|
// Filter sessions by search query
|
|
127
187
|
const filteredSessions = searchQuery.trim()
|
|
128
188
|
? sessions.filter((s) => (s.title || "").toLowerCase().includes(searchQuery.toLowerCase()))
|
|
@@ -191,14 +251,27 @@ export function ChatHistoryBar({
|
|
|
191
251
|
<span className={pctColor(contextWindowPct)}>Ctx:{contextWindowPct}%</span>
|
|
192
252
|
</>
|
|
193
253
|
)}
|
|
254
|
+
{compactStatus === "compacting" && (
|
|
255
|
+
<>
|
|
256
|
+
<span className="text-text-subtle">·</span>
|
|
257
|
+
<span className="text-blue-400 animate-pulse">compacting...</span>
|
|
258
|
+
</>
|
|
259
|
+
)}
|
|
194
260
|
</button>
|
|
195
261
|
) : (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
262
|
+
<>
|
|
263
|
+
{contextWindowPct != null && (
|
|
264
|
+
<span className={`flex items-center gap-1 px-1.5 py-0.5 text-[11px] font-medium tabular-nums ${pctColor(contextWindowPct)}`}>
|
|
265
|
+
<Activity className="size-3" />
|
|
266
|
+
<span>Ctx:{contextWindowPct}%</span>
|
|
267
|
+
</span>
|
|
268
|
+
)}
|
|
269
|
+
{compactStatus === "compacting" && (
|
|
270
|
+
<span className="text-[11px] px-1.5 py-0.5 text-blue-400 animate-pulse">
|
|
271
|
+
compacting...
|
|
272
|
+
</span>
|
|
273
|
+
)}
|
|
274
|
+
</>
|
|
202
275
|
)}
|
|
203
276
|
|
|
204
277
|
{/* Spacer */}
|
|
@@ -215,6 +288,11 @@ export function ChatHistoryBar({
|
|
|
215
288
|
</button>
|
|
216
289
|
)}
|
|
217
290
|
|
|
291
|
+
{/* Debug info — copy session IDs + JSONL path */}
|
|
292
|
+
{sessionId && (
|
|
293
|
+
<DebugCopyButton sessionId={sessionId} projectName={projectName} />
|
|
294
|
+
)}
|
|
295
|
+
|
|
218
296
|
{/* Connection indicator */}
|
|
219
297
|
{onReconnect && (
|
|
220
298
|
<button
|
|
@@ -297,17 +375,35 @@ export function ChatHistoryBar({
|
|
|
297
375
|
>
|
|
298
376
|
{session.title || "Untitled"}
|
|
299
377
|
</button>
|
|
378
|
+
<button
|
|
379
|
+
onClick={(e) => togglePin(e, session)}
|
|
380
|
+
className={`p-0.5 rounded transition-all ${
|
|
381
|
+
session.pinned
|
|
382
|
+
? "text-primary hover:text-primary/70"
|
|
383
|
+
: "text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100"
|
|
384
|
+
}`}
|
|
385
|
+
title={session.pinned ? "Unpin session" : "Pin session"}
|
|
386
|
+
>
|
|
387
|
+
{session.pinned ? <PinOff className="size-3" /> : <Pin className="size-3" />}
|
|
388
|
+
</button>
|
|
300
389
|
<button
|
|
301
390
|
onClick={(e) => startEditing(session, e)}
|
|
302
|
-
className="p-0.5 rounded text-text-subtle hover:text-text-secondary opacity-0 group-hover:opacity-100 transition-opacity"
|
|
391
|
+
className="p-0.5 rounded text-text-subtle hover:text-text-secondary md:opacity-0 md:group-hover:opacity-100 transition-opacity"
|
|
303
392
|
title="Rename session"
|
|
304
393
|
>
|
|
305
394
|
<Pencil className="size-3" />
|
|
306
395
|
</button>
|
|
396
|
+
<button
|
|
397
|
+
onClick={(e) => deleteSession(e, session)}
|
|
398
|
+
className="p-0.5 rounded text-text-subtle hover:text-red-400 hover:bg-red-500/20 md:opacity-0 md:group-hover:opacity-100 transition-opacity"
|
|
399
|
+
title="Delete session"
|
|
400
|
+
>
|
|
401
|
+
<Trash2 className="size-3" />
|
|
402
|
+
</button>
|
|
307
403
|
</>
|
|
308
404
|
)}
|
|
309
405
|
{editingId !== session.id && session.updatedAt && (
|
|
310
|
-
<span className="text-[10px] text-text-subtle shrink-0">{formatDate(session.updatedAt)}</span>
|
|
406
|
+
<span className="text-[10px] text-text-subtle shrink-0 w-10 text-right">{formatDate(session.updatedAt)}</span>
|
|
311
407
|
)}
|
|
312
408
|
</div>
|
|
313
409
|
))
|
|
@@ -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,
|
|
@@ -134,14 +135,12 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
134
135
|
}
|
|
135
136
|
}, [sessionTitle]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
136
137
|
|
|
137
|
-
//
|
|
138
|
-
const
|
|
138
|
+
// Pending fork message — show in input for user to edit, not auto-send
|
|
139
|
+
const [forkDraft, setForkDraft] = useState<string | undefined>(metadata?.pendingMessage as string | undefined);
|
|
139
140
|
useEffect(() => {
|
|
140
|
-
if (
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (tabId) updateTab(tabId, { metadata: { ...metadata, pendingMessage: undefined } });
|
|
144
|
-
setTimeout(() => sendMessage(msg, { permissionMode }), 100);
|
|
141
|
+
if (forkDraft && isConnected && sessionId && tabId) {
|
|
142
|
+
// Clear from tab metadata once consumed
|
|
143
|
+
updateTab(tabId, { metadata: { ...metadata, pendingMessage: undefined } });
|
|
145
144
|
}
|
|
146
145
|
}, [isConnected, sessionId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
147
146
|
|
|
@@ -162,12 +161,13 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
162
161
|
}, [tabId, updateTab]);
|
|
163
162
|
|
|
164
163
|
/** Fork current session and open new tab with the forked session, resending userMessage */
|
|
165
|
-
const handleFork = useCallback(async (userMessage: string) => {
|
|
164
|
+
const handleFork = useCallback(async (userMessage: string, messageId?: string) => {
|
|
166
165
|
if (!sessionId || !projectName) return;
|
|
167
166
|
try {
|
|
168
167
|
const { api, projectUrl } = await import("@/lib/api-client");
|
|
169
168
|
const forked = await api.post<{ id: string; forkedFrom: string }>(
|
|
170
169
|
`${projectUrl(projectName)}/chat/sessions/${sessionId}/fork?providerId=${providerId}`,
|
|
170
|
+
{ messageId },
|
|
171
171
|
);
|
|
172
172
|
// Open new chat tab with forked session — it will send userMessage on connect
|
|
173
173
|
useTabStore.getState().openTab({
|
|
@@ -350,6 +350,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
350
350
|
projectName={projectName}
|
|
351
351
|
usageInfo={usageInfo}
|
|
352
352
|
contextWindowPct={contextWindowPct}
|
|
353
|
+
compactStatus={compactStatus}
|
|
353
354
|
usageLoading={usageLoading}
|
|
354
355
|
refreshUsage={refreshUsage}
|
|
355
356
|
lastFetchedAt={lastFetchedAt}
|
|
@@ -382,10 +383,14 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
382
383
|
|
|
383
384
|
{/* Input */}
|
|
384
385
|
<MessageInput
|
|
385
|
-
onSend={
|
|
386
|
+
onSend={(content, attachments, priority) => {
|
|
387
|
+
if (forkDraft) setForkDraft(undefined);
|
|
388
|
+
handleSend(content, attachments, priority);
|
|
389
|
+
}}
|
|
386
390
|
isStreaming={isStreaming}
|
|
387
391
|
onCancel={cancelStreaming}
|
|
388
|
-
autoFocus={!(metadata?.sessionId)}
|
|
392
|
+
autoFocus={!(metadata?.sessionId) || !!forkDraft}
|
|
393
|
+
initialValue={forkDraft}
|
|
389
394
|
projectName={projectName}
|
|
390
395
|
onSlashStateChange={handleSlashStateChange}
|
|
391
396
|
onSlashItemsLoaded={setSlashItems}
|