@hienlh/ppm 0.17.31 → 0.17.33
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 +16 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{ai-resource-editor-ByZApv3Y.js → ai-resource-editor-CZc98en9.js} +1 -1
- package/dist/web/assets/{audio-preview-izB0KGPQ.js → audio-preview-XkBaDV-d.js} +1 -1
- package/dist/web/assets/{chat-tab-CwWL5DbD.js → chat-tab-Cpxrbm1O.js} +6 -6
- package/dist/web/assets/{code-editor-Bx-Q27Nm.js → code-editor-DeqsPj8M.js} +2 -2
- package/dist/web/assets/{conflict-editor-B60UimS8.js → conflict-editor-CqORnyRu.js} +1 -1
- package/dist/web/assets/{csv-preview-BZv2SuCz.js → csv-preview-i79R-2TA.js} +1 -1
- package/dist/web/assets/{database-viewer-B5J_4lvX.js → database-viewer-ClrtBc_A.js} +1 -1
- package/dist/web/assets/{diff-viewer-DiytoNvX.js → diff-viewer-CWUjtM3T.js} +1 -1
- package/dist/web/assets/{docx-preview-CrjmF16z.js → docx-preview-CFjtBdLf.js} +1 -1
- package/dist/web/assets/{extension-webview-BSiQUxFD.js → extension-webview-DwRdXtK4.js} +1 -1
- package/dist/web/assets/{git-log-panel-Fb5EefXW.js → git-log-panel-DI_uaWVT.js} +1 -1
- package/dist/web/assets/{glide-data-grid-Cjve7mDb.js → glide-data-grid-Q2kMqNeo.js} +1 -1
- package/dist/web/assets/{group-chat-tab-DT0X7Cbz.js → group-chat-tab-2NJcUl_4.js} +1 -1
- package/dist/web/assets/{image-preview-BiVYVqa9.js → image-preview-QyeYNiJt.js} +1 -1
- package/dist/web/assets/{index-CuAijZGh.js → index-BI35sAVB.js} +4 -4
- package/dist/web/assets/{keybindings-store-sakq3gsF.js → keybindings-store-55cr_nFT.js} +1 -1
- package/dist/web/assets/{markdown-renderer-Cct9JHnx.js → markdown-renderer-ClDzjZ5T.js} +1 -1
- package/dist/web/assets/{markdown-renderer-BsKIIM83.js → markdown-renderer-DoofKFOC.js} +1 -1
- package/dist/web/assets/{notification-store-D4TsAnN3.js → notification-store-Bwzk7bz9.js} +1 -1
- package/dist/web/assets/panel-store-D-RxcUgH.js +1 -0
- package/dist/web/assets/{pdf-preview-B6G1QpE6.js → pdf-preview-Dd65pYcK.js} +1 -1
- package/dist/web/assets/{postgres-viewer-CdFu2Yj9.js → postgres-viewer-BAeRjowr.js} +1 -1
- package/dist/web/assets/{settings-tab-Dh3bD_dg.js → settings-tab-D_Vz5vjU.js} +1 -1
- package/dist/web/assets/{sql-query-editor-C8XXOY2R.js → sql-query-editor-RrxjEhFI.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-B1eXvaLK.js → sqlite-viewer-DULpl_6h.js} +1 -1
- package/dist/web/assets/{system-monitor-tab-D2QcCXl1.js → system-monitor-tab-DHOIu_5Y.js} +1 -1
- package/dist/web/assets/{tab-store-ByORqglW.js → tab-store-CNxCwdXw.js} +1 -1
- package/dist/web/assets/{terminal-tab-D2-buBAQ.js → terminal-tab-B64jdELv.js} +1 -1
- package/dist/web/assets/{tool-cards-DDxShmxN.js → tool-cards-DT3R2C18.js} +2 -2
- package/dist/web/assets/{use-monaco-theme-DMlVAYKD.js → use-monaco-theme-Bf_8qog4.js} +1 -1
- package/dist/web/assets/{video-preview-Cg3A7vrt.js → video-preview-C-ZB6w-Z.js} +1 -1
- package/dist/web/index.html +3 -3
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/server/routes/chat.ts +13 -4
- package/src/services/db.service.ts +36 -1
- package/src/services/session-branch.service.ts +43 -10
- package/src/services/slash-discovery/builtin-commands.ts +7 -3
- package/src/services/slash-discovery/index.ts +8 -1
- package/src/services/slash-discovery/sdk-commands.ts +126 -0
- package/src/services/slash-discovery/types.ts +5 -0
- package/src/shared/fuzzy-search.ts +37 -5
- package/src/web/components/chat/chat-tab.tsx +25 -5
- package/src/web/components/chat/slash-command-picker.tsx +2 -0
- package/src/web/components/layout/tab-bar.tsx +3 -3
- package/src/web/stores/dock-actions.ts +14 -4
- package/src/web/stores/panel-store.ts +8 -14
- package/dist/web/assets/panel-store-DsDZX8Mi.js +0 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { invalidateCache } from "./cache.ts";
|
|
2
|
+
import type { SlashItem } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Claude's own built-ins (/context, /init, /usage, …) live inside the CLI binary,
|
|
6
|
+
* so the filesystem scan in skill-loader.ts can never see them. The only way to
|
|
7
|
+
* enumerate them is to ask a live SDK session, which costs a CLI spawn — hence a
|
|
8
|
+
* much longer TTL than the filesystem cache.
|
|
9
|
+
*/
|
|
10
|
+
const TTL_MS = 30 * 60 * 1000;
|
|
11
|
+
const FETCH_TIMEOUT_MS = 20_000;
|
|
12
|
+
|
|
13
|
+
interface CacheEntry {
|
|
14
|
+
items: SlashItem[];
|
|
15
|
+
at: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const cache = new Map<string, CacheEntry>();
|
|
19
|
+
const inflight = new Map<string, Promise<SlashItem[]>>();
|
|
20
|
+
|
|
21
|
+
function isFresh(entry: CacheEntry | undefined): entry is CacheEntry {
|
|
22
|
+
return !!entry && Date.now() - entry.at < TTL_MS;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Keep only the SDK commands nothing else already provides. The SDK reports every
|
|
27
|
+
* command it knows — including the skills the filesystem scan just found — so
|
|
28
|
+
* dropping known names leaves exactly the ones that live inside the CLI binary.
|
|
29
|
+
*/
|
|
30
|
+
export function selectSdkOnlyCommands(sdkItems: SlashItem[], knownNames: Iterable<string>): SlashItem[] {
|
|
31
|
+
const known = new Set(knownNames);
|
|
32
|
+
return sdkItems.filter((item) => !known.has(item.name));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Cached SDK commands, or [] when none were fetched yet. Never spawns. */
|
|
36
|
+
export function getSdkCommands(projectPath: string): SlashItem[] {
|
|
37
|
+
const entry = cache.get(projectPath);
|
|
38
|
+
return isFresh(entry) ? entry.items : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Drop cached SDK commands so the next ensure() re-spawns. */
|
|
42
|
+
export function invalidateSdkCommands(projectPath?: string): void {
|
|
43
|
+
if (projectPath) cache.delete(projectPath);
|
|
44
|
+
else cache.clear();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchFromSdk(projectPath: string): Promise<SlashItem[]> {
|
|
48
|
+
// Loaded lazily: the CLI (`ppm skills`) uses this module's sync path only and
|
|
49
|
+
// must not pull in the provider's config/database dependencies.
|
|
50
|
+
const [{ query }, provider] = await Promise.all([
|
|
51
|
+
import("@anthropic-ai/claude-agent-sdk"),
|
|
52
|
+
import("../../providers/claude-agent-sdk.ts"),
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
const cliPath = provider.resolveCliExecutablePath();
|
|
56
|
+
|
|
57
|
+
// supportedCommands() resolves during the init handshake, but the CLI keeps
|
|
58
|
+
// running until its input stream ends — so hold the stream open until we're done.
|
|
59
|
+
let closeInput!: () => void;
|
|
60
|
+
const inputClosed = new Promise<void>((r) => { closeInput = r; });
|
|
61
|
+
async function* idleInput(): AsyncGenerator<never, void, undefined> {
|
|
62
|
+
await inputClosed;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const q = query({
|
|
66
|
+
prompt: idleInput(),
|
|
67
|
+
options: {
|
|
68
|
+
cwd: projectPath,
|
|
69
|
+
...(provider.needsNodeInterpreter(process.platform, cliPath) && { executable: "node" as const }),
|
|
70
|
+
...(cliPath && { pathToClaudeCodeExecutable: cliPath }),
|
|
71
|
+
settingSources: ["user", "project"],
|
|
72
|
+
maxTurns: 1,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
77
|
+
try {
|
|
78
|
+
const commands = await Promise.race([
|
|
79
|
+
q.supportedCommands(),
|
|
80
|
+
new Promise<never>((_, reject) => {
|
|
81
|
+
timer = setTimeout(() => reject(new Error("supportedCommands timed out")), FETCH_TIMEOUT_MS);
|
|
82
|
+
}),
|
|
83
|
+
]);
|
|
84
|
+
return commands.map((cmd) => ({
|
|
85
|
+
type: "builtin" as const,
|
|
86
|
+
name: cmd.name,
|
|
87
|
+
description: cmd.description ?? "",
|
|
88
|
+
argumentHint: cmd.argumentHint || undefined,
|
|
89
|
+
scope: "bundled" as const,
|
|
90
|
+
category: "session",
|
|
91
|
+
handler: "sdk" as const,
|
|
92
|
+
}));
|
|
93
|
+
} finally {
|
|
94
|
+
if (timer) clearTimeout(timer);
|
|
95
|
+
closeInput();
|
|
96
|
+
try { await q.interrupt?.(); } catch { /* already exiting */ }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Fetch SDK commands, reusing the cache. Concurrent callers share one spawn.
|
|
102
|
+
* Failures are cached too — an unauthenticated CLI would otherwise re-spawn on
|
|
103
|
+
* every picker open; the picker's refresh button clears it.
|
|
104
|
+
*/
|
|
105
|
+
export function ensureSdkCommands(projectPath: string): Promise<SlashItem[]> {
|
|
106
|
+
const entry = cache.get(projectPath);
|
|
107
|
+
if (isFresh(entry)) return Promise.resolve(entry.items);
|
|
108
|
+
|
|
109
|
+
const existing = inflight.get(projectPath);
|
|
110
|
+
if (existing) return existing;
|
|
111
|
+
|
|
112
|
+
const pending = fetchFromSdk(projectPath)
|
|
113
|
+
.catch((e: unknown) => {
|
|
114
|
+
console.warn(`[slash] SDK command discovery failed: ${(e as Error).message}`);
|
|
115
|
+
return [] as SlashItem[];
|
|
116
|
+
})
|
|
117
|
+
.then((items) => {
|
|
118
|
+
cache.set(projectPath, { items, at: Date.now() });
|
|
119
|
+
invalidateCache(projectPath); // merged item list is now stale
|
|
120
|
+
return items;
|
|
121
|
+
})
|
|
122
|
+
.finally(() => { inflight.delete(projectPath); });
|
|
123
|
+
|
|
124
|
+
inflight.set(projectPath, pending);
|
|
125
|
+
return pending;
|
|
126
|
+
}
|
|
@@ -16,6 +16,9 @@ export type SlashItemType = "skill" | "command" | "builtin" | "agent";
|
|
|
16
16
|
export type SlashItemScope = "project" | "user" | "bundled";
|
|
17
17
|
export type ItemOrigin = "skills" | "commands" | "agents";
|
|
18
18
|
|
|
19
|
+
/** Who runs a built-in: PPM server, the web client, or the Claude SDK */
|
|
20
|
+
export type SlashHandler = "ppm" | "sdk" | "client";
|
|
21
|
+
|
|
19
22
|
export interface SlashItem {
|
|
20
23
|
type: SlashItemType;
|
|
21
24
|
/** Slash name, e.g. "review", "devops/deploy", "ck:research" */
|
|
@@ -30,6 +33,8 @@ export interface SlashItem {
|
|
|
30
33
|
model?: string;
|
|
31
34
|
/** Agent-only: allowed tools (parsed from comma-separated string or YAML list) */
|
|
32
35
|
tools?: string[];
|
|
36
|
+
/** Built-in only: which layer executes the command */
|
|
37
|
+
handler?: SlashHandler;
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export interface SkillRoot {
|
|
@@ -48,9 +48,22 @@ export function scoreFuzzy(query: string, candidate: string): FuzzyScore | null
|
|
|
48
48
|
return null;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Promotion a recently used item gets, measured in match tiers. One tier lets a
|
|
53
|
+
* recent "contains" hit edge out a stranger's "prefix" hit, while a two-tier gap
|
|
54
|
+
* (prefix vs fuzzy) still wins on relevance.
|
|
55
|
+
*
|
|
56
|
+
* Only granted when the query matched the item's NAME. Descriptions are prose and
|
|
57
|
+
* match almost any short query, so boosting those would float every recent item
|
|
58
|
+
* to the top of every search.
|
|
59
|
+
*/
|
|
60
|
+
const RECENT_TIER_BOOST = 1;
|
|
61
|
+
|
|
51
62
|
/**
|
|
52
63
|
* Search items by query with fuzzy matching.
|
|
53
|
-
* Recently used items
|
|
64
|
+
* Recently used items are promoted by one match tier and then ordered
|
|
65
|
+
* most-recent-first, so a filtered list keeps the recency ordering the
|
|
66
|
+
* unfiltered picker shows.
|
|
54
67
|
* Returns ranked results (best match first), truncated to limit.
|
|
55
68
|
*/
|
|
56
69
|
export function searchFuzzy<T extends FuzzySearchable>(
|
|
@@ -63,8 +76,10 @@ export function searchFuzzy<T extends FuzzySearchable>(
|
|
|
63
76
|
// Cap query length to prevent quadratic blowup in Levenshtein
|
|
64
77
|
query = query.slice(0, 50);
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
79
|
+
// Position in recentNames, not a boolean: the caller's list is already ordered
|
|
80
|
+
// most-recent-first and that order has to survive into the results.
|
|
81
|
+
const recentRank = new Map(recentNames.map((name, i) => [name, i]));
|
|
82
|
+
const scored: Array<{ item: T; rank: number; distance: number; recency: number }> = [];
|
|
68
83
|
|
|
69
84
|
for (const item of items) {
|
|
70
85
|
const nameScore = scoreFuzzy(query, item.name);
|
|
@@ -73,13 +88,30 @@ export function searchFuzzy<T extends FuzzySearchable>(
|
|
|
73
88
|
.filter((s): s is FuzzyScore => s !== null)
|
|
74
89
|
.sort((a, b) => a.rank - b.rank || a.distance - b.distance)[0];
|
|
75
90
|
|
|
76
|
-
if (best)
|
|
91
|
+
if (best) {
|
|
92
|
+
const recency = recentRank.get(item.name);
|
|
93
|
+
// "Name reached the winning tier", not "name outscored the description":
|
|
94
|
+
// a description can happen to match at a smaller offset and would then
|
|
95
|
+
// silently disqualify a perfectly good name match from the boost.
|
|
96
|
+
const boosted = recency !== undefined && nameScore?.rank === best.rank;
|
|
97
|
+
scored.push({
|
|
98
|
+
item,
|
|
99
|
+
rank: best.rank - (boosted ? RECENT_TIER_BOOST : 0),
|
|
100
|
+
distance: best.distance,
|
|
101
|
+
// Sentinel, not Infinity: Infinity - Infinity is NaN and would silently
|
|
102
|
+
// neutralise the comparator for every pair of non-recent items.
|
|
103
|
+
recency: recency ?? recentNames.length,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
77
106
|
}
|
|
78
107
|
|
|
108
|
+
// Recency outranks distance so it actually takes effect: for "contains" matches
|
|
109
|
+
// distance is the match offset, which differs per item and previously decided
|
|
110
|
+
// every comparison before recency was ever consulted.
|
|
79
111
|
scored.sort((a, b) =>
|
|
80
112
|
a.rank - b.rank
|
|
113
|
+
|| a.recency - b.recency
|
|
81
114
|
|| a.distance - b.distance
|
|
82
|
-
|| (a.recent === b.recent ? 0 : a.recent ? -1 : 1)
|
|
83
115
|
|| a.item.name.localeCompare(b.item.name),
|
|
84
116
|
);
|
|
85
117
|
|
|
@@ -227,11 +227,11 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
227
227
|
return () => clearTimeout(t);
|
|
228
228
|
}, [editForking]);
|
|
229
229
|
|
|
230
|
-
const handleNewSession = useCallback(() => {
|
|
230
|
+
const handleNewSession = useCallback((title?: string, clearedFrom?: string) => {
|
|
231
231
|
useTabStore.getState().openTab({
|
|
232
232
|
type: "chat",
|
|
233
|
-
title: "AI Chat",
|
|
234
|
-
metadata: { projectName, providerId },
|
|
233
|
+
title: title || "AI Chat",
|
|
234
|
+
metadata: { projectName, providerId, ...(clearedFrom && { clearedFrom }) },
|
|
235
235
|
projectId: projectName || null,
|
|
236
236
|
closable: true,
|
|
237
237
|
});
|
|
@@ -379,6 +379,8 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
379
379
|
const session = await api.post<Session>(`${projectUrl(pName)}/chat/sessions`, {
|
|
380
380
|
providerId,
|
|
381
381
|
title: content.slice(0, 50),
|
|
382
|
+
// Set by /clear — the session only exists now, so persist the lineage here.
|
|
383
|
+
clearedFrom: metadata?.clearedFrom as string | undefined,
|
|
382
384
|
});
|
|
383
385
|
setSessionId(session.id);
|
|
384
386
|
setProviderId(session.providerId);
|
|
@@ -392,12 +394,30 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
392
394
|
}
|
|
393
395
|
sendMessage(fullContent, { permissionMode, priority });
|
|
394
396
|
},
|
|
395
|
-
[sessionId, providerId, projectName, sendMessage, buildMessageWithAttachments, permissionMode],
|
|
397
|
+
[sessionId, providerId, projectName, sendMessage, buildMessageWithAttachments, permissionMode, metadata],
|
|
396
398
|
);
|
|
397
399
|
|
|
400
|
+
// Read through a ref so handleInputSend keeps a stable identity — it is passed to
|
|
401
|
+
// the memoized MessageInput, and slashItems changes once the picker list loads.
|
|
402
|
+
const slashItemsRef = useRef(slashItems);
|
|
403
|
+
slashItemsRef.current = slashItems;
|
|
404
|
+
|
|
398
405
|
/** Stable wrapper for MessageInput onSend — clears forkDraft + draft and delegates to handleSend */
|
|
399
406
|
const handleInputSend = useCallback(
|
|
400
407
|
(content: string, attachments: ChatAttachment[], priority?: MessagePriority) => {
|
|
408
|
+
// Client-handled built-ins act on the UI, so they must not reach the SDK.
|
|
409
|
+
const slash = content.trim().match(/^\/(\S+)(?:\s+([\s\S]+))?$/);
|
|
410
|
+
if (slash) {
|
|
411
|
+
const item = slashItemsRef.current.find(
|
|
412
|
+
(i) => i.handler === "client" && (i.name === slash[1] || i.aliases?.includes(slash[1]!)),
|
|
413
|
+
);
|
|
414
|
+
if (item?.name === "clear") {
|
|
415
|
+
clearDraft();
|
|
416
|
+
handleNewSession(slash[2]?.trim(), sessionId ?? undefined);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
401
421
|
setForkDraft(undefined);
|
|
402
422
|
clearDraft();
|
|
403
423
|
if (editFork && sessionId && projectName) {
|
|
@@ -408,7 +428,7 @@ export function ChatTab({ metadata, tabId }: ChatTabProps) {
|
|
|
408
428
|
}
|
|
409
429
|
handleSend(content, attachments, priority);
|
|
410
430
|
},
|
|
411
|
-
[handleSend, clearDraft, editFork, sessionId, projectName, handleEditSend, buildMessageWithAttachments],
|
|
431
|
+
[handleSend, clearDraft, editFork, sessionId, projectName, handleEditSend, buildMessageWithAttachments, handleNewSession],
|
|
412
432
|
);
|
|
413
433
|
|
|
414
434
|
// Past user messages for the composer's ArrowUp/Down recall. Read through a ref
|
|
@@ -13,6 +13,8 @@ export interface SlashItem {
|
|
|
13
13
|
aliases?: string[];
|
|
14
14
|
/** Agent-only: model the subagent runs on */
|
|
15
15
|
model?: string;
|
|
16
|
+
/** Built-in only: which layer executes the command */
|
|
17
|
+
handler?: "ppm" | "sdk" | "client";
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
interface SlashCommandPickerProps {
|
|
@@ -225,7 +225,8 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
227
|
* Terminal-only "Move to Dock" / "Move to Editor" item.
|
|
228
|
-
*
|
|
228
|
+
* Reparents the same live session — the only way to keep a terminal running
|
|
229
|
+
* outside its current slot, since closing the tab ends it.
|
|
229
230
|
*/
|
|
230
231
|
function dockMoveMenuItems(tab: Tab): React.ReactNode {
|
|
231
232
|
if (tab.type !== "terminal") return null;
|
|
@@ -253,8 +254,7 @@ export const TabBar = memo(function TabBar({ panelId }: TabBarProps) {
|
|
|
253
254
|
break;
|
|
254
255
|
case "move-to-dock":
|
|
255
256
|
// Park the terminal in the dock (same live session — reparented, no restart).
|
|
256
|
-
panelState.
|
|
257
|
-
panelState.setDockVisible(true);
|
|
257
|
+
panelState.redockTab(tab.id, effectivePanelId);
|
|
258
258
|
break;
|
|
259
259
|
case "move-to-editor": {
|
|
260
260
|
// Send a dock terminal back to a grid panel: prefer the focused grid
|
|
@@ -174,10 +174,8 @@ export function makeOpenInDock(set: Set, get: Get) {
|
|
|
174
174
|
// ---------------------------------------------------------------------------
|
|
175
175
|
// Re-dock: move a terminal tab from a grid panel back to __dock__ + show dock.
|
|
176
176
|
//
|
|
177
|
-
//
|
|
178
|
-
// Closing a
|
|
179
|
-
// real kill only happens when closed from WITHIN the dock, on shell exit, or
|
|
180
|
-
// after the idle/grace period expires.
|
|
177
|
+
// Explicit user action only ("Move to Dock"): it parks a live terminal without
|
|
178
|
+
// restarting its PTY. Closing a tab never routes here — close ends the session.
|
|
181
179
|
//
|
|
182
180
|
// IMPORTANT: this function MUST NOT call closeTab (loop guard). It uses the
|
|
183
181
|
// low-level moveTab primitive directly via the store getter.
|
|
@@ -245,3 +243,15 @@ export function restoreDockForProject(
|
|
|
245
243
|
): DockState {
|
|
246
244
|
return projectDock[projectName] ?? { visible: false, height: 30 };
|
|
247
245
|
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Collapse a persisted "visible" dock on mobile.
|
|
249
|
+
*
|
|
250
|
+
* Mobile renders the dock as a full-width bottom sheet covering the tab bar, so
|
|
251
|
+
* restoring a visible dock — persisted locally or synced from a desktop session —
|
|
252
|
+
* would pop the sheet open over the workspace before the user asks for it. The
|
|
253
|
+
* sheet only opens from the nav's terminal button.
|
|
254
|
+
*/
|
|
255
|
+
export function collapseRestoredDockOnMobile(dock: DockState, isMobile: boolean): DockState {
|
|
256
|
+
return isMobile && dock.visible ? { ...dock, visible: false } : dock;
|
|
257
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
persistDock,
|
|
27
27
|
snapshotDockForProject,
|
|
28
28
|
restoreDockForProject,
|
|
29
|
+
collapseRestoredDockOnMobile,
|
|
29
30
|
activeProjectDockTabCount,
|
|
30
31
|
} from "./dock-actions";
|
|
31
32
|
|
|
@@ -284,7 +285,10 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
284
285
|
const mergedPanels = { ...panels, ...migratedPanels, [DOCK_PANEL_ID]: mergedDockPanel };
|
|
285
286
|
newProjectGrids[projectName] = loaded.grid;
|
|
286
287
|
newProjectFocused[projectName] = loaded.focusedPanelId;
|
|
287
|
-
const restoredDock =
|
|
288
|
+
const restoredDock = collapseRestoredDockOnMobile(
|
|
289
|
+
loaded.dock ?? restoreDockForProject(projectName, newProjectDock),
|
|
290
|
+
get().isMobile(),
|
|
291
|
+
);
|
|
288
292
|
set({
|
|
289
293
|
currentProject: projectName,
|
|
290
294
|
panels: mergedPanels,
|
|
@@ -518,19 +522,9 @@ export const usePanelStore = create<PanelStore>()((set, get) => {
|
|
|
518
522
|
if (!panel) return;
|
|
519
523
|
const pid = panel.id;
|
|
520
524
|
|
|
521
|
-
//
|
|
522
|
-
//
|
|
523
|
-
//
|
|
524
|
-
// Decision is purely location-based — no Tab.home flag needed.
|
|
525
|
-
if (tabId.startsWith("terminal:") && pid !== DOCK_PANEL_ID) {
|
|
526
|
-
// Park in dock — redockTab moves the tab object and shows the dock.
|
|
527
|
-
// Does NOT strip the localStorage session key (PTY stays alive).
|
|
528
|
-
// redockTab calls moveTab, never closeTab → no recursion.
|
|
529
|
-
get().redockTab(tabId, pid);
|
|
530
|
-
return;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Real-close path: terminal closed from dock, or non-terminal tab.
|
|
525
|
+
// Closing a terminal ends it, from a grid panel and from the dock alike.
|
|
526
|
+
// Parking a live session is an explicit action ("Move to Dock" / redockTab),
|
|
527
|
+
// never a side effect of close.
|
|
534
528
|
// Clear persisted terminal session so reopening creates a fresh PTY.
|
|
535
529
|
if (tabId.startsWith("terminal:")) {
|
|
536
530
|
try { localStorage.removeItem(`ppm:terminal-session:${tabId}`); } catch { /* */ }
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./react-8hrD3-kv.js";import{r as t}from"./utils-E0yyGxXt.js";var n=`__dock__`,r=new Set([`terminal`,`system-monitor`]);function i(){return{id:n,tabs:[],activeTabId:null,tabHistory:[]}}function a(){return`panel-${t()}`}function o(e=[],t=null){return{id:a(),tabs:e,activeTabId:t,tabHistory:t?[t]:[]}}function s(e){return e?1:3}function c(e,t){return e.map(e=>e.filter(e=>e!==t)).filter(e=>e.length>0)}function l(e,t){for(let n=0;n<e.length;n++){let r=e[n].indexOf(t);if(r!==-1)return{row:n,col:r}}return null}function u(e){let t=0;for(let n of Object.values(e))for(let e of n.tabs){let n=e.id.match(/^editor:untitled-(\d+)$/);n&&(t=Math.max(t,Number(n[1])))}return t+1}function d(e,n){switch(e){case`editor`:return n?.viewerKey?`editor:viewer:${n.viewerKey}`:n?.isUntitled?`editor:untitled-${n.untitledNumber??1}`:`editor:${n?.filePath??`untitled`}`;case`chat`:return`chat:${n?.providerId??`default`}/${n?.sessionId??t()}`;case`terminal`:return`terminal:${n?.terminalIndex??1}`;case`database`:return`database:${n?.connectionId??`default`}:${n?.tableName??``}`;case`sqlite`:return`sqlite:${n?.filePath??`default`}`;case`postgres`:return`postgres:${n?.connectionId??`default`}:${n?.tableName??``}`;case`extension`:return`extension:${String(n?.viewType??`unknown`).replace(/\.view$/,``)}`;case`git-diff`:return`git-diff:${n?.filePath??`unknown`}`;case`conflict-editor`:return`conflict-editor:${n?.filePath??`unknown`}`;case`settings`:return`settings`;case`group`:return`group:${n?.groupId??`unknown`}`;default:return`${e}:${t()}`}}var f=new Set([`ports`,`tunnels`]);function p(e){if(!e.tabs.some(e=>f.has(e.type)))return e;let t=new Set(e.tabs.filter(e=>f.has(e.type)).map(e=>e.id)),n=e.tabs.filter(e=>!t.has(e.id)),r=e.tabHistory.filter(e=>!t.has(e)),i=e.activeTabId&&!t.has(e.activeTabId)?e.activeTabId:r[r.length-1]??n[n.length-1]?.id??null;return{...e,tabs:n,tabHistory:r,activeTabId:i}}function m(e){let t={};for(let[n,r]of Object.entries(e.panels))t[n]=p(r);let n=e.dockPanel?p(e.dockPanel):e.dockPanel;return{...e,panels:t,dockPanel:n}}function h(e){let t={...e,panels:{...e.panels}};for(let[e,n]of Object.entries(t.panels)){let r=n.tabs.map(e=>{if(e.id.startsWith(`tab-`)){let t=d(e.type,e.metadata);return{...e,id:t}}return e}),i=new Map;n.tabs.forEach((e,t)=>{e.id!==r[t].id&&i.set(e.id,r[t].id)});let a=i.get(n.activeTabId??``)??n.activeTabId,o=n.tabHistory.map(e=>i.get(e)??e);t.panels[e]={...n,tabs:r,activeTabId:a,tabHistory:o}}return t}var g=`ppm-panels-`,_=`ppm-tabs-`;function v(e){return`${g}${e}`}function y(e,t){try{let n={...t,updatedAt:new Date().toISOString()};localStorage.setItem(v(e),JSON.stringify(n)),e!==`__global__`&&w(e,t)}catch{}}function b(e){try{let t=localStorage.getItem(v(e));if(t)return x(h(m(JSON.parse(t))))}catch{}let t=D(e);return t?x(m(t)):null}function x(e){let t=e.dock??{visible:!1,height:30},n;if(!e.dockPanel)n=i();else{let t=e.dockPanel.tabs.filter(e=>r.has(e.type)),i=new Set(t.map(e=>e.id)),a=e.dockPanel.tabHistory.filter(e=>i.has(e)),o=e.dockPanel.activeTabId&&i.has(e.dockPanel.activeTabId)?e.dockPanel.activeTabId:a[a.length-1]??t[t.length-1]?.id??null;n={...e.dockPanel,tabs:t,tabHistory:a,activeTabId:o}}return{...e,dock:t,dockPanel:n}}async function S(e){if(e===`__global__`)return null;try{let t={},n=localStorage.getItem(`ppm-auth-token`);n&&(t.Authorization=`Bearer ${n}`);let r=await fetch(`/api/project/${encodeURIComponent(e)}/workspace`,{headers:t});if(!r.ok)return null;let i=await r.json();return!i.ok||!i.data?null:{...i.data.layout,updatedAt:i.data.updatedAt}}catch{return null}}var C=new Map;function w(e,t){let n=C.get(e);n&&clearTimeout(n),C.set(e,setTimeout(async()=>{C.delete(e);try{let n={"Content-Type":`application/json`},r=localStorage.getItem(`ppm-auth-token`);r&&(n.Authorization=`Bearer ${r}`);let i=await fetch(`/api/project/${encodeURIComponent(e)}/workspace`,{method:`PUT`,headers:n,body:JSON.stringify({layout:t})});if(i.ok){let t=await i.json();if(t.data?.updatedAt){let n=`${g}${e}`,r=localStorage.getItem(n);if(r){let e=JSON.parse(r);e.updatedAt=t.data.updatedAt,localStorage.setItem(n,JSON.stringify(e))}}}}catch{}},1500))}function T(e,t){if(!e&&!t)return null;if(!e)return t;if(!t)return e;let n=new Date(e.updatedAt).getTime();return new Date(t.updatedAt).getTime()>=n?t:e}async function E(e){if(e===`__global__`)return!1;let t=await S(e);if(!t)return!1;let n=`${g}${e}`,r=null;try{let e=localStorage.getItem(n);r=e?JSON.parse(e):null}catch{}if(T(r,t)!==t)return!1;try{let e={...m(t),updatedAt:t.updatedAt};return localStorage.setItem(n,JSON.stringify(e)),!0}catch{return!1}}function D(e){try{let t=localStorage.getItem(`${_}${e}`);if(!t)return null;let n=JSON.parse(t);if(!n.tabs?.length)return null;let r=o(n.tabs,n.activeTabId),i={panels:{[r.id]:r},grid:[[r.id]],focusedPanelId:r.id};return y(e,i),localStorage.removeItem(`${_}${e}`),i}catch{return null}}var O=15,k=85;function A(e){return Math.min(k,Math.max(O,e))}function j(e,t){let n=e.filter(e=>e!==t);return n.push(t),n.length>50&&n.shift(),n}function M(e){let t=e().panels[n];if(!t)return 0;let r=e().currentProject;return t.tabs.filter(e=>!e.projectId||!r||e.projectId===r).length}function N(e){let t=e().currentProject;e().openInDock({type:`terminal`,title:`Terminal`,projectId:t??null,closable:!0,metadata:t?{projectName:t}:void 0})}function P(e,t){return function(){let{dock:n}=t(),r=!n.visible;e({dock:{...n,visible:r}}),z(t),r&&M(t)===0&&N(t)}}function F(e,t){return function(n){let{dock:r}=t();e({dock:{...r,visible:n}}),z(t)}}function I(e,t){return function(n){let{dock:r}=t();e({dock:{...r,height:A(n)}}),z(t)}}function L(e,t){return function(r){let{panels:a,dock:o}=t(),s=a.__dock__??i(),c=r;if(r.type===`terminal`&&!r.metadata?.terminalIndex){let e=Object.values(a).flatMap(e=>e.tabs).filter(e=>e.type===`terminal`).map(e=>{let t=e.id.match(/^terminal:(\d+)/);return t?parseInt(t[1],10):0}),t=e.length>0?Math.max(...e)+1:1;c={...r,metadata:{...r.metadata,terminalIndex:t}}}let l=d(c.type,c.metadata);if(s.tabs.find(e=>e.id===l))return e(e=>({dock:{...e.dock,visible:!0},panels:{...e.panels,[n]:{...s,activeTabId:l,tabHistory:j(s.tabHistory,l)}}})),z(t),l;let u={...c,id:l},f=[...s.tabs,u],p=j(s.tabHistory,l);return e(e=>({dock:{...e.dock,visible:!0},panels:{...e.panels,[n]:{...s,tabs:f,activeTabId:l,tabHistory:p}}})),z(t),l}}function R(e,t){return function(r,i){t().moveTab(r,i,n);let{dock:a}=t();e({dock:{...a,visible:!0}}),z(t)}}function z(e){let{currentProject:t,panels:n,grid:r,focusedPanelId:a,dock:o}=e();if(!t)return;let s=new Set(r.flat()),c={};for(let[e,t]of Object.entries(n))s.has(e)&&(c[e]=t);y(t,{panels:c,grid:r,focusedPanelId:a,dock:o,dockPanel:n.__dock__??i()})}function B(e,t,n){return{...n,[e]:t}}function V(e,t){return t[e]??{visible:!1,height:30}}var H=new Set([`settings`,`git-log`]),U=new Set([`projects`,`git-status`,`git-graph`]);function W(e,t){if(!e)return null;let n=e.tabs.filter(e=>!e.projectId||e.projectId===t);if(n.length===0)return null;for(let t=e.tabHistory.length-1;t>=0;t--){let r=e.tabHistory[t];if(n.some(e=>e.id===r))return r}return n[n.length-1].id}function G(e,t){let n=e.filter(e=>e!==t);return n.push(t),n.length>50&&n.shift(),n}function K(e,t){let n=Date.now();return e.map(e=>e.id===t?{...e,metadata:{...e.metadata,lastActiveAt:n}}:e)}function q(){let e=o(),t=i();return{panels:{[e.id]:e,[n]:t},grid:[[e.id]],focusedPanelId:e.id}}var J=e()((e,t)=>{function r(){let{currentProject:e,panels:r,grid:i,focusedPanelId:a,dock:o}=t();if(!e)return;let s=new Set(i.flat()),c={};for(let[e,t]of Object.entries(r))s.has(e)&&(c[e]=t);y(e,{panels:c,grid:i,focusedPanelId:a,dock:o,dockPanel:r[n]})}function a(e){return Object.values(t().panels).find(t=>t.tabs.some(t=>t.id===e))}function u(e){return e??t().focusedPanelId}return{...q(),currentProject:null,projectGrids:{},projectFocused:{},dock:{visible:!1,height:30},projectDock:{},dockExpanded:!1,toggleDock:P(e,t),setDockVisible:F(e,t),setDockHeight:I(e,t),toggleDockExpanded:()=>e(e=>({dockExpanded:!e.dockExpanded})),openInDock:L(e,t),redockTab:R(e,t),switchProject:r=>{let{currentProject:a,panels:s,grid:c,focusedPanelId:l,projectGrids:u,projectFocused:d,dock:f,projectDock:p}=t();if(a===r)return;let m={...u},h={...d},g=a?B(a,f,p):{...p};if(a){m[a]=c,h[a]=l;let e=new Set(c.flat()),t={};for(let[n,r]of Object.entries(s))e.has(n)&&(t[n]=r);y(a,{panels:t,grid:c,focusedPanelId:l,dock:f,dockPanel:s[n]})}if(m[r]){let t=m[r],i=h[r]??t[0]?.[0]??``,a=V(r,g),o=s[n];e({currentProject:r,panels:o?{...s,[n]:{...o,activeTabId:W(o,r)}}:s,grid:t,focusedPanelId:i,projectGrids:m,projectFocused:h,dock:a,projectDock:g});return}let _=b(r);if(_&&Object.keys(_.panels).length>0){let t={};for(let[e,n]of Object.entries(_.panels)){let r=n.tabs.filter(e=>!U.has(e.type)),i=n.tabHistory.filter(e=>r.some(t=>t.id===e)),a=n.activeTabId&&r.some(e=>e.id===n.activeTabId)?n.activeTabId:i[i.length-1]??r[0]?.id??null;t[e]={...n,tabs:r,tabHistory:i,activeTabId:a}}let a=s.__dock__??i(),o=_.dockPanel??i(),c=[...a.tabs];for(let e of o.tabs)c.some(t=>t.id===e.id)||c.push(e);let l={...a,tabs:c,tabHistory:[...new Set([...a.tabHistory,...o.tabHistory])].filter(e=>c.some(t=>t.id===e)),activeTabId:null};l.activeTabId=W(l,r);let u={...s,...t,[n]:l};m[r]=_.grid,h[r]=_.focusedPanelId;let d=_.dock??V(r,g);e({currentProject:r,panels:u,grid:_.grid,focusedPanelId:_.focusedPanelId,projectGrids:m,projectFocused:h,dock:d,projectDock:g})}else{let t=o(),i=[[t.id]],a={...s,[t.id]:t};m[r]=i,h[r]=t.id;let c=V(r,g);y(r,{panels:{[t.id]:t},grid:i,focusedPanelId:t.id,dock:c,dockPanel:s[n]}),e({currentProject:r,panels:a,grid:i,focusedPanelId:t.id,projectGrids:m,projectFocused:h,dock:c,projectDock:g})}},reloadProject:r=>{let{projectGrids:i,projectFocused:a,projectDock:o,panels:s}=t(),c={...i},l={...a},u={...o};delete c[r],delete l[r],delete u[r];let d=i[r],f=d?new Set(d.flat()):new Set,p={...s};for(let e of f)e!==`__dock__`&&delete p[e];let m=p[n];if(m){let e=m.tabs.filter(e=>e.projectId!==r),t=new Set(e.map(e=>e.id));p[n]={...m,tabs:e,tabHistory:m.tabHistory.filter(e=>t.has(e)),activeTabId:m.activeTabId&&t.has(m.activeTabId)?m.activeTabId:e[e.length-1]?.id??null}}e({projectGrids:c,projectFocused:l,projectDock:u,panels:p,currentProject:null}),t().switchProject(r)},setFocusedPanel:n=>{t().panels[n]&&e({focusedPanelId:n})},openTab:(n,i)=>{let a=t().isMobile(),o=a?t().grid[0]?.[0]??u(i):u(i);if(!t().panels[o])return``;if(n.type===`terminal`&&!n.metadata?.terminalIndex){let e=Object.values(t().panels).flatMap(e=>e.tabs).filter(e=>e.type===`terminal`).map(e=>{let t=e.id.match(/^terminal:(\d+)/);return t?parseInt(t[1],10):0}),r=e.length>0?Math.max(...e)+1:1;n={...n,metadata:{...n.metadata,terminalIndex:r}}}let s=d(n.type,n.metadata);if(n.type===`chat`&&n.metadata?.sessionId){let i=n.metadata.sessionId;for(let n of Object.values(t().panels)){let t=n.tabs.find(e=>e.type===`chat`&&e.metadata?.sessionId===i);if(t)return e(e=>({focusedPanelId:n.id,panels:{...e.panels,[n.id]:{...n,tabs:K(n.tabs,t.id),activeTabId:t.id,tabHistory:G(n.tabHistory,t.id)}}})),r(),t.id}}if(H.has(n.type))for(let n of Object.values(t().panels)){let t=n.tabs.find(e=>e.id===s);if(t)return e(e=>({focusedPanelId:n.id,panels:{...e.panels,[n.id]:{...n,tabs:K(n.tabs,t.id),activeTabId:t.id,tabHistory:G(n.tabHistory,t.id)}}})),r(),t.id}if(a)for(let n of t().grid.flat()){let i=t().panels[n];if(!i)continue;let a=i.tabs.find(e=>e.id===s||e.id.startsWith(`${s}@`));if(a)return e(e=>({focusedPanelId:i.id,panels:{...e.panels,[i.id]:{...i,tabs:K(i.tabs,a.id),activeTabId:a.id,tabHistory:G(i.tabHistory,a.id)}}})),r(),a.id}let c=t().panels[o],l=c.tabs.find(e=>e.id===s);if(l)return e(e=>({panels:{...e.panels,[o]:{...c,tabs:K(c.tabs,l.id),activeTabId:l.id,tabHistory:G(c.tabHistory,l.id)}}})),r(),l.id;let f=Object.values(t().panels).some(e=>e.id!==o&&e.tabs.some(e=>e.id===s))?`${s}@${o}`:s,p={...n,id:f,metadata:{...n.metadata,lastActiveAt:Date.now()}};return e(e=>{let t=e.panels[o];return{focusedPanelId:o,panels:{...e.panels,[o]:{...t,tabs:[...t.tabs,p],activeTabId:f,tabHistory:G(t.tabHistory,f)}}}}),r(),f},closeTab:(n,i)=>{let o=i?t().panels[i]:a(n);if(!o)return;let s=o.id;if(n.startsWith(`terminal:`)&&s!==`__dock__`){t().redockTab(n,s);return}if(n.startsWith(`terminal:`))try{localStorage.removeItem(`ppm:terminal-session:${n}`)}catch{}e(e=>{let t=e.panels[s],r=t.tabs.filter(e=>e.id!==n),i=t.tabHistory.filter(e=>e!==n),a=t.activeTabId;if(t.activeTabId===n){let e=i.length>0?i[i.length-1]:null;a=e&&r.some(t=>t.id===e)?e:r[r.length-1]?.id??null}let o=e.grid.flat().length;if(r.length===0&&o>1&&s!==`__dock__`){let{[s]:t,...n}=e.panels,r=c(e.grid,s);return{panels:n,grid:r,focusedPanelId:e.focusedPanelId===s?r.flat()[0]??Object.keys(n)[0]:e.focusedPanelId}}return{panels:{...e.panels,[s]:{...t,tabs:r,activeTabId:a,tabHistory:i}}}}),r(),s===`__dock__`&&M(t)===0&&t().setDockVisible(!1)},setActiveTab:(n,i)=>{let o=i?t().panels[i]:a(n);if(!o)return;let s=o.id;e(e=>{let t=e.panels[s];return{focusedPanelId:s,panels:{...e.panels,[s]:{...t,tabs:K(t.tabs,n),activeTabId:n,tabHistory:G(t.tabHistory,n)}}}}),r()},updateTab:(t,n)=>{let i=a(t);i&&(e(e=>({panels:{...e.panels,[i.id]:{...i,tabs:i.tabs.map(e=>e.id===t?{...e,...n}:e)}}})),r())},reorderTab:(n,i,a)=>{let o=t().panels[i];if(!o)return;let s=o.tabs.findIndex(e=>e.id===n);if(s===-1||s===a)return;let c=[...o.tabs],[l]=c.splice(s,1);c.splice(a,0,l),e(e=>({panels:{...e.panels,[i]:{...o,tabs:c}}})),r()},moveTab:(n,i,a,o)=>{if(i===a)return;let s=t().panels[i],l=t().panels[a];if(!s||!l)return;let u=s.tabs.find(e=>e.id===n);if(!u)return;let d=s.tabs.filter(e=>e.id!==n),f=s.tabHistory.filter(e=>e!==n),p=s.activeTabId===n?f[f.length-1]??d[d.length-1]?.id??null:s.activeTabId,m=[...l.tabs];o===void 0?m.push(u):m.splice(o,0,u),e(e=>{let t=e.grid.flat().length;if(d.length===0&&t>1&&i!==`__dock__`){let{[i]:t,...r}=e.panels;return{panels:{...r,[a]:{...l,tabs:m,activeTabId:n,tabHistory:G(l.tabHistory,n)}},grid:c(e.grid,i),focusedPanelId:a}}return{focusedPanelId:a,panels:{...e.panels,[i]:{...s,tabs:d,activeTabId:p,tabHistory:f},[a]:{...l,tabs:m,activeTabId:n,tabHistory:G(l.tabHistory,n)}}}}),r()},splitPanel:(n,i,a,u)=>{let{grid:d,panels:f}=t(),p=t().isMobile(),m=f[a];if(!m)return!1;let h=m.tabs.find(e=>e.id===i);if(!h)return!1;let g=l(d,u??a);if(!g)return!1;let _=n===`left`||n===`right`,v=n===`up`||n===`down`;if(_&&(d[g.row]?.length??0)>=s(p)||v&&d.length>=3)return!1;let y=o([h],h.id);y.tabHistory=[h.id];let b=m.tabs.filter(e=>e.id!==i),x=m.tabHistory.filter(e=>e!==i),S=m.activeTabId===i?x[x.length-1]??b[b.length-1]?.id??null:m.activeTabId,C;if(_)C=d.map((e,t)=>{if(t!==g.row)return e;let r=[...e],i=n===`right`?g.col+1:g.col;return r.splice(i,0,y.id),r});else{C=[...d];let e=n===`down`?g.row+1:g.row;C.splice(e,0,[y.id])}return e(e=>{let t=e.grid.flat().length,n={...e.panels,[y.id]:y};if(b.length===0&&t>1){let{[a]:e,...t}=n;n=t,C=c(C,a)}else n[a]={...m,tabs:b,activeTabId:S,tabHistory:x};return{panels:n,grid:C,focusedPanelId:y.id}}),r(),!0},closePanel:n=>{let{panels:i,grid:a}=t();if(a.flat().length<=1)return;let o=i[n];if(!o)return;l(a,n);let s=a.flat(),u=s.indexOf(n),d=u>0?s[u-1]:s[1],f=i[d];f&&(e(e=>{let{[n]:t,...r}=e.panels,i=[...f.tabs,...o.tabs],a=f.activeTabId??o.activeTabId;return{panels:{...r,[d]:{...f,tabs:i,activeTabId:a,tabHistory:[...f.tabHistory,...o.tabHistory]}},grid:c(e.grid,n),focusedPanelId:d}}),r())},getPanelForTab:e=>a(e),isMobile:()=>typeof window<`u`&&window.innerWidth<768}});export{u as a,l as i,n,E as o,o as r,s,J as t};
|