jinzd-ai-cli 0.4.53 → 0.4.54
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/dist/{chunk-6I5FUNPR.js → chunk-6FYFVPVE.js} +2 -2
- package/dist/{chunk-W6AK76UM.js → chunk-FOFQAEU6.js} +1 -1
- package/dist/{chunk-YIMTDKUW.js → chunk-NP5KZVP6.js} +1 -1
- package/dist/{chunk-IXDGWT2Z.js → chunk-TAR67QTH.js} +1 -1
- package/dist/{hub-4DNFD6JK.js → hub-6V54V4O3.js} +1 -1
- package/dist/index.js +6 -6
- package/dist/{run-tests-NJQK4B43.js → run-tests-6G65OGSL.js} +1 -1
- package/dist/{run-tests-3NNL7Z2E.js → run-tests-P53FNUJY.js} +1 -1
- package/dist/{server-PFHWO3HL.js → server-BQHIMEBH.js} +5 -4
- package/dist/{task-orchestrator-C42TNHE6.js → task-orchestrator-TSY7CJE6.js} +2 -2
- package/dist/web/client/app.js +151 -42
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
ProviderNotFoundError,
|
|
8
8
|
RateLimitError,
|
|
9
9
|
schemaToJsonSchema
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-TAR67QTH.js";
|
|
11
11
|
import {
|
|
12
12
|
APP_NAME,
|
|
13
13
|
CONFIG_DIR_NAME,
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
MCP_TOOL_PREFIX,
|
|
21
21
|
PLUGINS_DIR_NAME,
|
|
22
22
|
VERSION
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-NP5KZVP6.js";
|
|
24
24
|
|
|
25
25
|
// src/config/config-manager.ts
|
|
26
26
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -385,7 +385,7 @@ ${content}`);
|
|
|
385
385
|
}
|
|
386
386
|
}
|
|
387
387
|
async function runTaskMode(config, providers, configManager, topic) {
|
|
388
|
-
const { TaskOrchestrator } = await import("./task-orchestrator-
|
|
388
|
+
const { TaskOrchestrator } = await import("./task-orchestrator-TSY7CJE6.js");
|
|
389
389
|
const orchestrator = new TaskOrchestrator(config, providers, configManager);
|
|
390
390
|
let interrupted = false;
|
|
391
391
|
const onSigint = () => {
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
saveDevState,
|
|
25
25
|
sessionHasMeaningfulContent,
|
|
26
26
|
setupProxy
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-6FYFVPVE.js";
|
|
28
28
|
import {
|
|
29
29
|
ToolExecutor,
|
|
30
30
|
ToolRegistry,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
spawnAgentContext,
|
|
39
39
|
theme,
|
|
40
40
|
undoStack
|
|
41
|
-
} from "./chunk-
|
|
41
|
+
} from "./chunk-TAR67QTH.js";
|
|
42
42
|
import {
|
|
43
43
|
fileCheckpoints
|
|
44
44
|
} from "./chunk-4BKXL7SM.js";
|
|
@@ -63,7 +63,7 @@ import {
|
|
|
63
63
|
SKILLS_DIR_NAME,
|
|
64
64
|
VERSION,
|
|
65
65
|
buildUserIdentityPrompt
|
|
66
|
-
} from "./chunk-
|
|
66
|
+
} from "./chunk-NP5KZVP6.js";
|
|
67
67
|
|
|
68
68
|
// src/index.ts
|
|
69
69
|
import { program } from "commander";
|
|
@@ -2106,7 +2106,7 @@ ${hint}` : "")
|
|
|
2106
2106
|
usage: "/test [command|filter]",
|
|
2107
2107
|
async execute(args, ctx) {
|
|
2108
2108
|
try {
|
|
2109
|
-
const { executeTests } = await import("./run-tests-
|
|
2109
|
+
const { executeTests } = await import("./run-tests-P53FNUJY.js");
|
|
2110
2110
|
const argStr = args.join(" ").trim();
|
|
2111
2111
|
let testArgs = {};
|
|
2112
2112
|
if (argStr) {
|
|
@@ -5493,7 +5493,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5493
5493
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5494
5494
|
process.exit(1);
|
|
5495
5495
|
}
|
|
5496
|
-
const { startWebServer } = await import("./server-
|
|
5496
|
+
const { startWebServer } = await import("./server-BQHIMEBH.js");
|
|
5497
5497
|
await startWebServer({ port, host: options.host });
|
|
5498
5498
|
});
|
|
5499
5499
|
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
|
|
@@ -5726,7 +5726,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
|
|
|
5726
5726
|
}),
|
|
5727
5727
|
config.get("customProviders")
|
|
5728
5728
|
);
|
|
5729
|
-
const { startHub } = await import("./hub-
|
|
5729
|
+
const { startHub } = await import("./hub-6V54V4O3.js");
|
|
5730
5730
|
await startHub(
|
|
5731
5731
|
{
|
|
5732
5732
|
topic: topic ?? "",
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
hadPreviousWriteToolCalls,
|
|
16
16
|
loadDevState,
|
|
17
17
|
setupProxy
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-6FYFVPVE.js";
|
|
19
19
|
import {
|
|
20
20
|
AuthManager
|
|
21
21
|
} from "./chunk-BYNY5JPB.js";
|
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
spawnAgentContext,
|
|
35
35
|
truncateOutput,
|
|
36
36
|
undoStack
|
|
37
|
-
} from "./chunk-
|
|
37
|
+
} from "./chunk-TAR67QTH.js";
|
|
38
38
|
import "./chunk-4BKXL7SM.js";
|
|
39
39
|
import {
|
|
40
40
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
SKILLS_DIR_NAME,
|
|
55
55
|
VERSION,
|
|
56
56
|
buildUserIdentityPrompt
|
|
57
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-NP5KZVP6.js";
|
|
58
58
|
|
|
59
59
|
// src/web/server.ts
|
|
60
60
|
import express from "express";
|
|
@@ -1691,7 +1691,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
1691
1691
|
case "test": {
|
|
1692
1692
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
1693
1693
|
try {
|
|
1694
|
-
const { executeTests } = await import("./run-tests-
|
|
1694
|
+
const { executeTests } = await import("./run-tests-P53FNUJY.js");
|
|
1695
1695
|
const argStr = args.join(" ").trim();
|
|
1696
1696
|
let testArgs = {};
|
|
1697
1697
|
if (argStr) {
|
|
@@ -2215,6 +2215,7 @@ Add .md files to create commands.` });
|
|
|
2215
2215
|
}));
|
|
2216
2216
|
this.send({
|
|
2217
2217
|
type: "session_messages",
|
|
2218
|
+
sessionId: session.id,
|
|
2218
2219
|
messages
|
|
2219
2220
|
});
|
|
2220
2221
|
}
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
getDangerLevel,
|
|
5
5
|
googleSearchContext,
|
|
6
6
|
truncateOutput
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-TAR67QTH.js";
|
|
8
8
|
import "./chunk-4BKXL7SM.js";
|
|
9
9
|
import {
|
|
10
10
|
SUBAGENT_ALLOWED_TOOLS
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-NP5KZVP6.js";
|
|
12
12
|
|
|
13
13
|
// src/hub/task-orchestrator.ts
|
|
14
14
|
import { createInterface } from "readline";
|
package/dist/web/client/app.js
CHANGED
|
@@ -182,7 +182,7 @@ function handleServerMessage(msg) {
|
|
|
182
182
|
case 'todo_update': handleTodoUpdate(msg.todos); break;
|
|
183
183
|
case 'status': handleStatus(msg); break;
|
|
184
184
|
case 'session_list': renderSessionList(msg.sessions); break;
|
|
185
|
-
case 'session_messages':renderSessionMessages(msg
|
|
185
|
+
case 'session_messages':renderSessionMessages(msg); break;
|
|
186
186
|
case 'tools_list': renderToolsList(msg); switchSidebarTab('tools'); break;
|
|
187
187
|
case 'export_data': handleExportData(msg); break;
|
|
188
188
|
case 'memory_content': handleMemoryContent(msg); break;
|
|
@@ -449,34 +449,68 @@ function handleTodoUpdate(todos) {
|
|
|
449
449
|
}
|
|
450
450
|
|
|
451
451
|
function handleStatus(msg) {
|
|
452
|
+
// Global UI (provider list, toolbar toggles) always reflects the latest
|
|
453
|
+
// server state regardless of which Tab the status belongs to — these are
|
|
454
|
+
// handler-scoped, not session-scoped.
|
|
452
455
|
providers = msg.providers || [];
|
|
453
456
|
updateProviderSelect(msg.provider);
|
|
454
457
|
updateModelSelect(msg.provider, msg.model);
|
|
455
458
|
|
|
456
|
-
|
|
457
|
-
|
|
459
|
+
// ── Route by sessionId ────────────────────────────────────────────
|
|
460
|
+
// Find the Tab this status payload belongs to. If the active Tab matches,
|
|
461
|
+
// we apply the full UI update. Otherwise we just refresh that Tab's stored
|
|
462
|
+
// metadata (title, token usage) without touching the live DOM — prevents
|
|
463
|
+
// "wrong content flashing on wrong tab" during fast tab switches.
|
|
464
|
+
let targetIdx = sessionTabs.findIndex(t => t.sessionId && t.sessionId === msg.sessionId);
|
|
465
|
+
|
|
466
|
+
// Fallback: if no tab owns this sessionId yet, bind it to the first tab
|
|
467
|
+
// that is still waiting for a `session new` response (FIFO matches the
|
|
468
|
+
// serial order of server command processing).
|
|
469
|
+
if (targetIdx < 0 && msg.sessionId) {
|
|
470
|
+
targetIdx = sessionTabs.findIndex(t => t.pendingBind);
|
|
471
|
+
if (targetIdx >= 0) {
|
|
472
|
+
sessionTabs[targetIdx].sessionId = msg.sessionId;
|
|
473
|
+
sessionTabs[targetIdx].pendingBind = false;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
458
476
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
477
|
+
if (targetIdx < 0) {
|
|
478
|
+
// Stale response — the tab that asked for it was closed before the
|
|
479
|
+
// server responded. Drop silently.
|
|
480
|
+
return;
|
|
462
481
|
}
|
|
463
482
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
el.classList.toggle('active', el.dataset.sessionId === msg.sessionId);
|
|
467
|
-
});
|
|
483
|
+
const targetTab = sessionTabs[targetIdx];
|
|
484
|
+
const isActiveTarget = targetIdx === activeTabIdx;
|
|
468
485
|
|
|
469
|
-
//
|
|
470
|
-
if (msg.
|
|
471
|
-
|
|
486
|
+
// Update tab-owned metadata (applies whether active or not)
|
|
487
|
+
if (msg.sessionTitle) targetTab.title = msg.sessionTitle;
|
|
488
|
+
else if (msg.sessionId && (!targetTab.title || targetTab.title === 'New Chat')) {
|
|
489
|
+
targetTab.title = msg.sessionId.slice(0, 8);
|
|
472
490
|
}
|
|
491
|
+
if (msg.tokenUsage) targetTab.tokenUsage = msg.tokenUsage;
|
|
473
492
|
|
|
474
|
-
|
|
475
|
-
|
|
493
|
+
if (isActiveTarget) {
|
|
494
|
+
// Active tab: full UI reflection
|
|
495
|
+
btnThink.classList.toggle('btn-active-toggle', msg.thinkingMode);
|
|
496
|
+
btnPlan.classList.toggle('btn-active-toggle', msg.planMode);
|
|
497
|
+
statusSession.textContent = `📋 ${msg.sessionId?.slice(0, 8) || '—'} (${msg.messageCount} msgs)`;
|
|
498
|
+
if (msg.tokenUsage) {
|
|
499
|
+
statusTokens.textContent = `📊 in: ${msg.tokenUsage.inputTokens} out: ${msg.tokenUsage.outputTokens}`;
|
|
500
|
+
}
|
|
501
|
+
sessionListEl.querySelectorAll('.session-item').forEach(el => {
|
|
502
|
+
el.classList.toggle('active', el.dataset.sessionId === msg.sessionId);
|
|
503
|
+
});
|
|
504
|
+
if (msg.sessionId) {
|
|
505
|
+
sessionStorage.setItem('aicli-active-session', msg.sessionId);
|
|
506
|
+
}
|
|
507
|
+
targetTab.isProcessing = processing;
|
|
508
|
+
const title = msg.sessionTitle || msg.sessionId?.slice(0, 8) || 'New Session';
|
|
509
|
+
document.title = `ai-cli — ${title}`;
|
|
510
|
+
}
|
|
476
511
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
document.title = `ai-cli — ${title}`;
|
|
512
|
+
renderTabBar();
|
|
513
|
+
saveTabState();
|
|
480
514
|
}
|
|
481
515
|
|
|
482
516
|
// ── Response helpers ───────────────────────────────────────────────
|
|
@@ -795,6 +829,7 @@ function loadSessionInTab(sessionId, title) {
|
|
|
795
829
|
// Load in current active tab
|
|
796
830
|
if (activeTabIdx >= 0 && activeTabIdx < sessionTabs.length) {
|
|
797
831
|
sessionTabs[activeTabIdx].sessionId = sessionId;
|
|
832
|
+
sessionTabs[activeTabIdx].pendingBind = false; // explicit load cancels any pending bind
|
|
798
833
|
if (title) sessionTabs[activeTabIdx].title = title;
|
|
799
834
|
renderTabBar();
|
|
800
835
|
saveTabState();
|
|
@@ -965,18 +1000,97 @@ function updateBatchBar() {
|
|
|
965
1000
|
}
|
|
966
1001
|
}
|
|
967
1002
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1003
|
+
/**
|
|
1004
|
+
* Render the `session_messages` payload for a session.
|
|
1005
|
+
* Routes to the correct UI tab based on payload.sessionId so that fast
|
|
1006
|
+
* tab-switching does not cause content to be applied to the wrong tab.
|
|
1007
|
+
*
|
|
1008
|
+
* msg = { type: 'session_messages', sessionId, messages: [...] }
|
|
1009
|
+
*
|
|
1010
|
+
* If the matched tab is active → render directly to the live DOM (and
|
|
1011
|
+
* snapshot into the tab cache). Otherwise build the HTML off-DOM and
|
|
1012
|
+
* write it into the tab's `messagesHtml` cache; the live DOM is left
|
|
1013
|
+
* untouched so the active tab's content never flashes wrong data.
|
|
1014
|
+
*/
|
|
1015
|
+
function renderSessionMessages(msg) {
|
|
1016
|
+
// Back-compat: if called with a bare array (legacy), treat as active-tab apply
|
|
1017
|
+
const messages = Array.isArray(msg) ? msg : msg.messages;
|
|
1018
|
+
const sessionId = Array.isArray(msg) ? null : msg.sessionId;
|
|
1019
|
+
|
|
1020
|
+
// Locate the target tab
|
|
1021
|
+
let targetIdx = -1;
|
|
1022
|
+
if (sessionId) {
|
|
1023
|
+
targetIdx = sessionTabs.findIndex(t => t.sessionId === sessionId);
|
|
1024
|
+
}
|
|
1025
|
+
if (targetIdx < 0) {
|
|
1026
|
+
// No explicit match — fall back to active tab (legacy behavior)
|
|
1027
|
+
targetIdx = activeTabIdx;
|
|
1028
|
+
}
|
|
1029
|
+
if (targetIdx < 0 || targetIdx >= sessionTabs.length) return;
|
|
1030
|
+
|
|
1031
|
+
const isActiveTarget = targetIdx === activeTabIdx;
|
|
1032
|
+
|
|
1033
|
+
if (isActiveTarget) {
|
|
1034
|
+
// Active tab: paint directly into the live DOM (preserves any in-flight
|
|
1035
|
+
// streaming helpers that rely on messagesEl)
|
|
1036
|
+
messagesEl.innerHTML = '';
|
|
1037
|
+
for (const m of messages) {
|
|
1038
|
+
if (m.role === 'user') {
|
|
1039
|
+
addUserMessage(m.content);
|
|
1040
|
+
} else if (m.role === 'assistant') {
|
|
1041
|
+
const el = createAssistantMessage();
|
|
1042
|
+
renderMarkdown(el, m.content);
|
|
1043
|
+
}
|
|
977
1044
|
}
|
|
1045
|
+
scrollToBottom();
|
|
1046
|
+
// Snapshot into cache so subsequent tab-snapshots see the latest content
|
|
1047
|
+
sessionTabs[targetIdx].messagesHtml = messagesEl.innerHTML;
|
|
1048
|
+
} else {
|
|
1049
|
+
// Inactive tab: build HTML off-DOM, store in tab cache, do NOT touch
|
|
1050
|
+
// the live DOM. When the user eventually switches to this tab,
|
|
1051
|
+
// restoreTab() will paint the cached HTML.
|
|
1052
|
+
sessionTabs[targetIdx].messagesHtml = buildMessagesHtmlOffDom(messages);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
/**
|
|
1057
|
+
* Build the HTML string for a list of messages without affecting the
|
|
1058
|
+
* currently visible DOM. Uses a save-clear-render-capture-restore cycle
|
|
1059
|
+
* on the existing messagesEl so we can re-use addUserMessage /
|
|
1060
|
+
* createAssistantMessage (both of which reference messagesEl directly).
|
|
1061
|
+
* The round-trip is synchronous so the user never sees it.
|
|
1062
|
+
*/
|
|
1063
|
+
function buildMessagesHtmlOffDom(messages) {
|
|
1064
|
+
const savedHtml = messagesEl.innerHTML;
|
|
1065
|
+
const savedScroll = chatArea.scrollTop;
|
|
1066
|
+
// Save streaming pointers so we don't orphan them
|
|
1067
|
+
const savedAssistantEl = currentAssistantEl;
|
|
1068
|
+
const savedAssistantContent = currentAssistantContent;
|
|
1069
|
+
const savedThinkingEl = currentThinkingEl;
|
|
1070
|
+
const savedThinkingContent = currentThinkingContent;
|
|
1071
|
+
try {
|
|
1072
|
+
messagesEl.innerHTML = '';
|
|
1073
|
+
currentAssistantEl = null;
|
|
1074
|
+
currentAssistantContent = '';
|
|
1075
|
+
currentThinkingEl = null;
|
|
1076
|
+
currentThinkingContent = '';
|
|
1077
|
+
for (const m of messages) {
|
|
1078
|
+
if (m.role === 'user') {
|
|
1079
|
+
addUserMessage(m.content);
|
|
1080
|
+
} else if (m.role === 'assistant') {
|
|
1081
|
+
const el = createAssistantMessage();
|
|
1082
|
+
renderMarkdown(el, m.content);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
return messagesEl.innerHTML;
|
|
1086
|
+
} finally {
|
|
1087
|
+
messagesEl.innerHTML = savedHtml;
|
|
1088
|
+
chatArea.scrollTop = savedScroll;
|
|
1089
|
+
currentAssistantEl = savedAssistantEl;
|
|
1090
|
+
currentAssistantContent = savedAssistantContent;
|
|
1091
|
+
currentThinkingEl = savedThinkingEl;
|
|
1092
|
+
currentThinkingContent = savedThinkingContent;
|
|
978
1093
|
}
|
|
979
|
-
scrollToBottom();
|
|
980
1094
|
}
|
|
981
1095
|
|
|
982
1096
|
// New session button — opens in a new tab
|
|
@@ -1867,6 +1981,10 @@ function addTab(sessionId, title) {
|
|
|
1867
1981
|
scrollPos: 0,
|
|
1868
1982
|
tokenUsage: { inputTokens: 0, outputTokens: 0 },
|
|
1869
1983
|
isProcessing: false,
|
|
1984
|
+
// pendingBind: this tab is waiting for the server to assign a sessionId.
|
|
1985
|
+
// Set when `session new` is dispatched; cleared once `status` arrives with
|
|
1986
|
+
// the assigned id. Prevents duplicate `session new` requests on fast re-switch.
|
|
1987
|
+
pendingBind: !sessionId,
|
|
1870
1988
|
_currentAssistantContent: '',
|
|
1871
1989
|
};
|
|
1872
1990
|
sessionTabs.push(tab);
|
|
@@ -1908,7 +2026,11 @@ function switchToTab(index) {
|
|
|
1908
2026
|
// Tell server to switch session
|
|
1909
2027
|
if (tab.sessionId) {
|
|
1910
2028
|
send({ type: 'command', name: 'session', args: ['load', tab.sessionId] });
|
|
1911
|
-
} else {
|
|
2029
|
+
} else if (!tab.pendingBind) {
|
|
2030
|
+
// No id yet AND no outstanding `session new` in flight — request one.
|
|
2031
|
+
// If pendingBind is true, the original `session new` response is still
|
|
2032
|
+
// inbound; don't send another request or we'd create a dangling session.
|
|
2033
|
+
tab.pendingBind = true;
|
|
1912
2034
|
send({ type: 'command', name: 'session', args: ['new'] });
|
|
1913
2035
|
}
|
|
1914
2036
|
|
|
@@ -1942,19 +2064,6 @@ function findTabBySessionId(sessionId) {
|
|
|
1942
2064
|
return sessionTabs.findIndex(t => t.sessionId === sessionId);
|
|
1943
2065
|
}
|
|
1944
2066
|
|
|
1945
|
-
/** Update the active tab's metadata from a status message */
|
|
1946
|
-
function updateActiveTabFromStatus(msg) {
|
|
1947
|
-
if (activeTabIdx < 0 || activeTabIdx >= sessionTabs.length) return;
|
|
1948
|
-
const tab = sessionTabs[activeTabIdx];
|
|
1949
|
-
if (msg.sessionId) tab.sessionId = msg.sessionId;
|
|
1950
|
-
if (msg.sessionTitle) tab.title = msg.sessionTitle;
|
|
1951
|
-
else if (msg.sessionId && !tab.title) tab.title = msg.sessionId.slice(0, 8);
|
|
1952
|
-
tab.isProcessing = processing;
|
|
1953
|
-
if (msg.tokenUsage) tab.tokenUsage = msg.tokenUsage;
|
|
1954
|
-
renderTabBar();
|
|
1955
|
-
saveTabState();
|
|
1956
|
-
}
|
|
1957
|
-
|
|
1958
2067
|
/** Persist tab state to sessionStorage for page reload */
|
|
1959
2068
|
function saveTabState() {
|
|
1960
2069
|
try {
|