jinzd-ai-cli 0.4.206 → 0.4.207
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/electron-server.js +3 -6
- package/dist/index.js +75 -73
- package/dist/{server-LDPQ3DLK.js → server-L2MXWYF7.js} +3 -6
- package/dist/web/client/actions.js +84 -0
- package/dist/web/client/app.js +21 -203
- package/dist/web/client/index.html +27 -42
- package/dist/web/client/sw.js +1 -1
- package/dist/web/client/templates.js +188 -0
- package/package.json +1 -1
package/dist/electron-server.js
CHANGED
|
@@ -15731,12 +15731,9 @@ async function startWebServer(options = {}) {
|
|
|
15731
15731
|
"Content-Security-Policy",
|
|
15732
15732
|
[
|
|
15733
15733
|
"default-src 'self'",
|
|
15734
|
-
//
|
|
15735
|
-
//
|
|
15736
|
-
|
|
15737
|
-
// CSP violation in the console). localhost dev/admin tool — the
|
|
15738
|
-
// unsafe-inline XSS surface is acceptable under that threat model.
|
|
15739
|
-
"script-src 'self' 'unsafe-inline'",
|
|
15734
|
+
// Inline event handlers and startup scripts live in app.js, so script
|
|
15735
|
+
// execution stays locked to same-origin files only.
|
|
15736
|
+
"script-src 'self'",
|
|
15740
15737
|
"style-src 'self' 'unsafe-inline'",
|
|
15741
15738
|
"img-src 'self' data: blob:",
|
|
15742
15739
|
"font-src 'self' data:",
|
package/dist/index.js
CHANGED
|
@@ -144,8 +144,8 @@ import { program } from "commander";
|
|
|
144
144
|
|
|
145
145
|
// src/repl/repl.ts
|
|
146
146
|
import * as readline from "readline";
|
|
147
|
-
import { existsSync as
|
|
148
|
-
import { join as
|
|
147
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync4, mkdirSync as mkdirSync4 } from "fs";
|
|
148
|
+
import { join as join5, resolve as resolve2, extname as extname2, dirname as dirname3, basename as basename2 } from "path";
|
|
149
149
|
import chalk4 from "chalk";
|
|
150
150
|
|
|
151
151
|
// src/session/title-generator.ts
|
|
@@ -637,10 +637,10 @@ Error${typeName}: ${lines.join("\n")}
|
|
|
637
637
|
};
|
|
638
638
|
|
|
639
639
|
// src/repl/commands/index.ts
|
|
640
|
-
import { writeFileSync, mkdirSync as mkdirSync2, existsSync as
|
|
640
|
+
import { writeFileSync, mkdirSync as mkdirSync2, existsSync as existsSync3, readFileSync as readFileSync2, statSync as statSync3, appendFileSync } from "fs";
|
|
641
641
|
import { execSync as execSync2 } from "child_process";
|
|
642
642
|
import { platform } from "os";
|
|
643
|
-
import { resolve, dirname as dirname2, join as
|
|
643
|
+
import { resolve, dirname as dirname2, join as join3, basename } from "path";
|
|
644
644
|
import chalk2 from "chalk";
|
|
645
645
|
|
|
646
646
|
// src/repl/clipboard.ts
|
|
@@ -730,12 +730,9 @@ function getClipboardHint() {
|
|
|
730
730
|
return "";
|
|
731
731
|
}
|
|
732
732
|
|
|
733
|
-
// src/repl/commands/
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
|
|
737
|
-
return `${tokens}`;
|
|
738
|
-
}
|
|
733
|
+
// src/repl/commands/project-init.ts
|
|
734
|
+
import { existsSync as existsSync2, readFileSync, readdirSync, statSync as statSync2 } from "fs";
|
|
735
|
+
import { join as join2 } from "path";
|
|
739
736
|
var SCAN_SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
740
737
|
"node_modules",
|
|
741
738
|
".git",
|
|
@@ -793,7 +790,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
|
793
790
|
const name = sorted[i];
|
|
794
791
|
const fullPath = join2(d, name);
|
|
795
792
|
const isLast = i === sorted.length - 1;
|
|
796
|
-
const connector = isLast ? "
|
|
793
|
+
const connector = isLast ? "+-- " : "|-- ";
|
|
797
794
|
let isDir;
|
|
798
795
|
try {
|
|
799
796
|
isDir = statSync2(fullPath).isDirectory();
|
|
@@ -803,7 +800,7 @@ function scanDirTree(dir, maxDepth = 2, maxEntries = 80) {
|
|
|
803
800
|
lines.push(prefix + connector + name + (isDir ? "/" : ""));
|
|
804
801
|
count++;
|
|
805
802
|
if (isDir) {
|
|
806
|
-
walk(fullPath, prefix + (isLast ? " " : "
|
|
803
|
+
walk(fullPath, prefix + (isLast ? " " : "| "), depth + 1);
|
|
807
804
|
}
|
|
808
805
|
}
|
|
809
806
|
};
|
|
@@ -847,9 +844,9 @@ function scanProject(cwd) {
|
|
|
847
844
|
try {
|
|
848
845
|
const pkg = JSON.parse(readFileSync(join2(cwd, "package.json"), "utf-8"));
|
|
849
846
|
const scripts = pkg.scripts ?? {};
|
|
850
|
-
info.buildCommand = scripts.build ?
|
|
851
|
-
info.testCommand = scripts.test ?
|
|
852
|
-
info.devCommand = scripts.dev ?
|
|
847
|
+
info.buildCommand = scripts.build ? "npm run build" : void 0;
|
|
848
|
+
info.testCommand = scripts.test ? "npm test" : void 0;
|
|
849
|
+
info.devCommand = scripts.dev ? "npm run dev" : void 0;
|
|
853
850
|
const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
854
851
|
if (allDeps["react"]) info.framework = "React";
|
|
855
852
|
else if (allDeps["vue"]) info.framework = "Vue";
|
|
@@ -891,10 +888,8 @@ function scanProject(cwd) {
|
|
|
891
888
|
}
|
|
892
889
|
function buildInitPrompt(info, cwd) {
|
|
893
890
|
const parts = [
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
## Project Info
|
|
897
|
-
`,
|
|
891
|
+
"Please generate an AICLI.md context file (Markdown format) for the following project. This file will be injected into the AI conversation system prompt to help the AI understand the project structure and coding conventions.",
|
|
892
|
+
"\n## Project Info\n",
|
|
898
893
|
`- Working directory: ${cwd}`,
|
|
899
894
|
`- Type: ${info.type}`,
|
|
900
895
|
`- Language: ${info.language}`
|
|
@@ -923,6 +918,13 @@ Please generate a structured Markdown file containing:
|
|
|
923
918
|
Output the Markdown content directly, do not wrap the entire file in a code block. Keep it concise, within 200 lines.`);
|
|
924
919
|
return parts.join("\n");
|
|
925
920
|
}
|
|
921
|
+
|
|
922
|
+
// src/repl/commands/index.ts
|
|
923
|
+
function fmtCtx(tokens) {
|
|
924
|
+
if (tokens >= 1e6) return `${Math.round(tokens / 1e5) / 10}M`;
|
|
925
|
+
if (tokens >= 1e3) return `${Math.round(tokens / 1024)}K`;
|
|
926
|
+
return `${tokens}`;
|
|
927
|
+
}
|
|
926
928
|
function copyToClipboard(text) {
|
|
927
929
|
const plat = platform();
|
|
928
930
|
if (plat === "win32") {
|
|
@@ -1827,16 +1829,16 @@ No tools match "${filter}".
|
|
|
1827
1829
|
usage: "/mcp [reconnect [serverId] | trust-project]",
|
|
1828
1830
|
async execute(args, ctx) {
|
|
1829
1831
|
if (args[0] === "trust-project") {
|
|
1830
|
-
const { join:
|
|
1831
|
-
const { existsSync:
|
|
1832
|
+
const { join: join6 } = await import("path");
|
|
1833
|
+
const { existsSync: existsSync6 } = await import("fs");
|
|
1832
1834
|
const { getGitRoot: getGitRoot2 } = await import("./git-context-EXOEHQSF.js");
|
|
1833
1835
|
const { MCP_PROJECT_CONFIG_NAME: MCP_PROJECT_CONFIG_NAME2 } = await import("./constants-NHGTSHKT.js");
|
|
1834
1836
|
const { approveProject, hashMcpFile } = await import("./project-trust-NKYHL3VZ.js");
|
|
1835
1837
|
const cwd = process.cwd();
|
|
1836
1838
|
const projectRoot = getGitRoot2(cwd) ?? cwd;
|
|
1837
|
-
const mcpPath =
|
|
1839
|
+
const mcpPath = join6(projectRoot, MCP_PROJECT_CONFIG_NAME2);
|
|
1838
1840
|
console.log();
|
|
1839
|
-
if (!
|
|
1841
|
+
if (!existsSync6(mcpPath)) {
|
|
1840
1842
|
console.log(theme.dim(` No .mcp.json in ${projectRoot}.`));
|
|
1841
1843
|
console.log();
|
|
1842
1844
|
return;
|
|
@@ -2169,9 +2171,9 @@ No tools match "${filter}".
|
|
|
2169
2171
|
usage: "/init [--force]",
|
|
2170
2172
|
async execute(args, ctx) {
|
|
2171
2173
|
const cwd = process.cwd();
|
|
2172
|
-
const targetPath =
|
|
2174
|
+
const targetPath = join3(cwd, "AICLI.md");
|
|
2173
2175
|
const force = args.includes("--force");
|
|
2174
|
-
if (
|
|
2176
|
+
if (existsSync3(targetPath) && !force) {
|
|
2175
2177
|
ctx.renderer.printInfo(`AICLI.md already exists at ${targetPath}`);
|
|
2176
2178
|
ctx.renderer.printInfo("Use /init --force to overwrite, or edit it manually.");
|
|
2177
2179
|
return;
|
|
@@ -3049,7 +3051,7 @@ ${hint}` : "")
|
|
|
3049
3051
|
description: "Persistent memory (memory.md) + chat memory recall index (v0.4.89+)",
|
|
3050
3052
|
usage: "/memory [show|add <text>|clear|path|rebuild|refresh|status|recall <query>|index-clear]",
|
|
3051
3053
|
async execute(args, ctx) {
|
|
3052
|
-
const memoryFile =
|
|
3054
|
+
const memoryFile = join3(ctx.config.getConfigDir(), MEMORY_FILE_NAME);
|
|
3053
3055
|
const sub = args[0] ?? "show";
|
|
3054
3056
|
if (sub === "rebuild" || sub === "refresh") {
|
|
3055
3057
|
const full = sub === "rebuild";
|
|
@@ -3133,11 +3135,11 @@ ${hint}` : "")
|
|
|
3133
3135
|
return;
|
|
3134
3136
|
}
|
|
3135
3137
|
if (sub === "show" || sub === "view") {
|
|
3136
|
-
if (!
|
|
3138
|
+
if (!existsSync3(memoryFile)) {
|
|
3137
3139
|
ctx.renderer.printInfo("Memory is empty (memory.md not found)");
|
|
3138
3140
|
return;
|
|
3139
3141
|
}
|
|
3140
|
-
const content =
|
|
3142
|
+
const content = readFileSync2(memoryFile, "utf-8").trim();
|
|
3141
3143
|
if (!content) {
|
|
3142
3144
|
ctx.renderer.printInfo("Memory is empty");
|
|
3143
3145
|
return;
|
|
@@ -3168,7 +3170,7 @@ ${text}
|
|
|
3168
3170
|
ctx.renderer.renderError(`Failed to write memory: ${err instanceof Error ? err.message : String(err)}`);
|
|
3169
3171
|
}
|
|
3170
3172
|
} else if (sub === "clear") {
|
|
3171
|
-
if (!
|
|
3173
|
+
if (!existsSync3(memoryFile)) {
|
|
3172
3174
|
ctx.renderer.printInfo("Memory is already empty");
|
|
3173
3175
|
return;
|
|
3174
3176
|
}
|
|
@@ -3204,20 +3206,20 @@ ${text}
|
|
|
3204
3206
|
console.log(theme.heading("Config Files:"));
|
|
3205
3207
|
console.log(` Dir: ${theme.accent(configDir)}`);
|
|
3206
3208
|
const checkFile = (label, filePath) => {
|
|
3207
|
-
const exists =
|
|
3209
|
+
const exists = existsSync3(filePath);
|
|
3208
3210
|
const icon = exists ? theme.success("\u2713") : theme.dim("\u2013");
|
|
3209
3211
|
let extra = "";
|
|
3210
3212
|
if (exists) {
|
|
3211
3213
|
try {
|
|
3212
|
-
extra = theme.dim(` (${
|
|
3214
|
+
extra = theme.dim(` (${statSync3(filePath).size} bytes)`);
|
|
3213
3215
|
} catch {
|
|
3214
3216
|
}
|
|
3215
3217
|
}
|
|
3216
3218
|
console.log(` ${icon} ${label.padEnd(14)} ${exists ? filePath : theme.dim("(not found)")}${extra}`);
|
|
3217
3219
|
};
|
|
3218
|
-
checkFile("config.json",
|
|
3219
|
-
checkFile("memory.md",
|
|
3220
|
-
checkFile("dev-state.md",
|
|
3220
|
+
checkFile("config.json", join3(configDir, "config.json"));
|
|
3221
|
+
checkFile("memory.md", join3(configDir, MEMORY_FILE_NAME));
|
|
3222
|
+
checkFile("dev-state.md", join3(configDir, "dev-state.md"));
|
|
3221
3223
|
console.log();
|
|
3222
3224
|
const mcpManager = ctx.getMcpManager();
|
|
3223
3225
|
if (mcpManager) {
|
|
@@ -3381,9 +3383,9 @@ ${text}
|
|
|
3381
3383
|
let newFiles = 0;
|
|
3382
3384
|
let modifiedFiles = 0;
|
|
3383
3385
|
for (const [filePath, { earliest }] of fileMap) {
|
|
3384
|
-
const currentContent =
|
|
3386
|
+
const currentContent = existsSync3(filePath) ? (() => {
|
|
3385
3387
|
try {
|
|
3386
|
-
return
|
|
3388
|
+
return readFileSync2(filePath, "utf-8");
|
|
3387
3389
|
} catch {
|
|
3388
3390
|
return null;
|
|
3389
3391
|
}
|
|
@@ -3509,12 +3511,12 @@ Summary: ${fileMap.size} file(s) \u2014 ${newFiles} new, ${modifiedFiles} modifi
|
|
|
3509
3511
|
if (scanAll) {
|
|
3510
3512
|
const metas = ctx.sessions.listSessions();
|
|
3511
3513
|
console.log(theme.info(` Scanning ${metas.length} session(s)\u2026`));
|
|
3512
|
-
const { readFileSync:
|
|
3513
|
-
const { join:
|
|
3514
|
+
const { readFileSync: readFileSync5 } = await import("fs");
|
|
3515
|
+
const { join: join6 } = await import("path");
|
|
3514
3516
|
const historyDir = ctx.config.getHistoryDir();
|
|
3515
3517
|
for (const m of metas) {
|
|
3516
3518
|
try {
|
|
3517
|
-
const content =
|
|
3519
|
+
const content = readFileSync5(join6(historyDir, `${m.id}.json`), "utf-8");
|
|
3518
3520
|
const hits2 = scanString(content, opts);
|
|
3519
3521
|
if (hits2.length) {
|
|
3520
3522
|
filesWithHits++;
|
|
@@ -4199,13 +4201,13 @@ Managing ${displayName} API Key`);
|
|
|
4199
4201
|
};
|
|
4200
4202
|
|
|
4201
4203
|
// src/repl/custom-commands.ts
|
|
4202
|
-
import { existsSync as
|
|
4203
|
-
import { join as
|
|
4204
|
+
import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
4205
|
+
import { join as join4, extname } from "path";
|
|
4204
4206
|
import { execSync as execSync3 } from "child_process";
|
|
4205
4207
|
function parseCommandFile(filePath) {
|
|
4206
4208
|
let content;
|
|
4207
4209
|
try {
|
|
4208
|
-
content =
|
|
4210
|
+
content = readFileSync3(filePath, "utf-8");
|
|
4209
4211
|
} catch {
|
|
4210
4212
|
return null;
|
|
4211
4213
|
}
|
|
@@ -4229,7 +4231,7 @@ function expandTemplate(template, args) {
|
|
|
4229
4231
|
result = result.replace(/\{\{input\}\}/g, args.join(" "));
|
|
4230
4232
|
result = result.replace(/\{\{file:([^}]+)\}\}/g, (_m, p) => {
|
|
4231
4233
|
try {
|
|
4232
|
-
return
|
|
4234
|
+
return readFileSync3(p.trim(), "utf-8");
|
|
4233
4235
|
} catch {
|
|
4234
4236
|
return `[Error: cannot read ${p.trim()}]`;
|
|
4235
4237
|
}
|
|
@@ -4254,14 +4256,14 @@ var CustomCommandManager = class {
|
|
|
4254
4256
|
commands = /* @__PURE__ */ new Map();
|
|
4255
4257
|
loadCommands() {
|
|
4256
4258
|
this.commands.clear();
|
|
4257
|
-
if (!
|
|
4259
|
+
if (!existsSync4(this.commandsDir)) {
|
|
4258
4260
|
mkdirSync3(this.commandsDir, { recursive: true });
|
|
4259
4261
|
return 0;
|
|
4260
4262
|
}
|
|
4261
4263
|
let count = 0;
|
|
4262
4264
|
for (const file of readdirSync2(this.commandsDir)) {
|
|
4263
4265
|
if (extname(file) !== ".md") continue;
|
|
4264
|
-
const cmd = parseCommandFile(
|
|
4266
|
+
const cmd = parseCommandFile(join4(this.commandsDir, file));
|
|
4265
4267
|
if (cmd) {
|
|
4266
4268
|
this.commands.set(cmd.meta.name, cmd);
|
|
4267
4269
|
count++;
|
|
@@ -4566,17 +4568,17 @@ function parseAtReferences(input2, cwd) {
|
|
|
4566
4568
|
const absPath = resolve2(cwd, rawPath);
|
|
4567
4569
|
const ext = extname2(rawPath).toLowerCase();
|
|
4568
4570
|
const mime = IMAGE_MIME[ext];
|
|
4569
|
-
if (!
|
|
4571
|
+
if (!existsSync5(absPath)) {
|
|
4570
4572
|
refs.push({ path: rawPath, type: "notfound" });
|
|
4571
4573
|
continue;
|
|
4572
4574
|
}
|
|
4573
4575
|
if (mime) {
|
|
4574
|
-
const fileSize =
|
|
4576
|
+
const fileSize = statSync4(absPath).size;
|
|
4575
4577
|
if (fileSize > MAX_IMAGE_BYTES) {
|
|
4576
4578
|
refs.push({ path: rawPath, type: "toolarge" });
|
|
4577
4579
|
continue;
|
|
4578
4580
|
}
|
|
4579
|
-
const data =
|
|
4581
|
+
const data = readFileSync4(absPath).toString("base64");
|
|
4580
4582
|
imageParts.push({
|
|
4581
4583
|
type: "image_url",
|
|
4582
4584
|
image_url: { url: `data:${mime};base64,${data}` }
|
|
@@ -4584,7 +4586,7 @@ function parseAtReferences(input2, cwd) {
|
|
|
4584
4586
|
refs.push({ path: rawPath, type: "image" });
|
|
4585
4587
|
textBody = textBody.replace(match[0], "").trim();
|
|
4586
4588
|
} else {
|
|
4587
|
-
const content =
|
|
4589
|
+
const content = readFileSync4(absPath, "utf-8");
|
|
4588
4590
|
const inlined = `
|
|
4589
4591
|
|
|
4590
4592
|
[File: ${rawPath}]
|
|
@@ -4809,12 +4811,12 @@ var Repl = class {
|
|
|
4809
4811
|
const filtered = entries.filter((e) => !SKIP_DIRS_SET.has(e));
|
|
4810
4812
|
for (let i = 0; i < filtered.length && entryCount < MAX_TREE_ENTRIES; i++) {
|
|
4811
4813
|
const name = filtered[i];
|
|
4812
|
-
const fullPath =
|
|
4814
|
+
const fullPath = join5(dir, name);
|
|
4813
4815
|
const isLast = i === filtered.length - 1;
|
|
4814
4816
|
const connector = isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4815
4817
|
let isDir;
|
|
4816
4818
|
try {
|
|
4817
|
-
isDir =
|
|
4819
|
+
isDir = statSync4(fullPath).isDirectory();
|
|
4818
4820
|
} catch {
|
|
4819
4821
|
continue;
|
|
4820
4822
|
}
|
|
@@ -4843,10 +4845,10 @@ ${treeLines.join("\n")}`
|
|
|
4843
4845
|
for (const name of entries) {
|
|
4844
4846
|
if (totalChars >= MAX_TOTAL_CHARS) break;
|
|
4845
4847
|
if (SKIP_DIRS_SET.has(name)) continue;
|
|
4846
|
-
const fullPath =
|
|
4848
|
+
const fullPath = join5(dir, name);
|
|
4847
4849
|
let st;
|
|
4848
4850
|
try {
|
|
4849
|
-
st =
|
|
4851
|
+
st = statSync4(fullPath);
|
|
4850
4852
|
} catch {
|
|
4851
4853
|
continue;
|
|
4852
4854
|
}
|
|
@@ -4858,7 +4860,7 @@ ${treeLines.join("\n")}`
|
|
|
4858
4860
|
if (!TEXT_EXTS.has(ext) && !isSpecial) continue;
|
|
4859
4861
|
if (st.size > MAX_FILE_CHARS * 3) continue;
|
|
4860
4862
|
try {
|
|
4861
|
-
let content =
|
|
4863
|
+
let content = readFileSync4(fullPath, "utf-8");
|
|
4862
4864
|
if (content.length > MAX_FILE_CHARS) {
|
|
4863
4865
|
content = content.slice(0, MAX_FILE_CHARS) + `
|
|
4864
4866
|
... (truncated, ${content.length} chars total)`;
|
|
@@ -4888,12 +4890,12 @@ ${content}
|
|
|
4888
4890
|
*/
|
|
4889
4891
|
addExtraContextDir(dirPath) {
|
|
4890
4892
|
const absPath = resolve2(dirPath);
|
|
4891
|
-
if (!
|
|
4893
|
+
if (!existsSync5(absPath)) {
|
|
4892
4894
|
return { success: false, charCount: 0, added: false, error: `Directory not found: ${dirPath}` };
|
|
4893
4895
|
}
|
|
4894
4896
|
let isDir;
|
|
4895
4897
|
try {
|
|
4896
|
-
isDir =
|
|
4898
|
+
isDir = statSync4(absPath).isDirectory();
|
|
4897
4899
|
} catch {
|
|
4898
4900
|
return { success: false, charCount: 0, added: false, error: `Cannot access: ${dirPath}` };
|
|
4899
4901
|
}
|
|
@@ -4922,9 +4924,9 @@ ${content}
|
|
|
4922
4924
|
*/
|
|
4923
4925
|
findContextFile(dir, candidates = CONTEXT_FILE_CANDIDATES) {
|
|
4924
4926
|
for (const candidate of candidates) {
|
|
4925
|
-
const fullPath =
|
|
4926
|
-
if (
|
|
4927
|
-
const content =
|
|
4927
|
+
const fullPath = join5(dir, candidate);
|
|
4928
|
+
if (existsSync5(fullPath)) {
|
|
4929
|
+
const content = readFileSync4(fullPath, "utf-8").trim();
|
|
4928
4930
|
if (content) return { filePath: fullPath, content };
|
|
4929
4931
|
}
|
|
4930
4932
|
}
|
|
@@ -4952,10 +4954,10 @@ ${content}
|
|
|
4952
4954
|
const cwd = process.cwd();
|
|
4953
4955
|
const gitRoot = getGitRoot(cwd);
|
|
4954
4956
|
const projectRoot = gitRoot ?? cwd;
|
|
4955
|
-
const mcpPath =
|
|
4956
|
-
if (!
|
|
4957
|
+
const mcpPath = join5(projectRoot, MCP_PROJECT_CONFIG_NAME);
|
|
4958
|
+
if (!existsSync5(mcpPath)) return null;
|
|
4957
4959
|
try {
|
|
4958
|
-
const raw = JSON.parse(
|
|
4960
|
+
const raw = JSON.parse(readFileSync4(mcpPath, "utf-8"));
|
|
4959
4961
|
const servers = raw?.mcpServers;
|
|
4960
4962
|
if (!servers || typeof servers !== "object") {
|
|
4961
4963
|
process.stderr.write(
|
|
@@ -5001,8 +5003,8 @@ ${content}
|
|
|
5001
5003
|
);
|
|
5002
5004
|
return { layers: [], mergedContent: "" };
|
|
5003
5005
|
}
|
|
5004
|
-
if (
|
|
5005
|
-
const content =
|
|
5006
|
+
if (existsSync5(fullPath)) {
|
|
5007
|
+
const content = readFileSync4(fullPath, "utf-8").trim();
|
|
5006
5008
|
if (content) {
|
|
5007
5009
|
const layer = {
|
|
5008
5010
|
level: "project",
|
|
@@ -5059,9 +5061,9 @@ ${content}
|
|
|
5059
5061
|
* 超过 MEMORY_MAX_CHARS 时只取末尾最新部分。
|
|
5060
5062
|
*/
|
|
5061
5063
|
loadMemoryContent() {
|
|
5062
|
-
const memoryPath =
|
|
5063
|
-
if (!
|
|
5064
|
-
let content =
|
|
5064
|
+
const memoryPath = join5(this.config.getConfigDir(), MEMORY_FILE_NAME);
|
|
5065
|
+
if (!existsSync5(memoryPath)) return null;
|
|
5066
|
+
let content = readFileSync4(memoryPath, "utf-8").trim();
|
|
5065
5067
|
if (!content) return null;
|
|
5066
5068
|
if (content.length > MEMORY_MAX_CHARS) {
|
|
5067
5069
|
content = content.slice(-MEMORY_MAX_CHARS);
|
|
@@ -5449,14 +5451,14 @@ Session '${this.resumeSessionId}' not found.
|
|
|
5449
5451
|
process.stdout.write(theme.dim(` \u{1F50C} Plugins loaded: ${pluginCount} tool(s) from plugins/
|
|
5450
5452
|
`));
|
|
5451
5453
|
}
|
|
5452
|
-
const skillsDir =
|
|
5454
|
+
const skillsDir = join5(this.config.getConfigDir(), SKILLS_DIR_NAME);
|
|
5453
5455
|
this.skillManager = new SkillManager(skillsDir, this.config.get("ui").skillSizeWarn);
|
|
5454
5456
|
const skillCount = this.skillManager.loadSkills();
|
|
5455
5457
|
if (skillCount > 0) {
|
|
5456
5458
|
process.stdout.write(theme.dim(` \u{1F3AF} Skills: ${skillCount} available (use /skill to manage)
|
|
5457
5459
|
`));
|
|
5458
5460
|
}
|
|
5459
|
-
const commandsDir =
|
|
5461
|
+
const commandsDir = join5(this.config.getConfigDir(), CUSTOM_COMMANDS_DIR_NAME);
|
|
5460
5462
|
this.customCommandManager = new CustomCommandManager(commandsDir);
|
|
5461
5463
|
const customCmdCount = this.customCommandManager.loadCommands();
|
|
5462
5464
|
if (customCmdCount > 0) {
|
|
@@ -6092,15 +6094,15 @@ Session '${this.resumeSessionId}' not found.
|
|
|
6092
6094
|
const dir = normalized.includes("/") ? dirname3(normalized) : ".";
|
|
6093
6095
|
const prefix = normalized.includes("/") ? basename2(normalized) : normalized;
|
|
6094
6096
|
const absDir = resolve2(process.cwd(), dir);
|
|
6095
|
-
if (!
|
|
6097
|
+
if (!existsSync5(absDir)) return [];
|
|
6096
6098
|
const entries = readdirSync3(absDir);
|
|
6097
6099
|
const results = [];
|
|
6098
6100
|
for (const entry of entries) {
|
|
6099
6101
|
if (entry.startsWith(".")) continue;
|
|
6100
6102
|
if (!entry.toLowerCase().startsWith(prefix.toLowerCase())) continue;
|
|
6101
6103
|
try {
|
|
6102
|
-
const fullPath =
|
|
6103
|
-
const stat =
|
|
6104
|
+
const fullPath = join5(absDir, entry);
|
|
6105
|
+
const stat = statSync4(fullPath);
|
|
6104
6106
|
const rel = dir === "." ? entry : `${dir}/${entry}`;
|
|
6105
6107
|
results.push(stat.isDirectory() ? `${rel}/` : rel);
|
|
6106
6108
|
} catch {
|
|
@@ -7269,7 +7271,7 @@ program.command("web").description("Start Web UI server with browser-based chat
|
|
|
7269
7271
|
console.error("Error: Invalid port number. Must be between 1 and 65535.");
|
|
7270
7272
|
process.exit(1);
|
|
7271
7273
|
}
|
|
7272
|
-
const { startWebServer } = await import("./server-
|
|
7274
|
+
const { startWebServer } = await import("./server-L2MXWYF7.js");
|
|
7273
7275
|
await startWebServer({ port, host: options.host });
|
|
7274
7276
|
});
|
|
7275
7277
|
program.command("user [action] [username]").description("Manage Web UI users (list | create <name> | delete <name> | reset-password <name> | logout-all <name> | migrate <name>)").action(async (action, username) => {
|
|
@@ -3528,12 +3528,9 @@ async function startWebServer(options = {}) {
|
|
|
3528
3528
|
"Content-Security-Policy",
|
|
3529
3529
|
[
|
|
3530
3530
|
"default-src 'self'",
|
|
3531
|
-
//
|
|
3532
|
-
//
|
|
3533
|
-
|
|
3534
|
-
// CSP violation in the console). localhost dev/admin tool — the
|
|
3535
|
-
// unsafe-inline XSS surface is acceptable under that threat model.
|
|
3536
|
-
"script-src 'self' 'unsafe-inline'",
|
|
3531
|
+
// Inline event handlers and startup scripts live in app.js, so script
|
|
3532
|
+
// execution stays locked to same-origin files only.
|
|
3533
|
+
"script-src 'self'",
|
|
3537
3534
|
"style-src 'self' 'unsafe-inline'",
|
|
3538
3535
|
"img-src 'self' data: blob:",
|
|
3539
3536
|
"font-src 'self' data:",
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative Web UI actions and startup hooks.
|
|
3
|
+
* Kept separate from app.js so static and dynamic UI markup can avoid inline handlers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function handleDeclarativeAction(action, el) {
|
|
7
|
+
switch (action) {
|
|
8
|
+
case 'toggle-auth-mode': toggleAuthMode(); break;
|
|
9
|
+
case 'show-enable-auth': showEnableAuthDialog(); break;
|
|
10
|
+
case 'logout': handleLogout(); break;
|
|
11
|
+
case 'toggle-batch-select': toggleBatchSelect(); break;
|
|
12
|
+
case 'select-all-sessions': selectAllSessions(); break;
|
|
13
|
+
case 'batch-delete-selected': batchDeleteSelected(); break;
|
|
14
|
+
case 'start-hub': window.startHub(); break;
|
|
15
|
+
case 'abort-hub': window.abortHub(); break;
|
|
16
|
+
case 'close-sidebar': closeSidebar(); break;
|
|
17
|
+
case 'import-templates': importTemplates(); break;
|
|
18
|
+
case 'export-templates': exportTemplates(); break;
|
|
19
|
+
case 'show-add-template': showAddTemplate(); break;
|
|
20
|
+
case 'cancel-template-form': cancelTemplateForm(); break;
|
|
21
|
+
case 'save-template-form': saveTemplateForm(); break;
|
|
22
|
+
case 'confirm-response': window.respondConfirm(el.dataset.requestId, el.dataset.approved === 'true'); break;
|
|
23
|
+
case 'batch-confirm-response': window.respondBatchConfirm(el.dataset.requestId, el); break;
|
|
24
|
+
case 'batch-deny-response': window.respondBatchDeny(el.dataset.requestId, el); break;
|
|
25
|
+
case 'ask-user-response': window.respondAskUser(el.dataset.requestId); break;
|
|
26
|
+
case 'auto-pause-response': window.respondAutoPause(el.dataset.requestId, el.dataset.pauseAction); break;
|
|
27
|
+
case 'use-template': useTemplate(el.closest('[data-tpl-id]')?.dataset.tplId); break;
|
|
28
|
+
case 'edit-template': editTemplate(el.closest('[data-tpl-id]')?.dataset.tplId); break;
|
|
29
|
+
case 'delete-template': deleteTemplate(el.closest('[data-tpl-id]')?.dataset.tplId); break;
|
|
30
|
+
case 'insert-file-ref': insertFileRef(el.dataset.filePath || ''); break;
|
|
31
|
+
case 'show-full-preview':
|
|
32
|
+
if (el.previousElementSibling) el.previousElementSibling.style.maxHeight = 'none';
|
|
33
|
+
el.remove();
|
|
34
|
+
break;
|
|
35
|
+
case 'load-session': send({ type: 'command', name: 'session', args: ['load', el.dataset.sessionId] }); break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
document.addEventListener('click', (event) => {
|
|
40
|
+
const themeEl = event.target.closest('[data-theme-choice]');
|
|
41
|
+
if (themeEl) {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
window.setTheme(themeEl.dataset.themeChoice);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const actionEl = event.target.closest('[data-action]');
|
|
48
|
+
if (!actionEl) return;
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
handleDeclarativeAction(actionEl.dataset.action, actionEl);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
document.addEventListener('keydown', (event) => {
|
|
54
|
+
if (event.key !== 'Enter') return;
|
|
55
|
+
const actionEl = event.target.closest('[data-enter-action]');
|
|
56
|
+
if (!actionEl) return;
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
handleDeclarativeAction(actionEl.dataset.enterAction, actionEl);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
document.getElementById('auth-form')?.addEventListener('submit', handleAuth);
|
|
62
|
+
|
|
63
|
+
function registerServiceWorker() {
|
|
64
|
+
// Self-updating service worker (v0.4.159). A new SW postMessages
|
|
65
|
+
// 'sw-updated' (and triggers controllerchange) when it takes over; reload
|
|
66
|
+
// once so users do not stay on stale index.html/app.js after an upgrade.
|
|
67
|
+
if (!('serviceWorker' in navigator)) return;
|
|
68
|
+
let swReloaded = false;
|
|
69
|
+
const reloadOnce = () => {
|
|
70
|
+
if (!swReloaded) {
|
|
71
|
+
swReloaded = true;
|
|
72
|
+
location.reload();
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
navigator.serviceWorker.addEventListener('message', (e) => {
|
|
76
|
+
if (e.data && e.data.type === 'sw-updated') reloadOnce();
|
|
77
|
+
});
|
|
78
|
+
navigator.serviceWorker.addEventListener('controllerchange', reloadOnce);
|
|
79
|
+
navigator.serviceWorker.register('sw.js').then((reg) => {
|
|
80
|
+
reg.update();
|
|
81
|
+
}).catch(() => {});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
registerServiceWorker();
|
package/dist/web/client/app.js
CHANGED
|
@@ -336,8 +336,8 @@ function handleConfirmRequest(msg) {
|
|
|
336
336
|
</div>
|
|
337
337
|
${msg.diff ? `<div class="confirm-diff w-full">${renderDiffHtml(msg.diff)}</div>` : ''}
|
|
338
338
|
<div class="flex gap-2 mt-2">
|
|
339
|
-
<button class="btn btn-success btn-sm btn-outline"
|
|
340
|
-
<button class="btn btn-error btn-sm btn-outline"
|
|
339
|
+
<button class="btn btn-success btn-sm btn-outline" data-action="confirm-response" data-request-id="${msg.requestId}" data-approved="true">✓ Approve</button>
|
|
340
|
+
<button class="btn btn-error btn-sm btn-outline" data-action="confirm-response" data-request-id="${msg.requestId}" data-approved="false">✗ Deny</button>
|
|
341
341
|
</div>
|
|
342
342
|
`;
|
|
343
343
|
messagesEl.appendChild(el);
|
|
@@ -363,8 +363,8 @@ function handleBatchConfirmRequest(msg) {
|
|
|
363
363
|
</div>
|
|
364
364
|
<div class="w-full my-1">${fileListHtml}</div>
|
|
365
365
|
<div class="flex gap-2 mt-2">
|
|
366
|
-
<button class="btn btn-success btn-sm btn-outline"
|
|
367
|
-
<button class="btn btn-error btn-sm btn-outline"
|
|
366
|
+
<button class="btn btn-success btn-sm btn-outline" data-action="batch-confirm-response" data-request-id="${msg.requestId}">✓ Approve Selected</button>
|
|
367
|
+
<button class="btn btn-error btn-sm btn-outline" data-action="batch-deny-response" data-request-id="${msg.requestId}">✗ Reject All</button>
|
|
368
368
|
</div>
|
|
369
369
|
`;
|
|
370
370
|
messagesEl.appendChild(el);
|
|
@@ -383,8 +383,8 @@ function handleAskUserRequest(msg) {
|
|
|
383
383
|
<input type="text" id="ask-input-${msg.requestId}"
|
|
384
384
|
class="input input-sm input-bordered flex-1"
|
|
385
385
|
placeholder="Your answer..."
|
|
386
|
-
|
|
387
|
-
<button class="btn btn-primary btn-sm"
|
|
386
|
+
data-enter-action="ask-user-response" data-request-id="${msg.requestId}">
|
|
387
|
+
<button class="btn btn-primary btn-sm" data-action="ask-user-response" data-request-id="${msg.requestId}">Send</button>
|
|
388
388
|
</div>
|
|
389
389
|
`;
|
|
390
390
|
messagesEl.appendChild(el);
|
|
@@ -407,12 +407,12 @@ function handleAutoPauseRequest(msg) {
|
|
|
407
407
|
<input type="text" id="pause-input-${msg.requestId}"
|
|
408
408
|
class="input input-sm input-bordered flex-1"
|
|
409
409
|
placeholder="Optional: redirect AI with a new instruction..."
|
|
410
|
-
|
|
410
|
+
data-enter-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="redirect">
|
|
411
411
|
</div>
|
|
412
412
|
<div class="flex gap-2">
|
|
413
|
-
<button class="btn btn-success btn-sm flex-1"
|
|
414
|
-
<button class="btn btn-primary btn-sm flex-1"
|
|
415
|
-
<button class="btn btn-error btn-sm flex-1"
|
|
413
|
+
<button class="btn btn-success btn-sm flex-1" data-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="continue">▶ Continue</button>
|
|
414
|
+
<button class="btn btn-primary btn-sm flex-1" data-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="redirect">↪ Redirect</button>
|
|
415
|
+
<button class="btn btn-error btn-sm flex-1" data-action="auto-pause-response" data-request-id="${msg.requestId}" data-pause-action="stop">⏹ Stop</button>
|
|
416
416
|
</div>
|
|
417
417
|
`;
|
|
418
418
|
messagesEl.appendChild(el);
|
|
@@ -666,6 +666,7 @@ window.setTheme = function(theme) {
|
|
|
666
666
|
|
|
667
667
|
// ── UI helpers ─────────────────────────────────────────────────────
|
|
668
668
|
|
|
669
|
+
|
|
669
670
|
function createAssistantMessage() {
|
|
670
671
|
const el = document.createElement('div');
|
|
671
672
|
el.className = 'msg-assistant';
|
|
@@ -714,11 +715,11 @@ function renderMarkdown(el, text) {
|
|
|
714
715
|
const btn = document.createElement('button');
|
|
715
716
|
btn.className = 'copy-code-btn btn btn-ghost btn-xs';
|
|
716
717
|
btn.textContent = '📋 Copy';
|
|
717
|
-
btn.
|
|
718
|
-
navigator.clipboard.writeText(block.textContent || '');
|
|
719
|
-
btn.textContent = '
|
|
720
|
-
setTimeout(() => btn.textContent = '
|
|
721
|
-
};
|
|
718
|
+
btn.addEventListener('click', () => {
|
|
719
|
+
navigator.clipboard.writeText(block.textContent || '');
|
|
720
|
+
btn.textContent = 'Copied';
|
|
721
|
+
setTimeout(() => btn.textContent = 'Copy', 1500);
|
|
722
|
+
});
|
|
722
723
|
pre.style.position = 'relative';
|
|
723
724
|
pre.appendChild(btn);
|
|
724
725
|
}
|
|
@@ -1920,12 +1921,12 @@ function addImageToPreview(file) {
|
|
|
1920
1921
|
<img src="${reader.result}" class="rounded max-h-16 max-w-[100px] object-contain border border-base-content/20" alt="${escapeHtml(file.name)}">
|
|
1921
1922
|
<button class="btn btn-xs btn-circle btn-error absolute -top-1 -right-1 opacity-80" title="Remove">✕</button>
|
|
1922
1923
|
`;
|
|
1923
|
-
thumb.querySelector('button').
|
|
1924
|
+
thumb.querySelector('button').addEventListener('click', () => {
|
|
1924
1925
|
const idx = pendingImages.findIndex(img => img.name === file.name && img.data === base64);
|
|
1925
1926
|
if (idx >= 0) pendingImages.splice(idx, 1);
|
|
1926
1927
|
thumb.remove();
|
|
1927
1928
|
if (pendingImages.length === 0) imagePreviewArea.classList.add('hidden');
|
|
1928
|
-
};
|
|
1929
|
+
});
|
|
1929
1930
|
imagePreviewArea.appendChild(thumb);
|
|
1930
1931
|
imagePreviewArea.classList.remove('hidden');
|
|
1931
1932
|
};
|
|
@@ -1991,189 +1992,6 @@ function handleMemoryContent(msg) {
|
|
|
1991
1992
|
|
|
1992
1993
|
// ── Prompt Templates ───────────────────────────────────────────────
|
|
1993
1994
|
|
|
1994
|
-
const TEMPLATES_KEY = 'aicli-templates';
|
|
1995
|
-
|
|
1996
|
-
function loadTemplates() {
|
|
1997
|
-
try {
|
|
1998
|
-
return JSON.parse(localStorage.getItem(TEMPLATES_KEY) || '[]');
|
|
1999
|
-
} catch { return []; }
|
|
2000
|
-
}
|
|
2001
|
-
|
|
2002
|
-
function saveTemplatesToStorage(templates) {
|
|
2003
|
-
localStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
|
|
2004
|
-
}
|
|
2005
|
-
|
|
2006
|
-
function openTemplatesModal() {
|
|
2007
|
-
const modal = document.getElementById('templates-modal');
|
|
2008
|
-
if (!modal) return;
|
|
2009
|
-
cancelTemplateForm();
|
|
2010
|
-
renderTemplateList();
|
|
2011
|
-
document.getElementById('template-search').value = '';
|
|
2012
|
-
modal.showModal();
|
|
2013
|
-
}
|
|
2014
|
-
|
|
2015
|
-
function renderTemplateList(filter) {
|
|
2016
|
-
const listEl = document.getElementById('template-list');
|
|
2017
|
-
if (!listEl) return;
|
|
2018
|
-
const templates = loadTemplates();
|
|
2019
|
-
const q = (filter || '').toLowerCase();
|
|
2020
|
-
const filtered = q
|
|
2021
|
-
? templates.filter(t => t.name.toLowerCase().includes(q) || (t.tags || []).some(tag => tag.toLowerCase().includes(q)) || t.content.toLowerCase().includes(q))
|
|
2022
|
-
: templates;
|
|
2023
|
-
|
|
2024
|
-
if (filtered.length === 0) {
|
|
2025
|
-
listEl.innerHTML = `<div class="template-empty">${q ? 'No matching templates' : 'No templates yet. Click + New to create one.'}</div>`;
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
|
|
2029
|
-
listEl.innerHTML = filtered.map(t => {
|
|
2030
|
-
const preview = t.content.length > 80 ? t.content.slice(0, 80) + '...' : t.content;
|
|
2031
|
-
const tagsHtml = (t.tags || []).map(tag => `<span class="template-item-tag">${escapeHtml(tag)}</span>`).join('');
|
|
2032
|
-
return `
|
|
2033
|
-
<div class="template-item" data-tpl-id="${t.id}" onclick="useTemplate('${t.id}')">
|
|
2034
|
-
<div class="template-item-body">
|
|
2035
|
-
<div class="template-item-name">${escapeHtml(t.name)}</div>
|
|
2036
|
-
<div class="template-item-preview">${escapeHtml(preview)}</div>
|
|
2037
|
-
${tagsHtml ? `<div class="template-item-tags">${tagsHtml}</div>` : ''}
|
|
2038
|
-
</div>
|
|
2039
|
-
<div class="template-item-actions">
|
|
2040
|
-
<button class="btn btn-xs btn-ghost" onclick="event.stopPropagation(); editTemplate('${t.id}')" title="Edit">✏️</button>
|
|
2041
|
-
<button class="btn btn-xs btn-ghost" onclick="event.stopPropagation(); deleteTemplate('${t.id}')" title="Delete">🗑️</button>
|
|
2042
|
-
</div>
|
|
2043
|
-
</div>`;
|
|
2044
|
-
}).join('');
|
|
2045
|
-
}
|
|
2046
|
-
|
|
2047
|
-
function useTemplate(id) {
|
|
2048
|
-
const templates = loadTemplates();
|
|
2049
|
-
const tpl = templates.find(t => t.id === id);
|
|
2050
|
-
if (!tpl) return;
|
|
2051
|
-
userInput.value = tpl.content;
|
|
2052
|
-
userInput.focus();
|
|
2053
|
-
userInput.style.height = 'auto';
|
|
2054
|
-
userInput.style.height = Math.min(userInput.scrollHeight, 200) + 'px';
|
|
2055
|
-
document.getElementById('templates-modal')?.close();
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
function showAddTemplate() {
|
|
2059
|
-
const form = document.getElementById('template-form');
|
|
2060
|
-
form.classList.remove('hidden');
|
|
2061
|
-
document.getElementById('tpl-edit-id').value = '';
|
|
2062
|
-
document.getElementById('tpl-name').value = '';
|
|
2063
|
-
document.getElementById('tpl-content').value = '';
|
|
2064
|
-
document.getElementById('tpl-tags').value = '';
|
|
2065
|
-
document.getElementById('tpl-name').focus();
|
|
2066
|
-
}
|
|
2067
|
-
|
|
2068
|
-
function editTemplate(id) {
|
|
2069
|
-
const templates = loadTemplates();
|
|
2070
|
-
const tpl = templates.find(t => t.id === id);
|
|
2071
|
-
if (!tpl) return;
|
|
2072
|
-
const form = document.getElementById('template-form');
|
|
2073
|
-
form.classList.remove('hidden');
|
|
2074
|
-
document.getElementById('tpl-edit-id').value = tpl.id;
|
|
2075
|
-
document.getElementById('tpl-name').value = tpl.name;
|
|
2076
|
-
document.getElementById('tpl-content').value = tpl.content;
|
|
2077
|
-
document.getElementById('tpl-tags').value = (tpl.tags || []).join(', ');
|
|
2078
|
-
document.getElementById('tpl-name').focus();
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
function cancelTemplateForm() {
|
|
2082
|
-
document.getElementById('template-form')?.classList.add('hidden');
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
function saveTemplateForm() {
|
|
2086
|
-
const name = document.getElementById('tpl-name').value.trim();
|
|
2087
|
-
const content = document.getElementById('tpl-content').value.trim();
|
|
2088
|
-
const tagsRaw = document.getElementById('tpl-tags').value.trim();
|
|
2089
|
-
const editId = document.getElementById('tpl-edit-id').value;
|
|
2090
|
-
|
|
2091
|
-
if (!name || !content) return;
|
|
2092
|
-
|
|
2093
|
-
const tags = tagsRaw ? tagsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
2094
|
-
const templates = loadTemplates();
|
|
2095
|
-
|
|
2096
|
-
if (editId) {
|
|
2097
|
-
const idx = templates.findIndex(t => t.id === editId);
|
|
2098
|
-
if (idx >= 0) {
|
|
2099
|
-
templates[idx].name = name;
|
|
2100
|
-
templates[idx].content = content;
|
|
2101
|
-
templates[idx].tags = tags;
|
|
2102
|
-
}
|
|
2103
|
-
} else {
|
|
2104
|
-
templates.unshift({
|
|
2105
|
-
id: 'tpl-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
2106
|
-
name,
|
|
2107
|
-
content,
|
|
2108
|
-
tags,
|
|
2109
|
-
createdAt: new Date().toISOString(),
|
|
2110
|
-
});
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
saveTemplatesToStorage(templates);
|
|
2114
|
-
cancelTemplateForm();
|
|
2115
|
-
renderTemplateList();
|
|
2116
|
-
}
|
|
2117
|
-
|
|
2118
|
-
function deleteTemplate(id) {
|
|
2119
|
-
const templates = loadTemplates().filter(t => t.id !== id);
|
|
2120
|
-
saveTemplatesToStorage(templates);
|
|
2121
|
-
renderTemplateList(document.getElementById('template-search')?.value);
|
|
2122
|
-
}
|
|
2123
|
-
|
|
2124
|
-
function exportTemplates() {
|
|
2125
|
-
const templates = loadTemplates();
|
|
2126
|
-
if (templates.length === 0) return;
|
|
2127
|
-
const blob = new Blob([JSON.stringify(templates, null, 2)], { type: 'application/json' });
|
|
2128
|
-
const url = URL.createObjectURL(blob);
|
|
2129
|
-
const a = document.createElement('a');
|
|
2130
|
-
a.href = url;
|
|
2131
|
-
a.download = `aicli-templates-${new Date().toISOString().slice(0, 10)}.json`;
|
|
2132
|
-
a.click();
|
|
2133
|
-
URL.revokeObjectURL(url);
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
function importTemplates() {
|
|
2137
|
-
const input = document.createElement('input');
|
|
2138
|
-
input.type = 'file';
|
|
2139
|
-
input.accept = '.json';
|
|
2140
|
-
input.onchange = () => {
|
|
2141
|
-
const file = input.files[0];
|
|
2142
|
-
if (!file) return;
|
|
2143
|
-
const reader = new FileReader();
|
|
2144
|
-
reader.onload = () => {
|
|
2145
|
-
try {
|
|
2146
|
-
const imported = JSON.parse(reader.result);
|
|
2147
|
-
if (!Array.isArray(imported)) throw new Error('Not an array');
|
|
2148
|
-
const existing = loadTemplates();
|
|
2149
|
-
const existingIds = new Set(existing.map(t => t.id));
|
|
2150
|
-
let added = 0;
|
|
2151
|
-
for (const t of imported) {
|
|
2152
|
-
if (t.id && t.name && t.content && !existingIds.has(t.id)) {
|
|
2153
|
-
existing.push(t);
|
|
2154
|
-
added++;
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
saveTemplatesToStorage(existing);
|
|
2158
|
-
renderTemplateList();
|
|
2159
|
-
addInfoMessage(`📥 Imported ${added} template(s).`);
|
|
2160
|
-
} catch {
|
|
2161
|
-
addErrorMessage('Failed to import templates: invalid JSON format.');
|
|
2162
|
-
}
|
|
2163
|
-
};
|
|
2164
|
-
reader.readAsText(file);
|
|
2165
|
-
};
|
|
2166
|
-
input.click();
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
// Template button + search binding
|
|
2170
|
-
document.getElementById('btn-templates')?.addEventListener('click', openTemplatesModal);
|
|
2171
|
-
document.getElementById('template-search')?.addEventListener('input', (e) => {
|
|
2172
|
-
renderTemplateList(e.target.value);
|
|
2173
|
-
});
|
|
2174
|
-
|
|
2175
|
-
// ── File Tree ──────────────────────────────────────────────────────
|
|
2176
|
-
|
|
2177
1995
|
const fileTreeEl = document.getElementById('file-tree');
|
|
2178
1996
|
const fileTreeCwdEl = document.getElementById('file-tree-cwd');
|
|
2179
1997
|
const btnFileTreeRefresh = document.getElementById('btn-file-tree-refresh');
|
|
@@ -2324,10 +2142,10 @@ async function previewFileInChat(filePath) {
|
|
|
2324
2142
|
<div class="flex items-center gap-2 mb-1">
|
|
2325
2143
|
<span class="text-xs font-semibold opacity-70">📄 ${escapeHtml(filePath)}</span>
|
|
2326
2144
|
<span class="badge badge-ghost badge-xs">${sizeKb} KB</span>
|
|
2327
|
-
<button class="btn btn-xs btn-ghost ml-auto opacity-50"
|
|
2145
|
+
<button class="btn btn-xs btn-ghost ml-auto opacity-50" data-action="insert-file-ref" data-file-path="${escapeHtml(filePath)}">Insert @ref</button>
|
|
2328
2146
|
</div>
|
|
2329
2147
|
<pre style="max-height:300px"><code class="language-${escapeHtml(ext)}">${escapeHtml(data.content)}</code></pre>
|
|
2330
|
-
<button class="btn btn-xs btn-ghost mt-1 opacity-50 text-xs"
|
|
2148
|
+
<button class="btn btn-xs btn-ghost mt-1 opacity-50 text-xs" data-action="show-full-preview">▼ Show all</button>
|
|
2331
2149
|
`;
|
|
2332
2150
|
el.querySelectorAll('pre code').forEach(block => {
|
|
2333
2151
|
try { hljs.highlightElement(block); } catch {}
|
|
@@ -3308,7 +3126,7 @@ function hubDone(msg) {
|
|
|
3308
3126
|
document.getElementById('hub-abort-btn').classList.add('hidden');
|
|
3309
3127
|
const status = document.getElementById('hub-status');
|
|
3310
3128
|
if (msg.saved && msg.sessionId) {
|
|
3311
|
-
status.innerHTML = `Saved. <a class="link link-primary"
|
|
3129
|
+
status.innerHTML = `Saved. <a class="link link-primary" data-action="load-session" data-session-id="${msg.sessionId}">Open session</a>`;
|
|
3312
3130
|
} else {
|
|
3313
3131
|
status.textContent = msg.saved ? 'Saved to history.' : 'Done (not saved).';
|
|
3314
3132
|
}
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
<p id="auth-subtitle" class="text-sm text-base-content/60 mt-1">Sign in to continue</p>
|
|
37
37
|
</div>
|
|
38
38
|
<div id="auth-error" class="alert alert-error text-sm mb-4 hidden"></div>
|
|
39
|
-
<form id="auth-form"
|
|
39
|
+
<form id="auth-form" >
|
|
40
40
|
<div class="form-control mb-3">
|
|
41
41
|
<label class="label"><span class="label-text">Username</span></label>
|
|
42
42
|
<input id="auth-username" type="text" class="input input-bordered w-full" placeholder="username" autocomplete="username" required minlength="2" maxlength="32">
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
<button id="auth-submit" type="submit" class="btn btn-primary w-full">Sign In</button>
|
|
49
49
|
</form>
|
|
50
50
|
<div class="text-center mt-4">
|
|
51
|
-
<button id="auth-toggle" class="btn btn-ghost btn-sm"
|
|
51
|
+
<button id="auth-toggle" class="btn btn-ghost btn-sm" data-action="toggle-auth-mode" type="button">
|
|
52
52
|
Don't have an account? <span class="text-primary">Register</span>
|
|
53
53
|
</button>
|
|
54
54
|
</div>
|
|
@@ -76,18 +76,18 @@
|
|
|
76
76
|
<div class="dropdown dropdown-end">
|
|
77
77
|
<div tabindex="0" role="button" class="btn btn-sm btn-ghost">🎨</div>
|
|
78
78
|
<ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-10 w-40 p-2 shadow-lg border border-base-content/10">
|
|
79
|
-
<li><a
|
|
80
|
-
<li><a
|
|
81
|
-
<li><a
|
|
82
|
-
<li><a
|
|
83
|
-
<li><a
|
|
84
|
-
<li><a
|
|
85
|
-
<li><a
|
|
86
|
-
<li><a
|
|
79
|
+
<li><a data-theme-choice="dark">🌙 Dark</a></li>
|
|
80
|
+
<li><a data-theme-choice="night">🌃 Night</a></li>
|
|
81
|
+
<li><a data-theme-choice="dim">🌑 Dim</a></li>
|
|
82
|
+
<li><a data-theme-choice="synthwave">🎵 Synthwave</a></li>
|
|
83
|
+
<li><a data-theme-choice="cyberpunk">🤖 Cyberpunk</a></li>
|
|
84
|
+
<li><a data-theme-choice="light">☀️ Light</a></li>
|
|
85
|
+
<li><a data-theme-choice="cupcake">🧁 Cupcake</a></li>
|
|
86
|
+
<li><a data-theme-choice="nord">❄️ Nord</a></li>
|
|
87
87
|
</ul>
|
|
88
88
|
</div>
|
|
89
89
|
<!-- Enable Auth button (shown when no users registered) -->
|
|
90
|
-
<button id="btn-enable-auth" class="btn btn-sm btn-ghost hidden" title="Enable multi-user auth"
|
|
90
|
+
<button id="btn-enable-auth" class="btn btn-sm btn-ghost hidden" title="Enable multi-user auth" data-action="show-enable-auth">👤 Enable Auth</button>
|
|
91
91
|
<!-- User menu (shown when authenticated) -->
|
|
92
92
|
<div id="user-menu" class="dropdown dropdown-end hidden">
|
|
93
93
|
<div tabindex="0" role="button" class="btn btn-sm btn-ghost gap-1">
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
</div>
|
|
96
96
|
<ul tabindex="0" class="dropdown-content menu bg-base-200 rounded-box z-10 w-40 p-2 shadow-lg border border-base-content/10">
|
|
97
97
|
<li><a id="user-label" class="text-sm font-semibold pointer-events-none"></a></li>
|
|
98
|
-
<li><a
|
|
98
|
+
<li><a data-action="logout">🚪 Logout</a></li>
|
|
99
99
|
</ul>
|
|
100
100
|
</div>
|
|
101
101
|
</div>
|
|
@@ -119,16 +119,16 @@
|
|
|
119
119
|
<div id="tab-sessions" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden">
|
|
120
120
|
<div class="p-2 border-b border-base-content/10 flex items-center justify-between gap-1">
|
|
121
121
|
<input id="session-search" type="text" class="input input-xs input-bordered flex-1 min-w-0" placeholder="Search...">
|
|
122
|
-
<button id="btn-batch-select" class="btn btn-xs btn-ghost flex-shrink-0" title="Select multiple"
|
|
122
|
+
<button id="btn-batch-select" class="btn btn-xs btn-ghost flex-shrink-0" title="Select multiple" data-action="toggle-batch-select">☑</button>
|
|
123
123
|
<button id="btn-new-session" class="btn btn-xs btn-primary btn-outline flex-shrink-0 whitespace-nowrap" title="New session">+ New</button>
|
|
124
124
|
</div>
|
|
125
125
|
<!-- Batch action bar (hidden by default) -->
|
|
126
126
|
<div id="batch-bar" class="hidden px-2 py-1 border-b border-base-content/10 flex items-center gap-1 bg-base-300 text-xs">
|
|
127
127
|
<span class="batch-count opacity-60">0 selected</span>
|
|
128
128
|
<span class="flex-1"></span>
|
|
129
|
-
<button class="btn btn-xs btn-ghost"
|
|
130
|
-
<button class="btn btn-xs btn-error btn-outline"
|
|
131
|
-
<button class="btn btn-xs btn-ghost"
|
|
129
|
+
<button class="btn btn-xs btn-ghost" data-action="select-all-sessions">All</button>
|
|
130
|
+
<button class="btn btn-xs btn-error btn-outline" data-action="batch-delete-selected">🗑 Delete</button>
|
|
131
|
+
<button class="btn btn-xs btn-ghost" data-action="toggle-batch-select">Cancel</button>
|
|
132
132
|
</div>
|
|
133
133
|
<div id="session-list" class="flex-1 overflow-y-auto p-2 flex flex-col gap-1 text-sm">
|
|
134
134
|
<div class="text-xs opacity-40 text-center py-4">No sessions yet</div>
|
|
@@ -216,8 +216,8 @@
|
|
|
216
216
|
<label class="flex items-center gap-2 text-xs cursor-pointer">
|
|
217
217
|
<input id="hub-lanes" type="checkbox" class="checkbox checkbox-xs"> Lanes view (one column per agent)
|
|
218
218
|
</label>
|
|
219
|
-
<button id="hub-start-btn" class="btn btn-primary btn-sm w-full"
|
|
220
|
-
<button id="hub-abort-btn" class="btn btn-error btn-outline btn-xs w-full hidden"
|
|
219
|
+
<button id="hub-start-btn" class="btn btn-primary btn-sm w-full" data-action="start-hub">🏛 Start discussion</button>
|
|
220
|
+
<button id="hub-abort-btn" class="btn btn-error btn-outline btn-xs w-full hidden" data-action="abort-hub">⏹ Stop</button>
|
|
221
221
|
<div id="hub-status" class="text-xs opacity-60"></div>
|
|
222
222
|
</div>
|
|
223
223
|
</div>
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
<!-- Sidebar resize handle -->
|
|
226
226
|
<div id="sidebar-resize" class="sidebar-resize-handle" title="Drag to resize sidebar"></div>
|
|
227
227
|
<!-- Sidebar backdrop (mobile overlay) -->
|
|
228
|
-
<div id="sidebar-backdrop" class="sidebar-backdrop hidden"
|
|
228
|
+
<div id="sidebar-backdrop" class="sidebar-backdrop hidden" data-action="close-sidebar"></div>
|
|
229
229
|
|
|
230
230
|
<!-- Chat Area -->
|
|
231
231
|
<div class="flex-1 flex flex-col overflow-hidden">
|
|
@@ -293,9 +293,9 @@
|
|
|
293
293
|
<div class="flex items-center justify-between mb-3">
|
|
294
294
|
<h3 class="font-bold text-lg">📝 Prompt Templates</h3>
|
|
295
295
|
<div class="flex gap-1">
|
|
296
|
-
<button class="btn btn-xs btn-ghost"
|
|
297
|
-
<button class="btn btn-xs btn-ghost"
|
|
298
|
-
<button class="btn btn-xs btn-ghost"
|
|
296
|
+
<button class="btn btn-xs btn-ghost" data-action="import-templates" title="Import">📥 Import</button>
|
|
297
|
+
<button class="btn btn-xs btn-ghost" data-action="export-templates" title="Export">📤 Export</button>
|
|
298
|
+
<button class="btn btn-xs btn-ghost" data-action="show-add-template" title="New template">+ New</button>
|
|
299
299
|
</div>
|
|
300
300
|
</div>
|
|
301
301
|
|
|
@@ -305,8 +305,8 @@
|
|
|
305
305
|
<textarea id="tpl-content" class="textarea textarea-bordered w-full text-sm mb-2" rows="4" placeholder="Prompt content..."></textarea>
|
|
306
306
|
<input id="tpl-tags" type="text" class="input input-sm input-bordered w-full mb-2" placeholder="Tags (comma-separated, optional)">
|
|
307
307
|
<div class="flex gap-2 justify-end">
|
|
308
|
-
<button class="btn btn-sm btn-ghost"
|
|
309
|
-
<button class="btn btn-sm btn-primary"
|
|
308
|
+
<button class="btn btn-sm btn-ghost" data-action="cancel-template-form">Cancel</button>
|
|
309
|
+
<button class="btn btn-sm btn-primary" data-action="save-template-form">Save</button>
|
|
310
310
|
</div>
|
|
311
311
|
<input type="hidden" id="tpl-edit-id">
|
|
312
312
|
</div>
|
|
@@ -338,23 +338,8 @@
|
|
|
338
338
|
<form method="dialog" class="modal-backdrop"><button>close</button></form>
|
|
339
339
|
</dialog>
|
|
340
340
|
|
|
341
|
-
<script src="app.js"></script>
|
|
342
|
-
<script>
|
|
343
|
-
|
|
344
|
-
// 'sw-updated' (and triggers controllerchange) when it takes over; we
|
|
345
|
-
// reload once so the user never gets stuck on a stale index.html/app.js
|
|
346
|
-
// after upgrading aicli. index.html/app.js are also network-only in sw.js.
|
|
347
|
-
if ('serviceWorker' in navigator) {
|
|
348
|
-
let swReloaded = false;
|
|
349
|
-
const reloadOnce = () => { if (!swReloaded) { swReloaded = true; location.reload(); } };
|
|
350
|
-
navigator.serviceWorker.addEventListener('message', (e) => {
|
|
351
|
-
if (e.data && e.data.type === 'sw-updated') reloadOnce();
|
|
352
|
-
});
|
|
353
|
-
navigator.serviceWorker.addEventListener('controllerchange', reloadOnce);
|
|
354
|
-
navigator.serviceWorker.register('sw.js').then((reg) => {
|
|
355
|
-
reg.update();
|
|
356
|
-
}).catch(() => {});
|
|
357
|
-
}
|
|
358
|
-
</script>
|
|
341
|
+
<script src="app.js"></script>
|
|
342
|
+
<script src="templates.js"></script>
|
|
343
|
+
<script src="actions.js"></script>
|
|
359
344
|
</body>
|
|
360
345
|
</html>
|
package/dist/web/client/sw.js
CHANGED
|
@@ -16,7 +16,7 @@ const CACHE_NAME = 'aicli-v3';
|
|
|
16
16
|
// HTML/JS that must always be fresh — never served from the SW cache, so an
|
|
17
17
|
// upgraded server is reflected on the very next load. Other assets (vendored
|
|
18
18
|
// CSS/JS, icons) are large + content-stable and fine to cache.
|
|
19
|
-
const NEVER_CACHE = new Set(['/', '/index.html', '/app.js']);
|
|
19
|
+
const NEVER_CACHE = new Set(['/', '/index.html', '/app.js', '/templates.js', '/actions.js']);
|
|
20
20
|
|
|
21
21
|
// App shell to precache (offline-capable startup). Deliberately EXCLUDES the
|
|
22
22
|
// always-fresh HTML/JS in NEVER_CACHE — only large content-stable assets.
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt template modal and import/export helpers.
|
|
3
|
+
* Loaded after app.js so it can use shared UI helpers.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const TEMPLATES_KEY = 'aicli-templates';
|
|
7
|
+
|
|
8
|
+
function loadTemplates() {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(localStorage.getItem(TEMPLATES_KEY) || '[]');
|
|
11
|
+
} catch {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function saveTemplatesToStorage(templates) {
|
|
17
|
+
localStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function openTemplatesModal() {
|
|
21
|
+
const modal = document.getElementById('templates-modal');
|
|
22
|
+
if (!modal) return;
|
|
23
|
+
cancelTemplateForm();
|
|
24
|
+
renderTemplateList();
|
|
25
|
+
document.getElementById('template-search').value = '';
|
|
26
|
+
modal.showModal();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function renderTemplateList(filter) {
|
|
30
|
+
const listEl = document.getElementById('template-list');
|
|
31
|
+
if (!listEl) return;
|
|
32
|
+
const templates = loadTemplates();
|
|
33
|
+
const q = (filter || '').toLowerCase();
|
|
34
|
+
const filtered = q
|
|
35
|
+
? templates.filter(t => t.name.toLowerCase().includes(q) || (t.tags || []).some(tag => tag.toLowerCase().includes(q)) || t.content.toLowerCase().includes(q))
|
|
36
|
+
: templates;
|
|
37
|
+
|
|
38
|
+
if (filtered.length === 0) {
|
|
39
|
+
listEl.innerHTML = `<div class="template-empty">${q ? 'No matching templates' : 'No templates yet. Click + New to create one.'}</div>`;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
listEl.innerHTML = filtered.map(t => {
|
|
44
|
+
const preview = t.content.length > 80 ? t.content.slice(0, 80) + '...' : t.content;
|
|
45
|
+
const tagsHtml = (t.tags || []).map(tag => `<span class="template-item-tag">${escapeHtml(tag)}</span>`).join('');
|
|
46
|
+
return `
|
|
47
|
+
<div class="template-item" data-tpl-id="${t.id}" data-action="use-template">
|
|
48
|
+
<div class="template-item-body">
|
|
49
|
+
<div class="template-item-name">${escapeHtml(t.name)}</div>
|
|
50
|
+
<div class="template-item-preview">${escapeHtml(preview)}</div>
|
|
51
|
+
${tagsHtml ? `<div class="template-item-tags">${tagsHtml}</div>` : ''}
|
|
52
|
+
</div>
|
|
53
|
+
<div class="template-item-actions">
|
|
54
|
+
<button class="btn btn-xs btn-ghost" data-action="edit-template" title="Edit">Edit</button>
|
|
55
|
+
<button class="btn btn-xs btn-ghost" data-action="delete-template" title="Delete">Delete</button>
|
|
56
|
+
</div>
|
|
57
|
+
</div>`;
|
|
58
|
+
}).join('');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function useTemplate(id) {
|
|
62
|
+
const templates = loadTemplates();
|
|
63
|
+
const tpl = templates.find(t => t.id === id);
|
|
64
|
+
if (!tpl) return;
|
|
65
|
+
const input = document.getElementById('user-input');
|
|
66
|
+
if (!input) return;
|
|
67
|
+
input.value = tpl.content;
|
|
68
|
+
input.focus();
|
|
69
|
+
input.style.height = 'auto';
|
|
70
|
+
input.style.height = Math.min(input.scrollHeight, 200) + 'px';
|
|
71
|
+
document.getElementById('templates-modal')?.close();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function showAddTemplate() {
|
|
75
|
+
const form = document.getElementById('template-form');
|
|
76
|
+
form.classList.remove('hidden');
|
|
77
|
+
document.getElementById('tpl-edit-id').value = '';
|
|
78
|
+
document.getElementById('tpl-name').value = '';
|
|
79
|
+
document.getElementById('tpl-content').value = '';
|
|
80
|
+
document.getElementById('tpl-tags').value = '';
|
|
81
|
+
document.getElementById('tpl-name').focus();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function editTemplate(id) {
|
|
85
|
+
const templates = loadTemplates();
|
|
86
|
+
const tpl = templates.find(t => t.id === id);
|
|
87
|
+
if (!tpl) return;
|
|
88
|
+
const form = document.getElementById('template-form');
|
|
89
|
+
form.classList.remove('hidden');
|
|
90
|
+
document.getElementById('tpl-edit-id').value = tpl.id;
|
|
91
|
+
document.getElementById('tpl-name').value = tpl.name;
|
|
92
|
+
document.getElementById('tpl-content').value = tpl.content;
|
|
93
|
+
document.getElementById('tpl-tags').value = (tpl.tags || []).join(', ');
|
|
94
|
+
document.getElementById('tpl-name').focus();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cancelTemplateForm() {
|
|
98
|
+
document.getElementById('template-form')?.classList.add('hidden');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function saveTemplateForm() {
|
|
102
|
+
const name = document.getElementById('tpl-name').value.trim();
|
|
103
|
+
const content = document.getElementById('tpl-content').value.trim();
|
|
104
|
+
const tagsRaw = document.getElementById('tpl-tags').value.trim();
|
|
105
|
+
const editId = document.getElementById('tpl-edit-id').value;
|
|
106
|
+
|
|
107
|
+
if (!name || !content) return;
|
|
108
|
+
|
|
109
|
+
const tags = tagsRaw ? tagsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
110
|
+
const templates = loadTemplates();
|
|
111
|
+
|
|
112
|
+
if (editId) {
|
|
113
|
+
const idx = templates.findIndex(t => t.id === editId);
|
|
114
|
+
if (idx >= 0) {
|
|
115
|
+
templates[idx].name = name;
|
|
116
|
+
templates[idx].content = content;
|
|
117
|
+
templates[idx].tags = tags;
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
templates.unshift({
|
|
121
|
+
id: 'tpl-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
|
|
122
|
+
name,
|
|
123
|
+
content,
|
|
124
|
+
tags,
|
|
125
|
+
createdAt: new Date().toISOString(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
saveTemplatesToStorage(templates);
|
|
130
|
+
cancelTemplateForm();
|
|
131
|
+
renderTemplateList();
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function deleteTemplate(id) {
|
|
135
|
+
const templates = loadTemplates().filter(t => t.id !== id);
|
|
136
|
+
saveTemplatesToStorage(templates);
|
|
137
|
+
renderTemplateList(document.getElementById('template-search')?.value);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function exportTemplates() {
|
|
141
|
+
const templates = loadTemplates();
|
|
142
|
+
if (templates.length === 0) return;
|
|
143
|
+
const blob = new Blob([JSON.stringify(templates, null, 2)], { type: 'application/json' });
|
|
144
|
+
const url = URL.createObjectURL(blob);
|
|
145
|
+
const a = document.createElement('a');
|
|
146
|
+
a.href = url;
|
|
147
|
+
a.download = `aicli-templates-${new Date().toISOString().slice(0, 10)}.json`;
|
|
148
|
+
a.click();
|
|
149
|
+
URL.revokeObjectURL(url);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function importTemplates() {
|
|
153
|
+
const input = document.createElement('input');
|
|
154
|
+
input.type = 'file';
|
|
155
|
+
input.accept = '.json';
|
|
156
|
+
input.addEventListener('change', () => {
|
|
157
|
+
const file = input.files[0];
|
|
158
|
+
if (!file) return;
|
|
159
|
+
const reader = new FileReader();
|
|
160
|
+
reader.addEventListener('load', () => {
|
|
161
|
+
try {
|
|
162
|
+
const imported = JSON.parse(reader.result);
|
|
163
|
+
if (!Array.isArray(imported)) throw new Error('Not an array');
|
|
164
|
+
const existing = loadTemplates();
|
|
165
|
+
const existingIds = new Set(existing.map(t => t.id));
|
|
166
|
+
let added = 0;
|
|
167
|
+
for (const t of imported) {
|
|
168
|
+
if (t.id && t.name && t.content && !existingIds.has(t.id)) {
|
|
169
|
+
existing.push(t);
|
|
170
|
+
added++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
saveTemplatesToStorage(existing);
|
|
174
|
+
renderTemplateList();
|
|
175
|
+
addInfoMessage(`Imported ${added} template(s).`);
|
|
176
|
+
} catch {
|
|
177
|
+
addErrorMessage('Failed to import templates: invalid JSON format.');
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
reader.readAsText(file);
|
|
181
|
+
});
|
|
182
|
+
input.click();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
document.getElementById('btn-templates')?.addEventListener('click', openTemplatesModal);
|
|
186
|
+
document.getElementById('template-search')?.addEventListener('input', (e) => {
|
|
187
|
+
renderTemplateList(e.target.value);
|
|
188
|
+
});
|