jinzd-ai-cli 0.4.221 → 0.4.222

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.
Files changed (36) hide show
  1. package/dist/{batch-LSWWSINO.js → batch-QOOO7HRW.js} +4 -3
  2. package/dist/{chunk-4AX2MEJA.js → chunk-5EE4GRGG.js} +21 -21
  3. package/dist/{chunk-S5EEKS4P.js → chunk-AV5OPK7Q.js} +1 -1
  4. package/dist/{chunk-BYQEWWA4.js → chunk-FBK7NSIK.js} +13 -13
  5. package/dist/{chunk-QDN5G7JN.js → chunk-NNWWMGYK.js} +1 -1
  6. package/dist/{chunk-4MOBTDS6.js → chunk-S6L5R6SS.js} +1 -1
  7. package/dist/{chunk-QKJ3MAP7.js → chunk-SISFAT6W.js} +8 -8
  8. package/dist/{chunk-5BY4XICU.js → chunk-TNW22OUY.js} +3 -3
  9. package/dist/{chunk-PMTEZTXP.js → chunk-UWUTLIOH.js} +1 -1
  10. package/dist/{chunk-6SKMGONY.js → chunk-VBRCWH55.js} +8 -7
  11. package/dist/{chunk-OULVIHCT.js → chunk-VWTUYDZG.js} +1 -1
  12. package/dist/{ci-OIFTAJKR.js → ci-IB7NWHZN.js} +4 -4
  13. package/dist/{ci-format-H6BLMWQ5.js → ci-format-YLGZN3D4.js} +2 -2
  14. package/dist/{constants-EJRLT7DM.js → constants-BOMLZEXC.js} +1 -1
  15. package/dist/{doctor-cli-EXC7YU5D.js → doctor-cli-2ND4LONW.js} +4 -4
  16. package/dist/electron-server.js +3038 -3021
  17. package/dist/{hub-HQDRQBNM.js → hub-6WADFJNC.js} +1 -1
  18. package/dist/index.js +2294 -2231
  19. package/dist/{pr-VSZNVKBS.js → pr-F7WXUH27.js} +4 -4
  20. package/dist/{run-tests-OYXK27XH.js → run-tests-5NEMM6EF.js} +2 -2
  21. package/dist/{run-tests-55JGEZYL.js → run-tests-NIY24YDP.js} +1 -1
  22. package/dist/{semantic-PK7AUOJT.js → semantic-HLAE2O4F.js} +2 -2
  23. package/dist/{server-XX6UM4ZQ.js → server-ACFX2J66.js} +11 -11
  24. package/dist/{server-JKNIZ5CB.js → server-QGZYDDZW.js} +2412 -2395
  25. package/dist/{task-orchestrator-F3DUIACA.js → task-orchestrator-4GOBFXPN.js} +11 -11
  26. package/dist/{usage-4KFX3HTK.js → usage-JXT2YVA6.js} +2 -2
  27. package/dist/web/client/actions.js +27 -1
  28. package/dist/web/client/app.js +3275 -3342
  29. package/dist/web/client/dom.js +34 -0
  30. package/dist/web/client/index.html +29 -22
  31. package/dist/web/client/state.js +103 -0
  32. package/dist/web/client/sw.js +1 -1
  33. package/dist/web/client/templates.js +191 -188
  34. package/dist/web/client/util.js +45 -0
  35. package/package.json +1 -1
  36. package/dist/{chunk-T2NL5ZIA.js → chunk-UUSRWSSX.js} +3 -3
