jinzd-ai-cli 0.4.30 → 0.4.31
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-4TUVB27S.js → chunk-EF6CCPWA.js} +1 -1
- package/dist/{chunk-4WGZEAGJ.js → chunk-LY2B3WHN.js} +1 -1
- package/dist/{chunk-4EOFMJCR.js → chunk-XI3XPJEV.js} +1 -1
- package/dist/{chunk-HNS4LEHL.js → chunk-ZYM2FGYT.js} +2 -2
- package/dist/{hub-FKLTP6IO.js → hub-ZGCGCIOP.js} +1 -1
- package/dist/index.js +6 -6
- package/dist/{run-tests-NBCWIU4B.js → run-tests-OUR565AK.js} +1 -1
- package/dist/{run-tests-7LKUY2QO.js → run-tests-SYGSF4K7.js} +1 -1
- package/dist/{server-67B52TFH.js → server-WFEWHSQR.js} +4 -4
- package/dist/{task-orchestrator-VUAHWAAP.js → task-orchestrator-JOQMPOOR.js} +2 -2
- package/dist/web/client/app.js +264 -10
- package/dist/web/client/index.html +7 -0
- package/dist/web/client/style.css +113 -0
- 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-XI3XPJEV.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-LY2B3WHN.js";
|
|
24
24
|
|
|
25
25
|
// src/config/config-manager.ts
|
|
26
26
|
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
@@ -387,7 +387,7 @@ ${content}`);
|
|
|
387
387
|
}
|
|
388
388
|
}
|
|
389
389
|
async function runTaskMode(config, providers, configManager, topic) {
|
|
390
|
-
const { TaskOrchestrator } = await import("./task-orchestrator-
|
|
390
|
+
const { TaskOrchestrator } = await import("./task-orchestrator-JOQMPOOR.js");
|
|
391
391
|
const orchestrator = new TaskOrchestrator(config, providers, configManager);
|
|
392
392
|
let interrupted = false;
|
|
393
393
|
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-ZYM2FGYT.js";
|
|
28
28
|
import {
|
|
29
29
|
ToolExecutor,
|
|
30
30
|
ToolRegistry,
|
|
@@ -37,7 +37,7 @@ import {
|
|
|
37
37
|
spawnAgentContext,
|
|
38
38
|
theme,
|
|
39
39
|
undoStack
|
|
40
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-XI3XPJEV.js";
|
|
41
41
|
import {
|
|
42
42
|
fileCheckpoints
|
|
43
43
|
} from "./chunk-4BKXL7SM.js";
|
|
@@ -61,7 +61,7 @@ import {
|
|
|
61
61
|
SKILLS_DIR_NAME,
|
|
62
62
|
VERSION,
|
|
63
63
|
buildUserIdentityPrompt
|
|
64
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-LY2B3WHN.js";
|
|
65
65
|
|
|
66
66
|
// src/index.ts
|
|
67
67
|
import { program } from "commander";
|
|
@@ -2087,7 +2087,7 @@ ${hint}` : "")
|
|
|
2087
2087
|
usage: "/test [command|filter]",
|
|
2088
2088
|
async execute(args, ctx) {
|
|
2089
2089
|
try {
|
|
2090
|
-
const { executeTests } = await import("./run-tests-
|
|
2090
|
+
const { executeTests } = await import("./run-tests-OUR565AK.js");
|
|
2091
2091
|
const argStr = args.join(" ").trim();
|
|
2092
2092
|
let testArgs = {};
|
|
2093
2093
|
if (argStr) {
|
|
@@ -5397,7 +5397,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5397
5397
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5398
5398
|
process.exit(1);
|
|
5399
5399
|
}
|
|
5400
|
-
const { startWebServer } = await import("./server-
|
|
5400
|
+
const { startWebServer } = await import("./server-WFEWHSQR.js");
|
|
5401
5401
|
await startWebServer({ port, host: options.host });
|
|
5402
5402
|
});
|
|
5403
5403
|
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | migrate <name>)").action(async (action, username) => {
|
|
@@ -5630,7 +5630,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
|
|
|
5630
5630
|
}),
|
|
5631
5631
|
config.get("customProviders")
|
|
5632
5632
|
);
|
|
5633
|
-
const { startHub } = await import("./hub-
|
|
5633
|
+
const { startHub } = await import("./hub-ZGCGCIOP.js");
|
|
5634
5634
|
await startHub(
|
|
5635
5635
|
{
|
|
5636
5636
|
topic: topic ?? "",
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
hadPreviousWriteToolCalls,
|
|
16
16
|
loadDevState,
|
|
17
17
|
setupProxy
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-ZYM2FGYT.js";
|
|
19
19
|
import {
|
|
20
20
|
AuthManager
|
|
21
21
|
} from "./chunk-BYNY5JPB.js";
|
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
spawnAgentContext,
|
|
34
34
|
truncateOutput,
|
|
35
35
|
undoStack
|
|
36
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-XI3XPJEV.js";
|
|
37
37
|
import "./chunk-4BKXL7SM.js";
|
|
38
38
|
import {
|
|
39
39
|
AGENTIC_BEHAVIOR_GUIDELINE,
|
|
@@ -52,7 +52,7 @@ import {
|
|
|
52
52
|
SKILLS_DIR_NAME,
|
|
53
53
|
VERSION,
|
|
54
54
|
buildUserIdentityPrompt
|
|
55
|
-
} from "./chunk-
|
|
55
|
+
} from "./chunk-LY2B3WHN.js";
|
|
56
56
|
|
|
57
57
|
// src/web/server.ts
|
|
58
58
|
import express from "express";
|
|
@@ -1606,7 +1606,7 @@ ${undoResults.map((r) => ` \u2022 ${r}`).join("\n")}` });
|
|
|
1606
1606
|
case "test": {
|
|
1607
1607
|
this.send({ type: "info", message: "\u{1F9EA} Running tests..." });
|
|
1608
1608
|
try {
|
|
1609
|
-
const { executeTests } = await import("./run-tests-
|
|
1609
|
+
const { executeTests } = await import("./run-tests-OUR565AK.js");
|
|
1610
1610
|
const argStr = args.join(" ").trim();
|
|
1611
1611
|
let testArgs = {};
|
|
1612
1612
|
if (argStr) {
|
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
getDangerLevel,
|
|
5
5
|
googleSearchContext,
|
|
6
6
|
truncateOutput
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XI3XPJEV.js";
|
|
8
8
|
import "./chunk-4BKXL7SM.js";
|
|
9
9
|
import {
|
|
10
10
|
SUBAGENT_ALLOWED_TOOLS
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-LY2B3WHN.js";
|
|
12
12
|
|
|
13
13
|
// src/hub/task-orchestrator.ts
|
|
14
14
|
import { createInterface } from "readline";
|
package/dist/web/client/app.js
CHANGED
|
@@ -22,6 +22,12 @@ let historyIndex = -1; // -1 = not browsing history
|
|
|
22
22
|
let savedInputDraft = ''; // Saved current input when entering history mode
|
|
23
23
|
let toolTimers = new Map(); // callId → { startTime, intervalId }
|
|
24
24
|
|
|
25
|
+
// ── Multi-Tab state (P2-1) ────────────────────────────────────────
|
|
26
|
+
// Each "tab" represents an open session within the single page.
|
|
27
|
+
// Only one tab is active at a time; others store a DOM snapshot.
|
|
28
|
+
let sessionTabs = []; // [{ id, sessionId, title, messagesHtml, scrollPos, tokenUsage, isProcessing }]
|
|
29
|
+
let activeTabIdx = -1; // Index into sessionTabs
|
|
30
|
+
|
|
25
31
|
// ── DOM refs ───────────────────────────────────────────────────────
|
|
26
32
|
|
|
27
33
|
const messagesEl = document.getElementById('messages');
|
|
@@ -46,6 +52,8 @@ const sessionSearchInput = document.getElementById('session-search');
|
|
|
46
52
|
const toolsSearchInput = document.getElementById('tools-search');
|
|
47
53
|
let cachedSessions = [];
|
|
48
54
|
let cachedToolsData = null;
|
|
55
|
+
const sessionTabsListEl = document.getElementById('session-tabs-list');
|
|
56
|
+
const btnAddTab = document.getElementById('btn-add-tab');
|
|
49
57
|
|
|
50
58
|
// ── Configure marked.js ────────────────────────────────────────────
|
|
51
59
|
|
|
@@ -103,10 +111,25 @@ function connect() {
|
|
|
103
111
|
setProcessing(false);
|
|
104
112
|
addInfoMessage('⚡ Reconnected — previous generation may have been interrupted.');
|
|
105
113
|
}
|
|
106
|
-
// Restore
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
// Restore tabs on reconnect / page reload
|
|
115
|
+
if (sessionTabs.length === 0) {
|
|
116
|
+
// First connect — try to restore tabs from sessionStorage
|
|
117
|
+
const restored = restoreTabState();
|
|
118
|
+
if (restored && sessionTabs[activeTabIdx]?.sessionId) {
|
|
119
|
+
send({ type: 'command', name: 'session', args: ['load', sessionTabs[activeTabIdx].sessionId] });
|
|
120
|
+
} else if (!restored) {
|
|
121
|
+
// Legacy fallback: check old single-session storage
|
|
122
|
+
const savedSession = sessionStorage.getItem('aicli-active-session');
|
|
123
|
+
if (savedSession && !processing) {
|
|
124
|
+
addTab(savedSession, null);
|
|
125
|
+
} else {
|
|
126
|
+
// Create initial tab
|
|
127
|
+
addTab();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (sessionTabs[activeTabIdx]?.sessionId) {
|
|
131
|
+
// Reconnecting with existing tabs — reload active session
|
|
132
|
+
send({ type: 'command', name: 'session', args: ['load', sessionTabs[activeTabIdx].sessionId] });
|
|
110
133
|
}
|
|
111
134
|
};
|
|
112
135
|
|
|
@@ -448,6 +471,9 @@ function handleStatus(msg) {
|
|
|
448
471
|
sessionStorage.setItem('aicli-active-session', msg.sessionId);
|
|
449
472
|
}
|
|
450
473
|
|
|
474
|
+
// Update multi-tab state
|
|
475
|
+
updateActiveTabFromStatus(msg);
|
|
476
|
+
|
|
451
477
|
// Update browser tab title to reflect current session
|
|
452
478
|
const title = msg.sessionTitle || msg.sessionId?.slice(0, 8) || 'New Session';
|
|
453
479
|
document.title = `ai-cli — ${title}`;
|
|
@@ -603,6 +629,11 @@ function scrollToBottom() {
|
|
|
603
629
|
|
|
604
630
|
function setProcessing(value) {
|
|
605
631
|
processing = value;
|
|
632
|
+
// Sync tab processing indicator
|
|
633
|
+
if (activeTabIdx >= 0 && activeTabIdx < sessionTabs.length) {
|
|
634
|
+
sessionTabs[activeTabIdx].isProcessing = value;
|
|
635
|
+
renderTabBar();
|
|
636
|
+
}
|
|
606
637
|
// During processing: show BOTH send (as interjection) and stop buttons
|
|
607
638
|
// Send button changes style to indicate interjection mode
|
|
608
639
|
btnStop.classList.toggle('hidden', !value);
|
|
@@ -753,6 +784,24 @@ function renderSessionList(sessions) {
|
|
|
753
784
|
renderFilteredSessions(sessionSearchInput?.value || '');
|
|
754
785
|
}
|
|
755
786
|
|
|
787
|
+
/** Load a session — switch to its tab if already open, otherwise load in active tab */
|
|
788
|
+
function loadSessionInTab(sessionId, title) {
|
|
789
|
+
// If this session is already open in a tab, just switch to it
|
|
790
|
+
const existingIdx = findTabBySessionId(sessionId);
|
|
791
|
+
if (existingIdx >= 0 && existingIdx !== activeTabIdx) {
|
|
792
|
+
switchToTab(existingIdx);
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
// Load in current active tab
|
|
796
|
+
if (activeTabIdx >= 0 && activeTabIdx < sessionTabs.length) {
|
|
797
|
+
sessionTabs[activeTabIdx].sessionId = sessionId;
|
|
798
|
+
if (title) sessionTabs[activeTabIdx].title = title;
|
|
799
|
+
renderTabBar();
|
|
800
|
+
saveTabState();
|
|
801
|
+
}
|
|
802
|
+
send({ type: 'command', name: 'session', args: ['load', sessionId] });
|
|
803
|
+
}
|
|
804
|
+
|
|
756
805
|
let batchSelectMode = false;
|
|
757
806
|
const batchSelectedIds = new Set();
|
|
758
807
|
|
|
@@ -799,14 +848,14 @@ function renderFilteredSessions(filter) {
|
|
|
799
848
|
clickTimer = setTimeout(() => {
|
|
800
849
|
clickTimer = null;
|
|
801
850
|
const id = el.dataset.sessionId;
|
|
802
|
-
if (id)
|
|
851
|
+
if (id) loadSessionInTab(id, el.querySelector('.session-title')?.textContent);
|
|
803
852
|
}, 300);
|
|
804
853
|
return;
|
|
805
854
|
}
|
|
806
855
|
// Clicking elsewhere on the item — load immediately
|
|
807
856
|
const id = el.dataset.sessionId;
|
|
808
857
|
if (!id) return;
|
|
809
|
-
|
|
858
|
+
loadSessionInTab(id, el.querySelector('.session-title')?.textContent);
|
|
810
859
|
});
|
|
811
860
|
|
|
812
861
|
if (!batchSelectMode) {
|
|
@@ -930,11 +979,9 @@ function renderSessionMessages(messages) {
|
|
|
930
979
|
scrollToBottom();
|
|
931
980
|
}
|
|
932
981
|
|
|
933
|
-
// New session button
|
|
982
|
+
// New session button — opens in a new tab
|
|
934
983
|
btnNewSession.addEventListener('click', () => {
|
|
935
|
-
|
|
936
|
-
// Clear chat area
|
|
937
|
-
messagesEl.innerHTML = '';
|
|
984
|
+
addTab();
|
|
938
985
|
});
|
|
939
986
|
|
|
940
987
|
// Request session list on connect
|
|
@@ -1744,6 +1791,213 @@ if (btnFileTreeRefresh) {
|
|
|
1744
1791
|
});
|
|
1745
1792
|
}
|
|
1746
1793
|
|
|
1794
|
+
// ── Multi-Tab Management (P2-1) ───────────────────────────────────
|
|
1795
|
+
|
|
1796
|
+
function generateTabId() {
|
|
1797
|
+
return 'stab-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/** Save current active tab's DOM state (messages, scroll, streaming state) */
|
|
1801
|
+
function snapshotActiveTab() {
|
|
1802
|
+
if (activeTabIdx < 0 || activeTabIdx >= sessionTabs.length) return;
|
|
1803
|
+
const tab = sessionTabs[activeTabIdx];
|
|
1804
|
+
tab.messagesHtml = messagesEl.innerHTML;
|
|
1805
|
+
tab.scrollPos = chatArea.scrollTop;
|
|
1806
|
+
tab.isProcessing = processing;
|
|
1807
|
+
// Save streaming state so we don't lose partial content
|
|
1808
|
+
tab._currentAssistantContent = currentAssistantContent;
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** Restore a tab's DOM state */
|
|
1812
|
+
function restoreTab(index) {
|
|
1813
|
+
const tab = sessionTabs[index];
|
|
1814
|
+
if (!tab) return;
|
|
1815
|
+
messagesEl.innerHTML = tab.messagesHtml || '';
|
|
1816
|
+
chatArea.scrollTop = tab.scrollPos || 0;
|
|
1817
|
+
// Reset streaming state for the restored tab
|
|
1818
|
+
currentAssistantEl = null;
|
|
1819
|
+
currentAssistantContent = '';
|
|
1820
|
+
currentThinkingEl = null;
|
|
1821
|
+
currentThinkingContent = '';
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
/** Render the tab bar UI */
|
|
1825
|
+
function renderTabBar() {
|
|
1826
|
+
if (!sessionTabsListEl) return;
|
|
1827
|
+
sessionTabsListEl.innerHTML = sessionTabs.map((tab, i) => {
|
|
1828
|
+
const active = i === activeTabIdx ? ' active' : '';
|
|
1829
|
+
const title = tab.title || 'New Chat';
|
|
1830
|
+
const processingDot = tab.isProcessing ? '<span class="tab-processing"></span>' : '';
|
|
1831
|
+
return `<div class="session-tab-item${active}" data-tab-index="${i}" title="${escapeHtml(title)}">
|
|
1832
|
+
${processingDot}<span class="tab-title">${escapeHtml(title)}</span>
|
|
1833
|
+
<span class="tab-close" data-tab-close="${i}">×</span>
|
|
1834
|
+
</div>`;
|
|
1835
|
+
}).join('');
|
|
1836
|
+
|
|
1837
|
+
// Click handlers
|
|
1838
|
+
sessionTabsListEl.querySelectorAll('.session-tab-item').forEach(el => {
|
|
1839
|
+
el.addEventListener('click', (e) => {
|
|
1840
|
+
if (e.target.closest('.tab-close')) return;
|
|
1841
|
+
const idx = parseInt(el.dataset.tabIndex);
|
|
1842
|
+
if (idx !== activeTabIdx) switchToTab(idx);
|
|
1843
|
+
});
|
|
1844
|
+
});
|
|
1845
|
+
sessionTabsListEl.querySelectorAll('.tab-close').forEach(btn => {
|
|
1846
|
+
btn.addEventListener('click', (e) => {
|
|
1847
|
+
e.stopPropagation();
|
|
1848
|
+
closeTab(parseInt(btn.dataset.tabClose));
|
|
1849
|
+
});
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
// Scroll active tab into view
|
|
1853
|
+
const activeEl = sessionTabsListEl.querySelector('.session-tab-item.active');
|
|
1854
|
+
if (activeEl) activeEl.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
/** Add a new tab. If sessionId provided, load that session; otherwise create new. */
|
|
1858
|
+
function addTab(sessionId, title) {
|
|
1859
|
+
// Snapshot current tab before adding
|
|
1860
|
+
snapshotActiveTab();
|
|
1861
|
+
|
|
1862
|
+
const tab = {
|
|
1863
|
+
id: generateTabId(),
|
|
1864
|
+
sessionId: sessionId || null,
|
|
1865
|
+
title: title || 'New Chat',
|
|
1866
|
+
messagesHtml: '',
|
|
1867
|
+
scrollPos: 0,
|
|
1868
|
+
tokenUsage: { inputTokens: 0, outputTokens: 0 },
|
|
1869
|
+
isProcessing: false,
|
|
1870
|
+
_currentAssistantContent: '',
|
|
1871
|
+
};
|
|
1872
|
+
sessionTabs.push(tab);
|
|
1873
|
+
activeTabIdx = sessionTabs.length - 1;
|
|
1874
|
+
|
|
1875
|
+
// Clear chat area for the new tab
|
|
1876
|
+
messagesEl.innerHTML = '';
|
|
1877
|
+
currentAssistantEl = null;
|
|
1878
|
+
currentAssistantContent = '';
|
|
1879
|
+
currentThinkingEl = null;
|
|
1880
|
+
currentThinkingContent = '';
|
|
1881
|
+
chatArea.scrollTop = 0;
|
|
1882
|
+
|
|
1883
|
+
// Tell server to create or load session
|
|
1884
|
+
if (sessionId) {
|
|
1885
|
+
send({ type: 'command', name: 'session', args: ['load', sessionId] });
|
|
1886
|
+
} else {
|
|
1887
|
+
send({ type: 'command', name: 'session', args: ['new'] });
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
renderTabBar();
|
|
1891
|
+
saveTabState();
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
/** Switch to an existing tab by index */
|
|
1895
|
+
function switchToTab(index) {
|
|
1896
|
+
if (index === activeTabIdx || index < 0 || index >= sessionTabs.length) return;
|
|
1897
|
+
|
|
1898
|
+
// Snapshot current
|
|
1899
|
+
snapshotActiveTab();
|
|
1900
|
+
|
|
1901
|
+
activeTabIdx = index;
|
|
1902
|
+
const tab = sessionTabs[index];
|
|
1903
|
+
|
|
1904
|
+
// Restore DOM
|
|
1905
|
+
restoreTab(index);
|
|
1906
|
+
renderTabBar();
|
|
1907
|
+
|
|
1908
|
+
// Tell server to switch session
|
|
1909
|
+
if (tab.sessionId) {
|
|
1910
|
+
send({ type: 'command', name: 'session', args: ['load', tab.sessionId] });
|
|
1911
|
+
} else {
|
|
1912
|
+
send({ type: 'command', name: 'session', args: ['new'] });
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
saveTabState();
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/** Close a tab */
|
|
1919
|
+
function closeTab(index) {
|
|
1920
|
+
if (sessionTabs.length <= 1) return; // Must keep at least 1 tab
|
|
1921
|
+
|
|
1922
|
+
sessionTabs.splice(index, 1);
|
|
1923
|
+
|
|
1924
|
+
if (index === activeTabIdx) {
|
|
1925
|
+
// Closing active tab — switch to nearest
|
|
1926
|
+
activeTabIdx = Math.min(index, sessionTabs.length - 1);
|
|
1927
|
+
restoreTab(activeTabIdx);
|
|
1928
|
+
const tab = sessionTabs[activeTabIdx];
|
|
1929
|
+
if (tab.sessionId) {
|
|
1930
|
+
send({ type: 'command', name: 'session', args: ['load', tab.sessionId] });
|
|
1931
|
+
}
|
|
1932
|
+
} else if (index < activeTabIdx) {
|
|
1933
|
+
activeTabIdx--;
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
renderTabBar();
|
|
1937
|
+
saveTabState();
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
/** Find tab by sessionId, or -1 */
|
|
1941
|
+
function findTabBySessionId(sessionId) {
|
|
1942
|
+
return sessionTabs.findIndex(t => t.sessionId === sessionId);
|
|
1943
|
+
}
|
|
1944
|
+
|
|
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
|
+
/** Persist tab state to sessionStorage for page reload */
|
|
1959
|
+
function saveTabState() {
|
|
1960
|
+
try {
|
|
1961
|
+
const data = sessionTabs.map(t => ({
|
|
1962
|
+
id: t.id,
|
|
1963
|
+
sessionId: t.sessionId,
|
|
1964
|
+
title: t.title,
|
|
1965
|
+
}));
|
|
1966
|
+
sessionStorage.setItem('aicli-tabs', JSON.stringify({ tabs: data, activeIdx: activeTabIdx }));
|
|
1967
|
+
} catch {}
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/** Restore tabs from sessionStorage (called on page load) */
|
|
1971
|
+
function restoreTabState() {
|
|
1972
|
+
try {
|
|
1973
|
+
const raw = sessionStorage.getItem('aicli-tabs');
|
|
1974
|
+
if (!raw) return false;
|
|
1975
|
+
const { tabs, activeIdx } = JSON.parse(raw);
|
|
1976
|
+
if (!Array.isArray(tabs) || tabs.length === 0) return false;
|
|
1977
|
+
|
|
1978
|
+
sessionTabs = tabs.map(t => ({
|
|
1979
|
+
id: t.id || generateTabId(),
|
|
1980
|
+
sessionId: t.sessionId,
|
|
1981
|
+
title: t.title || 'New Chat',
|
|
1982
|
+
messagesHtml: '',
|
|
1983
|
+
scrollPos: 0,
|
|
1984
|
+
tokenUsage: { inputTokens: 0, outputTokens: 0 },
|
|
1985
|
+
isProcessing: false,
|
|
1986
|
+
_currentAssistantContent: '',
|
|
1987
|
+
}));
|
|
1988
|
+
activeTabIdx = Math.min(activeIdx || 0, sessionTabs.length - 1);
|
|
1989
|
+
renderTabBar();
|
|
1990
|
+
return true;
|
|
1991
|
+
} catch {
|
|
1992
|
+
return false;
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
// "+" button
|
|
1997
|
+
if (btnAddTab) {
|
|
1998
|
+
btnAddTab.addEventListener('click', () => addTab());
|
|
1999
|
+
}
|
|
2000
|
+
|
|
1747
2001
|
// ── Initialize ─────────────────────────────────────────────────────
|
|
1748
2002
|
|
|
1749
2003
|
// Restore theme + sync code highlight
|
|
@@ -153,6 +153,12 @@
|
|
|
153
153
|
<div id="sidebar-backdrop" class="sidebar-backdrop hidden" onclick="closeSidebar()"></div>
|
|
154
154
|
|
|
155
155
|
<!-- Chat Area -->
|
|
156
|
+
<div class="flex-1 flex flex-col overflow-hidden">
|
|
157
|
+
<!-- Session Tab Bar -->
|
|
158
|
+
<div id="session-tabs" class="session-tab-bar bg-base-200 border-b border-base-content/10 flex-shrink-0">
|
|
159
|
+
<div id="session-tabs-list" class="session-tabs-scroll"></div>
|
|
160
|
+
<button id="btn-add-tab" class="session-tab-add" title="New tab">+</button>
|
|
161
|
+
</div>
|
|
156
162
|
<main id="chat-area" class="flex-1 overflow-y-auto px-4 py-4 relative">
|
|
157
163
|
<!-- Round progress bar (sticky top, hidden by default) -->
|
|
158
164
|
<div id="round-progress" class="round-progress-bar hidden">
|
|
@@ -171,6 +177,7 @@
|
|
|
171
177
|
</div>
|
|
172
178
|
</div>
|
|
173
179
|
</main>
|
|
180
|
+
</div><!-- end chat column (tab bar + chat area) -->
|
|
174
181
|
|
|
175
182
|
</div>
|
|
176
183
|
|
|
@@ -510,6 +510,114 @@
|
|
|
510
510
|
font-family: 'Fira Code', 'JetBrains Mono', 'Consolas', monospace;
|
|
511
511
|
}
|
|
512
512
|
|
|
513
|
+
/* ── Session Tab Bar (P2-1: multi-tab in page) ──────── */
|
|
514
|
+
.session-tab-bar {
|
|
515
|
+
display: flex;
|
|
516
|
+
align-items: stretch;
|
|
517
|
+
min-height: 2.25rem;
|
|
518
|
+
padding: 0;
|
|
519
|
+
gap: 0;
|
|
520
|
+
overflow: hidden;
|
|
521
|
+
}
|
|
522
|
+
.session-tabs-scroll {
|
|
523
|
+
display: flex;
|
|
524
|
+
align-items: stretch;
|
|
525
|
+
overflow-x: auto;
|
|
526
|
+
overflow-y: hidden;
|
|
527
|
+
flex: 1;
|
|
528
|
+
min-width: 0;
|
|
529
|
+
scrollbar-width: thin;
|
|
530
|
+
scrollbar-color: oklch(var(--bc) / 0.15) transparent;
|
|
531
|
+
}
|
|
532
|
+
.session-tabs-scroll::-webkit-scrollbar { height: 3px; }
|
|
533
|
+
.session-tabs-scroll::-webkit-scrollbar-thumb { background: oklch(var(--bc) / 0.15); border-radius: 2px; }
|
|
534
|
+
|
|
535
|
+
.session-tab-item {
|
|
536
|
+
display: flex;
|
|
537
|
+
align-items: center;
|
|
538
|
+
gap: 0.35rem;
|
|
539
|
+
padding: 0 0.75rem;
|
|
540
|
+
font-size: 0.78rem;
|
|
541
|
+
white-space: nowrap;
|
|
542
|
+
cursor: pointer;
|
|
543
|
+
border-right: 1px solid oklch(var(--bc) / 0.08);
|
|
544
|
+
transition: background 0.12s;
|
|
545
|
+
min-width: 0;
|
|
546
|
+
max-width: 12rem;
|
|
547
|
+
flex-shrink: 0;
|
|
548
|
+
position: relative;
|
|
549
|
+
user-select: none;
|
|
550
|
+
}
|
|
551
|
+
.session-tab-item:hover {
|
|
552
|
+
background: oklch(var(--b3));
|
|
553
|
+
}
|
|
554
|
+
.session-tab-item.active {
|
|
555
|
+
background: oklch(var(--b1));
|
|
556
|
+
border-bottom: 2px solid oklch(var(--p));
|
|
557
|
+
color: oklch(var(--p));
|
|
558
|
+
font-weight: 600;
|
|
559
|
+
}
|
|
560
|
+
.session-tab-item:not(.active) {
|
|
561
|
+
border-bottom: 2px solid transparent;
|
|
562
|
+
}
|
|
563
|
+
.session-tab-item .tab-title {
|
|
564
|
+
overflow: hidden;
|
|
565
|
+
text-overflow: ellipsis;
|
|
566
|
+
white-space: nowrap;
|
|
567
|
+
min-width: 0;
|
|
568
|
+
flex: 1;
|
|
569
|
+
}
|
|
570
|
+
.session-tab-item .tab-processing {
|
|
571
|
+
width: 6px;
|
|
572
|
+
height: 6px;
|
|
573
|
+
border-radius: 50%;
|
|
574
|
+
background: oklch(var(--wa));
|
|
575
|
+
flex-shrink: 0;
|
|
576
|
+
animation: tab-pulse 1.2s infinite;
|
|
577
|
+
}
|
|
578
|
+
@keyframes tab-pulse {
|
|
579
|
+
0%, 100% { opacity: 0.4; }
|
|
580
|
+
50% { opacity: 1; }
|
|
581
|
+
}
|
|
582
|
+
.session-tab-item .tab-close {
|
|
583
|
+
opacity: 0;
|
|
584
|
+
font-size: 0.85rem;
|
|
585
|
+
line-height: 1;
|
|
586
|
+
padding: 0 0.15rem;
|
|
587
|
+
border-radius: 0.2rem;
|
|
588
|
+
cursor: pointer;
|
|
589
|
+
flex-shrink: 0;
|
|
590
|
+
transition: opacity 0.12s, background 0.12s;
|
|
591
|
+
}
|
|
592
|
+
.session-tab-item:hover .tab-close,
|
|
593
|
+
.session-tab-item.active .tab-close {
|
|
594
|
+
opacity: 0.5;
|
|
595
|
+
}
|
|
596
|
+
.session-tab-item .tab-close:hover {
|
|
597
|
+
opacity: 1;
|
|
598
|
+
background: oklch(var(--er) / 0.2);
|
|
599
|
+
color: oklch(var(--er));
|
|
600
|
+
}
|
|
601
|
+
.session-tab-add {
|
|
602
|
+
display: flex;
|
|
603
|
+
align-items: center;
|
|
604
|
+
justify-content: center;
|
|
605
|
+
width: 2rem;
|
|
606
|
+
flex-shrink: 0;
|
|
607
|
+
font-size: 1.1rem;
|
|
608
|
+
font-weight: 300;
|
|
609
|
+
cursor: pointer;
|
|
610
|
+
opacity: 0.4;
|
|
611
|
+
transition: opacity 0.12s, background 0.12s;
|
|
612
|
+
border: none;
|
|
613
|
+
background: transparent;
|
|
614
|
+
color: inherit;
|
|
615
|
+
}
|
|
616
|
+
.session-tab-add:hover {
|
|
617
|
+
opacity: 1;
|
|
618
|
+
background: oklch(var(--b3));
|
|
619
|
+
}
|
|
620
|
+
|
|
513
621
|
/* ── Round progress bar (sticky top of chat area) ──── */
|
|
514
622
|
.round-progress-bar {
|
|
515
623
|
position: sticky;
|
|
@@ -684,6 +792,11 @@ button, a, .session-item, .file-tree-row, .template-item, .tool-item, .mcp-serve
|
|
|
684
792
|
/* Sidebar: full width on small phones */
|
|
685
793
|
.sidebar.sidebar-open { width: min(85vw, 20rem); }
|
|
686
794
|
|
|
795
|
+
/* Tab bar: smaller on phone */
|
|
796
|
+
.session-tab-item { padding: 0 0.5rem; font-size: 0.72rem; max-width: 9rem; }
|
|
797
|
+
.session-tab-bar { min-height: 2rem; }
|
|
798
|
+
.session-tab-item .tab-close { opacity: 0.4; } /* always visible on mobile (no hover) */
|
|
799
|
+
|
|
687
800
|
/* Chat area */
|
|
688
801
|
#chat-area { padding: 0.75rem 0.5rem; }
|
|
689
802
|
.msg-assistant { padding: 0.75rem; font-size: 0.92rem; }
|