jinzd-ai-cli 0.4.29 → 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-FETMBU6E.js → chunk-EF6CCPWA.js} +1 -1
- package/dist/{chunk-TE6QE7FJ.js → chunk-LY2B3WHN.js} +1 -1
- package/dist/{chunk-SXL6H7XR.js → chunk-XI3XPJEV.js} +1 -1
- package/dist/{chunk-JHVH276O.js → chunk-ZYM2FGYT.js} +56 -6
- package/dist/{hub-RMUO6RN2.js → hub-ZGCGCIOP.js} +1 -1
- package/dist/index.js +20 -6
- package/dist/{run-tests-FNWRWJUI.js → run-tests-OUR565AK.js} +1 -1
- package/dist/{run-tests-GO7U333T.js → run-tests-SYGSF4K7.js} +1 -1
- package/dist/{server-DSICPNYM.js → server-WFEWHSQR.js} +4 -4
- package/dist/{task-orchestrator-LLCLCGR2.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";
|
|
@@ -1391,6 +1391,16 @@ function detectsCodeBlockPseudoCall(content) {
|
|
|
1391
1391
|
return CODE_BLOCK_PATTERNS.some((pattern) => pattern.test(content));
|
|
1392
1392
|
}
|
|
1393
1393
|
var DEEPSEEK_CODE_BLOCK_CORRECTION = "You wrote a code block in your response text, but you did NOT actually execute it. Code blocks in text are NOT executed by the system. You MUST use the function calling API to invoke the appropriate tool (e.g., mcp__postgres__query for SQL queries, bash for shell commands). Please call the correct tool NOW to execute the query/command.";
|
|
1394
|
+
var DEEPSEEK_ANTI_HALLUCINATION = `
|
|
1395
|
+
|
|
1396
|
+
[CRITICAL: Anti-Hallucination Enforcement \u2014 DeepSeek Specific]
|
|
1397
|
+
You have a known tendency to claim files were "saved" or "created" without actually calling write_file. This is UNACCEPTABLE.
|
|
1398
|
+
Rules you MUST follow:
|
|
1399
|
+
- NEVER type file content into your response text. ALL file content goes through write_file tool calls ONLY.
|
|
1400
|
+
- After calling write_file, do NOT describe the file content again in text \u2014 just confirm the tool call result.
|
|
1401
|
+
- When generating multiple files: call write_file for file 1 \u2192 call write_file for file 2 \u2192 ... \u2192 THEN summarize.
|
|
1402
|
+
- If you catch yourself writing markdown/code that should be a file, STOP and use write_file instead.
|
|
1403
|
+
- The system will detect and reject phantom claims. Each failed detection wastes a round. Be honest.`;
|
|
1394
1404
|
var DeepSeekProvider = class extends OpenAICompatibleProvider {
|
|
1395
1405
|
defaultBaseUrl = "https://api.deepseek.com/v1";
|
|
1396
1406
|
/** 禁用流式工具调用,确保 chatWithTools 覆写(代码块检测)生效 */
|
|
@@ -1427,7 +1437,11 @@ var DeepSeekProvider = class extends OpenAICompatibleProvider {
|
|
|
1427
1437
|
* 检测到后注入纠正消息强制重试一次。
|
|
1428
1438
|
*/
|
|
1429
1439
|
async chatWithTools(request, tools) {
|
|
1430
|
-
const
|
|
1440
|
+
const enhancedRequest = {
|
|
1441
|
+
...request,
|
|
1442
|
+
systemPrompt: (request.systemPrompt ?? "") + DEEPSEEK_ANTI_HALLUCINATION
|
|
1443
|
+
};
|
|
1444
|
+
const result = await super.chatWithTools(enhancedRequest, tools);
|
|
1431
1445
|
const hasBashTool = tools.some((t) => t.name === "bash");
|
|
1432
1446
|
if (hasBashTool && "content" in result && result.content && detectsCodeBlockPseudoCall(result.content)) {
|
|
1433
1447
|
process.stderr.write(
|
|
@@ -1545,8 +1559,20 @@ var HALLUCINATION_PATTERNS = [
|
|
|
1545
1559
|
// File written to / saved as(要求介词)
|
|
1546
1560
|
/生成完成[!!]/,
|
|
1547
1561
|
// 生成完成!
|
|
1548
|
-
/✅\s*(?:文件|已[生保写创]|第)\S*\.\w{1,5}
|
|
1562
|
+
/✅\s*(?:文件|已[生保写创]|第)\S*\.\w{1,5}/,
|
|
1549
1563
|
// ✅ 文件已保存 path.ext(要求文件扩展名)
|
|
1564
|
+
/文件已[成功]?创建/,
|
|
1565
|
+
// 文件已成功创建 / 文件已创建
|
|
1566
|
+
/教案已[成功]?[生创保写]/,
|
|
1567
|
+
// 教案已成功生成 / 教案已保存
|
|
1568
|
+
/已成功[保写创生]入?[::!!\s`'"]/,
|
|
1569
|
+
// 已成功保存 / 已成功写入 / 已成功创建
|
|
1570
|
+
/保存[到至]了?\s*[`'"]/,
|
|
1571
|
+
// 保存到了 `path` / 保存至 'path'
|
|
1572
|
+
/内容如下[::]/,
|
|
1573
|
+
// 内容如下:(后跟大段文件内容)
|
|
1574
|
+
/以下是.*(?:教案|文件|内容)[::]/
|
|
1575
|
+
// 以下是xx教案内容:(Kimi 常见模式)
|
|
1550
1576
|
];
|
|
1551
1577
|
function detectsHallucinatedFileOp(content) {
|
|
1552
1578
|
return HALLUCINATION_PATTERNS.some((pattern) => pattern.test(content));
|
|
@@ -1585,7 +1611,20 @@ When you need to create, write, or modify files, you MUST use the function calli
|
|
|
1585
1611
|
NEVER claim "file saved", "file created", "written to", etc. in your response text without actually calling the tool.
|
|
1586
1612
|
Describing file content in text without calling the tool = the file does not exist = task failure.
|
|
1587
1613
|
If multiple files need to be generated, you MUST call write_file separately for each file \u2014 do not skip any.
|
|
1588
|
-
Do NOT output fake "completion summaries" unless you have actually completed all file writes via tool_calls
|
|
1614
|
+
Do NOT output fake "completion summaries" unless you have actually completed all file writes via tool_calls.
|
|
1615
|
+
|
|
1616
|
+
CRITICAL \u2014 Batch file generation rules:
|
|
1617
|
+
1. You MUST call write_file once per file. There are NO shortcuts.
|
|
1618
|
+
2. After writing file N, immediately proceed to call write_file for file N+1. Do NOT stop to summarize.
|
|
1619
|
+
3. If you find yourself typing file content into your response text instead of into a write_file call, STOP and use the tool.
|
|
1620
|
+
4. Only produce a text summary AFTER all write_file calls have been made and returned success.
|
|
1621
|
+
5. The system compares every "file saved" claim against actual tool calls. Phantom claims trigger an automatic retry \u2014 do not waste rounds.`;
|
|
1622
|
+
function buildWriteRoundReminder(writtenCount) {
|
|
1623
|
+
return `
|
|
1624
|
+
|
|
1625
|
+
[Write Progress Reminder]
|
|
1626
|
+
You have successfully called write_file ${writtenCount} time(s) so far in this turn. If there are more files to write, call write_file NOW for the next file. Do NOT produce a text summary until ALL files have been written via tool calls.`;
|
|
1627
|
+
}
|
|
1589
1628
|
var HALLUCINATION_CORRECTION_MESSAGE = "You did NOT actually call the write_file tool \u2014 the file was NOT created! Please immediately use the write_file tool via the function calling API to perform the actual file write. Do NOT describe file content in text \u2014 you MUST invoke write_file through the tool_calls mechanism.";
|
|
1590
1629
|
function extractClaimedFilePaths(content) {
|
|
1591
1630
|
const paths = /* @__PURE__ */ new Set();
|
|
@@ -1683,7 +1722,16 @@ var KIMI_XML_REMINDER = `
|
|
|
1683
1722
|
[IMPORTANT: Tool Call Format Rules - Kimi Specific]
|
|
1684
1723
|
When calling any tool (write_file, bash, read_file, etc.), you MUST use the API's structured function calling interface exclusively.
|
|
1685
1724
|
NEVER output XML-formatted pseudo tool call tags (such as <write_file>, <bash>, <read_file>, etc.) in your reply text.
|
|
1686
|
-
XML tags in text will NOT be executed by the system, resulting in files not being written, commands not being run, and complete task failure
|
|
1725
|
+
XML tags in text will NOT be executed by the system, resulting in files not being written, commands not being run, and complete task failure.
|
|
1726
|
+
|
|
1727
|
+
[CRITICAL: Anti-Hallucination Enforcement]
|
|
1728
|
+
You have a known tendency to claim files were "saved" or "created" without actually calling write_file. This is UNACCEPTABLE.
|
|
1729
|
+
Rules you MUST follow:
|
|
1730
|
+
- NEVER type file content into your response text. ALL file content goes through write_file tool calls ONLY.
|
|
1731
|
+
- After calling write_file, do NOT describe the file content again in text \u2014 just confirm the tool call result.
|
|
1732
|
+
- When generating multiple files: call write_file for file 1 \u2192 call write_file for file 2 \u2192 ... \u2192 THEN summarize.
|
|
1733
|
+
- If you catch yourself writing markdown/code that should be a file, STOP and use write_file instead.
|
|
1734
|
+
- The system will detect and reject phantom claims. Each failed detection wastes a round. Be honest.`;
|
|
1687
1735
|
var KimiProvider = class extends OpenAICompatibleProvider {
|
|
1688
1736
|
defaultBaseUrl = "https://api.moonshot.ai/v1";
|
|
1689
1737
|
// 禁用流式工具调用:Kimi 的 XML 伪调用检测(方案 A)需要完整响应
|
|
@@ -3423,7 +3471,9 @@ export {
|
|
|
3423
3471
|
detectsHallucinatedFileOp,
|
|
3424
3472
|
hadPreviousWriteToolCalls,
|
|
3425
3473
|
TOOL_CALL_REMINDER,
|
|
3474
|
+
buildWriteRoundReminder,
|
|
3426
3475
|
HALLUCINATION_CORRECTION_MESSAGE,
|
|
3476
|
+
extractWrittenFilePaths,
|
|
3427
3477
|
findPhantomClaims,
|
|
3428
3478
|
buildPhantomCorrectionMessage,
|
|
3429
3479
|
ProviderRegistry,
|
|
@@ -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
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
SkillManager,
|
|
10
10
|
TOOL_CALL_REMINDER,
|
|
11
11
|
buildPhantomCorrectionMessage,
|
|
12
|
+
buildWriteRoundReminder,
|
|
12
13
|
clearDevState,
|
|
13
14
|
detectsHallucinatedFileOp,
|
|
15
|
+
extractWrittenFilePaths,
|
|
14
16
|
findPhantomClaims,
|
|
15
17
|
formatGitContextForPrompt,
|
|
16
18
|
getContentText,
|
|
@@ -22,7 +24,7 @@ import {
|
|
|
22
24
|
saveDevState,
|
|
23
25
|
sessionHasMeaningfulContent,
|
|
24
26
|
setupProxy
|
|
25
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-ZYM2FGYT.js";
|
|
26
28
|
import {
|
|
27
29
|
ToolExecutor,
|
|
28
30
|
ToolRegistry,
|
|
@@ -35,7 +37,7 @@ import {
|
|
|
35
37
|
spawnAgentContext,
|
|
36
38
|
theme,
|
|
37
39
|
undoStack
|
|
38
|
-
} from "./chunk-
|
|
40
|
+
} from "./chunk-XI3XPJEV.js";
|
|
39
41
|
import {
|
|
40
42
|
fileCheckpoints
|
|
41
43
|
} from "./chunk-4BKXL7SM.js";
|
|
@@ -59,7 +61,7 @@ import {
|
|
|
59
61
|
SKILLS_DIR_NAME,
|
|
60
62
|
VERSION,
|
|
61
63
|
buildUserIdentityPrompt
|
|
62
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-LY2B3WHN.js";
|
|
63
65
|
|
|
64
66
|
// src/index.ts
|
|
65
67
|
import { program } from "commander";
|
|
@@ -2085,7 +2087,7 @@ ${hint}` : "")
|
|
|
2085
2087
|
usage: "/test [command|filter]",
|
|
2086
2088
|
async execute(args, ctx) {
|
|
2087
2089
|
try {
|
|
2088
|
-
const { executeTests } = await import("./run-tests-
|
|
2090
|
+
const { executeTests } = await import("./run-tests-OUR565AK.js");
|
|
2089
2091
|
const argStr = args.join(" ").trim();
|
|
2090
2092
|
let testArgs = {};
|
|
2091
2093
|
if (argStr) {
|
|
@@ -4967,6 +4969,18 @@ You have a maximum of ${MAX_TOOL_ROUNDS} tool call rounds for this task. Plan ef
|
|
|
4967
4969
|
const reasoningContent = "reasoningContent" in result ? result.reasoningContent : void 0;
|
|
4968
4970
|
const newMsgs = provider.buildToolResultMessages(result.toolCalls, toolResults, reasoningContent);
|
|
4969
4971
|
extraMessages.push(...newMsgs);
|
|
4972
|
+
const thisRoundHadWrite = result.toolCalls.some(
|
|
4973
|
+
(tc) => tc.name === "write_file" || tc.name === "edit_file"
|
|
4974
|
+
);
|
|
4975
|
+
if (thisRoundHadWrite) {
|
|
4976
|
+
const totalWritten = extractWrittenFilePaths(extraMessages).length;
|
|
4977
|
+
if (totalWritten > 0) {
|
|
4978
|
+
extraMessages.push({
|
|
4979
|
+
role: "user",
|
|
4980
|
+
content: buildWriteRoundReminder(totalWritten)
|
|
4981
|
+
});
|
|
4982
|
+
}
|
|
4983
|
+
}
|
|
4970
4984
|
const allFree = result.toolCalls.every((tc) => FREE_ROUND_TOOLS.has(tc.name));
|
|
4971
4985
|
if (allFree) {
|
|
4972
4986
|
consecutiveFreeRounds++;
|
|
@@ -5383,7 +5397,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
5383
5397
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
5384
5398
|
process.exit(1);
|
|
5385
5399
|
}
|
|
5386
|
-
const { startWebServer } = await import("./server-
|
|
5400
|
+
const { startWebServer } = await import("./server-WFEWHSQR.js");
|
|
5387
5401
|
await startWebServer({ port, host: options.host });
|
|
5388
5402
|
});
|
|
5389
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) => {
|
|
@@ -5616,7 +5630,7 @@ program.command("hub [topic]").description("Start multi-agent hub (discuss / bra
|
|
|
5616
5630
|
}),
|
|
5617
5631
|
config.get("customProviders")
|
|
5618
5632
|
);
|
|
5619
|
-
const { startHub } = await import("./hub-
|
|
5633
|
+
const { startHub } = await import("./hub-ZGCGCIOP.js");
|
|
5620
5634
|
await startHub(
|
|
5621
5635
|
{
|
|
5622
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; }
|