@@ -0,0 +1,34 @@
1
+ /**
2
+ * ai-cli Web UI — shared DOM element references (P0-4 Phase 2, native ESM).
3
+ *
4
+ * Long-lived handles to static elements in index.html, resolved once at module
5
+ * load. Extracted from app.js so the "which elements the app holds" list has a
6
+ * single home. This module depends on nothing (acyclic leaf); it is imported by
7
+ * app.js. Module scripts are deferred, so the document is fully parsed before
8
+ * these getElementById calls run.
9
+ */
10
+
11
+ export const messagesEl = document.getElementById('messages');
12
+ export const chatArea = document.getElementById('chat-area');
13
+ export const userInput = document.getElementById('user-input');
14
+ export const btnSend = document.getElementById('btn-send');
15
+ export const btnStop = document.getElementById('btn-stop');
16
+ export const btnClear = document.getElementById('btn-clear');
17
+ export const btnCompact = document.getElementById('btn-compact');
18
+ export const btnThink = document.getElementById('btn-think');
19
+ export const btnPlan = document.getElementById('btn-plan');
20
+ export const providerSelect = document.getElementById('provider-select');
21
+ export const modelSelect = document.getElementById('model-select');
22
+ export const statusSession = document.getElementById('status-session');
23
+ export const statusTokens = document.getElementById('status-tokens');
24
+ export const connectionStatus = document.getElementById('connection-status');
25
+ export const sidebar = document.getElementById('sidebar');
26
+ export const sessionListEl = document.getElementById('session-list');
27
+ export const btnNewSession = document.getElementById('btn-new-session');
28
+ export const toolsListEl = document.getElementById('tools-list');
29
+ export const sessionSearchInput = document.getElementById('session-search');
30
+ export const toolsSearchInput = document.getElementById('tools-search');
31
+ export const sessionTabsListEl = document.getElementById('session-tabs-list');
32
+ export const btnAddTab = document.getElementById('btn-add-tab');
33
+ export const dashboardPanel = document.getElementById('dashboard-panel');
34
+ export const btnDashboardRefresh = document.getElementById('btn-dashboard-refresh');
@@ -116,15 +116,15 @@
116
116
  <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="memory" title="Chat memory semantic recall (B4, v0.4.90)">🧠 Memory</button>
117
117
  <button class="sidebar-tab flex-1 text-xs font-semibold py-2 px-1 text-center" data-tab="hub" title="Multi-agent brainstorm room (P4)">🏛 Hub</button>
118
118
  </div>
119
- <!-- Dashboard tab (Phase 6) -->
120
- <div id="tab-dashboard" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden">
121
- <div class="p-2 border-b border-base-content/10 flex items-center gap-1">
122
- <span class="text-xs font-semibold flex-1">Agent Console</span>
123
- <button id="btn-dashboard-refresh" class="btn btn-xs btn-ghost" title="Refresh dashboard">↻</button>
124
- </div>
125
- <div id="dashboard-panel" class="dashboard-panel flex-1 overflow-y-auto p-2 text-sm">
126
- <div class="text-xs opacity-40 text-center py-4">Waiting for status…</div>
127
- </div>
119
+ <!-- Dashboard tab (Phase 6) -->
120
+ <div id="tab-dashboard" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden">
121
+ <div class="p-2 border-b border-base-content/10 flex items-center gap-1">
122
+ <span class="text-xs font-semibold flex-1">Agent Console</span>
123
+ <button id="btn-dashboard-refresh" class="btn btn-xs btn-ghost" title="Refresh dashboard">↻</button>
124
+ </div>
125
+ <div id="dashboard-panel" class="dashboard-panel flex-1 overflow-y-auto p-2 text-sm">
126
+ <div class="text-xs opacity-40 text-center py-4">Waiting for status…</div>
127
+ </div>
128
128
  </div>
129
129
  <!-- Sessions tab -->
130
130
  <div id="tab-sessions" class="sidebar-tab-content flex flex-col flex-1 overflow-hidden hidden">
@@ -192,16 +192,16 @@
192
192
  <button id="btn-memory-refresh" class="btn btn-xs btn-ghost" title="Incremental refresh">↻</button>
193
193
  <button id="btn-memory-rebuild" class="btn btn-xs btn-ghost" title="Full rebuild (slow first run)">⟳ Rebuild</button>
194
194
  </div>
195
- <div id="memory-status" class="text-xs opacity-50 truncate">Loading index status…</div>
196
- <div id="memory-governance" class="border-t border-base-content/10 pt-2 mt-1">
197
- <div class="flex items-center gap-1 mb-1">
198
- <span class="text-xs font-semibold flex-1">Persistent memory</span>
199
- <button id="btn-memory-managed-refresh" class="btn btn-xs btn-ghost" title="Refresh persistent memory">↻</button>
200
- <button id="btn-memory-managed-export" class="btn btn-xs btn-ghost" title="Export persistent memory">Export</button>
201
- </div>
202
- <div id="memory-managed-list" class="flex flex-col gap-1 max-h-48 overflow-y-auto text-xs">
203
- <div class="opacity-40 py-1">Loading persistent memory…</div>
204
- </div>
195
+ <div id="memory-status" class="text-xs opacity-50 truncate">Loading index status…</div>
196
+ <div id="memory-governance" class="border-t border-base-content/10 pt-2 mt-1">
197
+ <div class="flex items-center gap-1 mb-1">
198
+ <span class="text-xs font-semibold flex-1">Persistent memory</span>
199
+ <button id="btn-memory-managed-refresh" class="btn btn-xs btn-ghost" title="Refresh persistent memory">↻</button>
200
+ <button id="btn-memory-managed-export" class="btn btn-xs btn-ghost" title="Export persistent memory">Export</button>
201
+ </div>
202
+ <div id="memory-managed-list" class="flex flex-col gap-1 max-h-48 overflow-y-auto text-xs">
203
+ <div class="opacity-40 py-1">Loading persistent memory…</div>
204
+ </div>
205
205
  </div>
206
206
  </div>
207
207
  <div id="memory-hits" class="flex-1 overflow-y-auto p-2 flex flex-col gap-2 text-sm">
@@ -366,8 +366,15 @@
366
366
  <form method="dialog" class="modal-backdrop"><button>close</button></form>
367
367
  </dialog>
368
368
 
369
- <script src="app.js"></script>
370
- <script src="templates.js"></script>
371
- <script src="actions.js"></script>
369
+ <!-- state.js stays a classic script (NOT a module): its top-level `let`
370
+ globals live in the shared global lexical environment so the ES modules
371
+ below can read AND reassign them as free variables. Must load first. -->
372
+ <script src="state.js"></script>
373
+ <!-- ES modules (P0-4 Phase 2). Ordered so top-level side effects run
374
+ app → templates → actions, matching the previous classic-script order.
375
+ util.js loads via `import` from app.js (no tag needed). -->
376
+ <script type="module" src="app.js"></script>
377
+ <script type="module" src="templates.js"></script>
378
+ <script type="module" src="actions.js"></script>
372
379
  </body>
373
380
  </html>
@@ -0,0 +1,103 @@
1
+ /**
2
+ * ai-cli Web UI — Central client state (P0-4 Phase 1)
3
+ *
4
+ * All cross-cutting mutable application state lives here, moved out of the
5
+ * 3400-line app.js so there is a single place to find "what state the UI holds".
6
+ *
7
+ * ── Runtime note (do NOT break) ──────────────────────────────────────────
8
+ * These are classic <script> files (not ES modules). Top-level `let`/`const`
9
+ * bindings go into the shared *global lexical environment*, so every binding
10
+ * declared here is visible — and, for `let`, reassignable — from app.js /
11
+ * templates.js / actions.js exactly as if they were still declared inline.
12
+ * This file MUST be loaded BEFORE app.js in index.html so the bindings exist
13
+ * before app.js's top-level DOM-ref code runs.
14
+ *
15
+ * ── Types ────────────────────────────────────────────────────────────────
16
+ * JSDoc typedefs below give editors/`tsc --checkJs` type information without
17
+ * introducing a build step (Phase 2 moves this to real .ts under Vite).
18
+ *
19
+ * @typedef {Object} SessionTab
20
+ * @property {string} id Local tab id (not the session id)
21
+ * @property {string|null} sessionId Backend session id, or null if unsaved
22
+ * @property {string} title Tab label
23
+ * @property {string} [messagesHtml] Snapshot of #messages innerHTML while inactive
24
+ * @property {number} [scrollPos] Saved scroll offset while inactive
25
+ * @property {*} [tokenUsage] Cached token/cost usage for the status bar
26
+ * @property {boolean} [isProcessing] Whether this tab has an in-flight generation
27
+ *
28
+ * @typedef {Object} PendingImage
29
+ * @property {string} name Original filename
30
+ * @property {string} data base64-encoded bytes
31
+ * @property {string} mime MIME type (e.g. image/png)
32
+ *
33
+ * @typedef {Object} ToolTimer
34
+ * @property {number} startTime performance/Date.now() when the tool started
35
+ * @property {number} intervalId setInterval handle updating the elapsed label
36
+ */
37
+
38
+ // ── Connection state ───────────────────────────────────────────────
39
+ /** @type {WebSocket|null} */
40
+ let ws = null;
41
+ let connected = false;
42
+ let processing = false;
43
+ /** @type {ReturnType<typeof setInterval>|null} */
44
+ let heartbeatTimer = null;
45
+ let reconnectDelay = 1000; // Start at 1s, exponential backoff
46
+
47
+ // ── Auth state ─────────────────────────────────────────────────────
48
+ // M4 fix (v0.4.107): the auth token now lives only in an httpOnly cookie.
49
+ // `authToken` remains as a legacy variable so existing call sites compile,
50
+ // but it should always be empty — the cookie is what authenticates the WS
51
+ // upgrade and HTTP requests via `credentials: 'same-origin'`.
52
+ let authToken = '';
53
+ // Clear any legacy localStorage token from an older client.
54
+ try { localStorage.removeItem('aicli-auth-token'); } catch {}
55
+ let authUsername = localStorage.getItem('aicli-auth-user') || '';
56
+ let authMode = 'login'; // 'login' or 'register'
57
+
58
+ // ── Streaming render state ─────────────────────────────────────────
59
+ /** @type {HTMLElement|null} */
60
+ let currentAssistantEl = null;
61
+ let currentAssistantContent = '';
62
+ /** @type {HTMLElement|null} */
63
+ let currentThinkingEl = null;
64
+ let currentThinkingContent = '';
65
+
66
+ // ── Data / input state ─────────────────────────────────────────────
67
+ /** @type {Array<*>} */
68
+ let providers = [];
69
+ /** @type {PendingImage[]} */
70
+ let pendingImages = [];
71
+ /** @type {string[]} */
72
+ let inputHistory = []; // Previous user inputs for ↑/↓ navigation
73
+ let historyIndex = -1; // -1 = not browsing history
74
+ let savedInputDraft = ''; // Saved current input when entering history mode
75
+ /** @type {Map<string, ToolTimer>} */
76
+ let toolTimers = new Map(); // callId → { startTime, intervalId }
77
+ /** @type {*} */
78
+ let latestStatus = null;
79
+ /** @type {Map<string, *>} */
80
+ let pendingApprovals = new Map(); // requestId -> approval metadata
81
+ /** @type {Array<*>} */
82
+ let lastToolResults = [];
83
+ /** @type {Array<*>} */
84
+ let replayCurrentMessages = [];
85
+ let replayCurrentFilter = 'all';
86
+ /** @type {Array<*>} */
87
+ let cachedSessions = [];
88
+ /** @type {*} */
89
+ let cachedToolsData = null;
90
+
91
+ // ── Multi-Tab state (P2-1) ────────────────────────────────────────
92
+ // Each "tab" represents an open session within the single page.
93
+ // Only one tab is active at a time; others store a DOM snapshot.
94
+ /** @type {SessionTab[]} */
95
+ let sessionTabs = [];
96
+ let activeTabIdx = -1; // Index into sessionTabs
97
+
98
+ // ── Tab identity (per browser tab, survives page reload) ──────────
99
+ let tabId = sessionStorage.getItem('aicli-tab-id');
100
+ if (!tabId) {
101
+ tabId = 'tab-' + Math.random().toString(36).slice(2, 10) + Math.random().toString(36).slice(2, 6);
102
+ sessionStorage.setItem('aicli-tab-id', tabId);
103
+ }
@@ -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', '/templates.js', '/actions.js']);
19
+ const NEVER_CACHE = new Set(['/', '/index.html', '/state.js', '/util.js', '/dom.js', '/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.
@@ -1,188 +1,191 @@
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
- });
1
+ /**
2
+ * Prompt template modal and import/export helpers (ES module — P0-4 Phase 2).
3
+ * Depends on shared helpers from app.js / util.js.
4
+ */
5
+
6
+ import { escapeHtml } from './util.js';
7
+ import { addInfoMessage, addErrorMessage } from './app.js';
8
+
9
+ const TEMPLATES_KEY = 'aicli-templates';
10
+
11
+ function loadTemplates() {
12
+ try {
13
+ return JSON.parse(localStorage.getItem(TEMPLATES_KEY) || '[]');
14
+ } catch {
15
+ return [];
16
+ }
17
+ }
18
+
19
+ function saveTemplatesToStorage(templates) {
20
+ localStorage.setItem(TEMPLATES_KEY, JSON.stringify(templates));
21
+ }
22
+
23
+ function openTemplatesModal() {
24
+ const modal = document.getElementById('templates-modal');
25
+ if (!modal) return;
26
+ cancelTemplateForm();
27
+ renderTemplateList();
28
+ document.getElementById('template-search').value = '';
29
+ modal.showModal();
30
+ }
31
+
32
+ function renderTemplateList(filter) {
33
+ const listEl = document.getElementById('template-list');
34
+ if (!listEl) return;
35
+ const templates = loadTemplates();
36
+ const q = (filter || '').toLowerCase();
37
+ const filtered = q
38
+ ? templates.filter(t => t.name.toLowerCase().includes(q) || (t.tags || []).some(tag => tag.toLowerCase().includes(q)) || t.content.toLowerCase().includes(q))
39
+ : templates;
40
+
41
+ if (filtered.length === 0) {
42
+ listEl.innerHTML = `<div class="template-empty">${q ? 'No matching templates' : 'No templates yet. Click + New to create one.'}</div>`;
43
+ return;
44
+ }
45
+
46
+ listEl.innerHTML = filtered.map(t => {
47
+ const preview = t.content.length > 80 ? t.content.slice(0, 80) + '...' : t.content;
48
+ const tagsHtml = (t.tags || []).map(tag => `<span class="template-item-tag">${escapeHtml(tag)}</span>`).join('');
49
+ return `
50
+ <div class="template-item" data-tpl-id="${t.id}" data-action="use-template">
51
+ <div class="template-item-body">
52
+ <div class="template-item-name">${escapeHtml(t.name)}</div>
53
+ <div class="template-item-preview">${escapeHtml(preview)}</div>
54
+ ${tagsHtml ? `<div class="template-item-tags">${tagsHtml}</div>` : ''}
55
+ </div>
56
+ <div class="template-item-actions">
57
+ <button class="btn btn-xs btn-ghost" data-action="edit-template" title="Edit">Edit</button>
58
+ <button class="btn btn-xs btn-ghost" data-action="delete-template" title="Delete">Delete</button>
59
+ </div>
60
+ </div>`;
61
+ }).join('');
62
+ }
63
+
64
+ export function useTemplate(id) {
65
+ const templates = loadTemplates();
66
+ const tpl = templates.find(t => t.id === id);
67
+ if (!tpl) return;
68
+ const input = document.getElementById('user-input');
69
+ if (!input) return;
70
+ input.value = tpl.content;
71
+ input.focus();
72
+ input.style.height = 'auto';
73
+ input.style.height = Math.min(input.scrollHeight, 200) + 'px';
74
+ document.getElementById('templates-modal')?.close();
75
+ }
76
+
77
+ export function showAddTemplate() {
78
+ const form = document.getElementById('template-form');
79
+ form.classList.remove('hidden');
80
+ document.getElementById('tpl-edit-id').value = '';
81
+ document.getElementById('tpl-name').value = '';
82
+ document.getElementById('tpl-content').value = '';
83
+ document.getElementById('tpl-tags').value = '';
84
+ document.getElementById('tpl-name').focus();
85
+ }
86
+
87
+ export function editTemplate(id) {
88
+ const templates = loadTemplates();
89
+ const tpl = templates.find(t => t.id === id);
90
+ if (!tpl) return;
91
+ const form = document.getElementById('template-form');
92
+ form.classList.remove('hidden');
93
+ document.getElementById('tpl-edit-id').value = tpl.id;
94
+ document.getElementById('tpl-name').value = tpl.name;
95
+ document.getElementById('tpl-content').value = tpl.content;
96
+ document.getElementById('tpl-tags').value = (tpl.tags || []).join(', ');
97
+ document.getElementById('tpl-name').focus();
98
+ }
99
+
100
+ export function cancelTemplateForm() {
101
+ document.getElementById('template-form')?.classList.add('hidden');
102
+ }
103
+
104
+ export function saveTemplateForm() {
105
+ const name = document.getElementById('tpl-name').value.trim();
106
+ const content = document.getElementById('tpl-content').value.trim();
107
+ const tagsRaw = document.getElementById('tpl-tags').value.trim();
108
+ const editId = document.getElementById('tpl-edit-id').value;
109
+
110
+ if (!name || !content) return;
111
+
112
+ const tags = tagsRaw ? tagsRaw.split(',').map(s => s.trim()).filter(Boolean) : [];
113
+ const templates = loadTemplates();
114
+
115
+ if (editId) {
116
+ const idx = templates.findIndex(t => t.id === editId);
117
+ if (idx >= 0) {
118
+ templates[idx].name = name;
119
+ templates[idx].content = content;
120
+ templates[idx].tags = tags;
121
+ }
122
+ } else {
123
+ templates.unshift({
124
+ id: 'tpl-' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
125
+ name,
126
+ content,
127
+ tags,
128
+ createdAt: new Date().toISOString(),
129
+ });
130
+ }
131
+
132
+ saveTemplatesToStorage(templates);
133
+ cancelTemplateForm();
134
+ renderTemplateList();
135
+ }
136
+
137
+ export function deleteTemplate(id) {
138
+ const templates = loadTemplates().filter(t => t.id !== id);
139
+ saveTemplatesToStorage(templates);
140
+ renderTemplateList(document.getElementById('template-search')?.value);
141
+ }
142
+
143
+ export function exportTemplates() {
144
+ const templates = loadTemplates();
145
+ if (templates.length === 0) return;
146
+ const blob = new Blob([JSON.stringify(templates, null, 2)], { type: 'application/json' });
147
+ const url = URL.createObjectURL(blob);
148
+ const a = document.createElement('a');
149
+ a.href = url;
150
+ a.download = `aicli-templates-${new Date().toISOString().slice(0, 10)}.json`;
151
+ a.click();
152
+ URL.revokeObjectURL(url);
153
+ }
154
+
155
+ export function importTemplates() {
156
+ const input = document.createElement('input');
157
+ input.type = 'file';
158
+ input.accept = '.json';
159
+ input.addEventListener('change', () => {
160
+ const file = input.files[0];
161
+ if (!file) return;
162
+ const reader = new FileReader();
163
+ reader.addEventListener('load', () => {
164
+ try {
165
+ const imported = JSON.parse(reader.result);
166
+ if (!Array.isArray(imported)) throw new Error('Not an array');
167
+ const existing = loadTemplates();
168
+ const existingIds = new Set(existing.map(t => t.id));
169
+ let added = 0;
170
+ for (const t of imported) {
171
+ if (t.id && t.name && t.content && !existingIds.has(t.id)) {
172
+ existing.push(t);
173
+ added++;
174
+ }
175
+ }
176
+ saveTemplatesToStorage(existing);
177
+ renderTemplateList();
178
+ addInfoMessage(`Imported ${added} template(s).`);
179
+ } catch {
180
+ addErrorMessage('Failed to import templates: invalid JSON format.');
181
+ }
182
+ });
183
+ reader.readAsText(file);
184
+ });
185
+ input.click();
186
+ }
187
+
188
+ document.getElementById('btn-templates')?.addEventListener('click', openTemplatesModal);
189
+ document.getElementById('template-search')?.addEventListener('input', (e) => {
190
+ renderTemplateList(e.target.value);
191
+ });