openclacky 1.0.0 โ 1.0.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +39 -0
- data/README.md +87 -53
- data/lib/clacky/agent/cost_tracker.rb +19 -2
- data/lib/clacky/agent/llm_caller.rb +218 -0
- data/lib/clacky/agent/message_compressor_helper.rb +32 -2
- data/lib/clacky/agent.rb +54 -22
- data/lib/clacky/client.rb +44 -5
- data/lib/clacky/default_parsers/pdf_parser.rb +58 -17
- data/lib/clacky/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/clacky/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +201 -77
- data/lib/clacky/default_skills/new/SKILL.md +3 -114
- data/lib/clacky/default_skills/onboard/SKILL.md +349 -133
- data/lib/clacky/default_skills/onboard/scripts/import_external_skills.rb +371 -0
- data/lib/clacky/default_skills/onboard/scripts/install_builtin_skills.rb +175 -0
- data/lib/clacky/default_skills/skill-add/scripts/install_from_zip.rb +59 -26
- data/lib/clacky/message_format/anthropic.rb +72 -8
- data/lib/clacky/message_format/bedrock.rb +6 -3
- data/lib/clacky/providers.rb +146 -3
- data/lib/clacky/server/channel/adapters/feishu/adapter.rb +14 -0
- data/lib/clacky/server/channel/adapters/feishu/bot.rb +10 -0
- data/lib/clacky/server/channel/adapters/feishu/message_parser.rb +1 -0
- data/lib/clacky/server/channel/channel_manager.rb +12 -4
- data/lib/clacky/server/channel/channel_ui_controller.rb +8 -2
- data/lib/clacky/server/http_server.rb +746 -13
- data/lib/clacky/server/session_registry.rb +55 -24
- data/lib/clacky/skill.rb +10 -9
- data/lib/clacky/skill_loader.rb +23 -11
- data/lib/clacky/tools/file_reader.rb +232 -127
- data/lib/clacky/tools/security.rb +42 -64
- data/lib/clacky/tools/terminal/persistent_session.rb +15 -4
- data/lib/clacky/tools/terminal/safe_rm.sh +106 -0
- data/lib/clacky/tools/terminal/session_manager.rb +8 -3
- data/lib/clacky/tools/terminal.rb +263 -16
- data/lib/clacky/ui2/layout_manager.rb +8 -1
- data/lib/clacky/ui2/output_buffer.rb +83 -23
- data/lib/clacky/ui2/ui_controller.rb +74 -7
- data/lib/clacky/utils/file_processor.rb +14 -40
- data/lib/clacky/utils/model_pricing.rb +215 -0
- data/lib/clacky/utils/parser_manager.rb +70 -6
- data/lib/clacky/utils/string_matcher.rb +23 -1
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +673 -9
- data/lib/clacky/web/app.js +40 -1608
- data/lib/clacky/web/i18n.js +209 -0
- data/lib/clacky/web/index.html +166 -2
- data/lib/clacky/web/onboard.js +77 -1
- data/lib/clacky/web/profile.js +442 -0
- data/lib/clacky/web/sessions.js +1034 -2
- data/lib/clacky/web/settings.js +127 -6
- data/lib/clacky/web/sidebar.js +39 -0
- data/lib/clacky/web/skills.js +460 -0
- data/lib/clacky/web/trash.js +343 -0
- data/lib/clacky/web/ws-dispatcher.js +255 -0
- data/lib/clacky.rb +5 -3
- metadata +16 -17
- data/lib/clacky/clacky_auth_client.rb +0 -152
- data/lib/clacky/clacky_cloud_config.rb +0 -123
- data/lib/clacky/cloud_project_client.rb +0 -169
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +0 -1377
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +0 -116
- data/lib/clacky/default_skills/deploy/tools/create_database_service.rb +0 -341
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +0 -99
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +0 -77
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +0 -67
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +0 -67
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +0 -189
- data/lib/clacky/default_skills/new/scripts/cloud_project_init.sh +0 -74
- data/lib/clacky/deploy_api_client.rb +0 -484
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
// profile.js โ Assistant Memory panel
|
|
2
|
+
//
|
|
3
|
+
// Three tabs, all read-only views with single-action buttons that delegate to
|
|
4
|
+
// the agent via slash-commands:
|
|
5
|
+
//
|
|
6
|
+
// ๐งฌ Soul โ SOUL.md rendered. Button opens /onboard scope:soul.
|
|
7
|
+
// ๐ค User โ USER.md rendered. Button opens /onboard scope:user.
|
|
8
|
+
// ๐ง Memories โ list of ~/.clacky/memories/*.md, sorted by updated_at desc.
|
|
9
|
+
// Per-card "Curate" opens /onboard path:<abs>.
|
|
10
|
+
// Per-card "Delete" calls DELETE /api/memories/:filename
|
|
11
|
+
// (with a confirm); the file lands in File Recall.
|
|
12
|
+
//
|
|
13
|
+
// The single `onboard` skill handles all three slash-command shapes: with no
|
|
14
|
+
// args it runs the full first-run ceremony; with `scope:` or `path:` it runs
|
|
15
|
+
// the corresponding light curate flow.
|
|
16
|
+
//
|
|
17
|
+
// Philosophy: the agent's inner state is never hand-edited. The UI shows it
|
|
18
|
+
// and offers curation buttons that start agent-led flows.
|
|
19
|
+
//
|
|
20
|
+
// Load order: after app.js modules (I18n, Sessions, Onboard), before boot.
|
|
21
|
+
|
|
22
|
+
const Profile = (() => {
|
|
23
|
+
let _wired = false;
|
|
24
|
+
let _activeTab = "soul";
|
|
25
|
+
let _data = { user: null, soul: null, memories: [] };
|
|
26
|
+
|
|
27
|
+
function $(id) { return document.getElementById(id); }
|
|
28
|
+
|
|
29
|
+
function _t(key, args) {
|
|
30
|
+
return (I18n && I18n.t) ? I18n.t(key, args) : key;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// โโ Minimal safe Markdown renderer โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
34
|
+
// Handles: # H1 / ## H2 / ### H3, **bold**, *em*, `code`,
|
|
35
|
+
// - / * bullets, numbered lists, blank-line paragraphs.
|
|
36
|
+
// Everything is HTML-escaped first, so raw user Markdown can never
|
|
37
|
+
// inject script/style/event attributes.
|
|
38
|
+
|
|
39
|
+
function _escapeHtml(s) {
|
|
40
|
+
return String(s ?? "")
|
|
41
|
+
.replace(/&/g, "&")
|
|
42
|
+
.replace(/</g, "<")
|
|
43
|
+
.replace(/>/g, ">")
|
|
44
|
+
.replace(/"/g, """)
|
|
45
|
+
.replace(/'/g, "'");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function _renderInline(text) {
|
|
49
|
+
// Inline: **bold**, *em*, `code`. Text is already HTML-escaped by caller.
|
|
50
|
+
return text
|
|
51
|
+
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
52
|
+
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
53
|
+
.replace(/(^|[^*])\*([^*\s][^*]*?)\*(?!\*)/g, "$1<em>$2</em>");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _renderMarkdown(raw) {
|
|
57
|
+
if (!raw || !raw.trim()) return "";
|
|
58
|
+
const escaped = _escapeHtml(raw);
|
|
59
|
+
const lines = escaped.split(/\r?\n/);
|
|
60
|
+
|
|
61
|
+
const out = [];
|
|
62
|
+
let listType = null; // "ul" | "ol" | null
|
|
63
|
+
let paraBuf = [];
|
|
64
|
+
|
|
65
|
+
function flushPara() {
|
|
66
|
+
if (paraBuf.length === 0) return;
|
|
67
|
+
out.push("<p>" + _renderInline(paraBuf.join(" ")) + "</p>");
|
|
68
|
+
paraBuf = [];
|
|
69
|
+
}
|
|
70
|
+
function openList(type) {
|
|
71
|
+
if (listType !== type) {
|
|
72
|
+
closeList();
|
|
73
|
+
out.push("<" + type + ">");
|
|
74
|
+
listType = type;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function closeList() {
|
|
78
|
+
if (listType) { out.push("</" + listType + ">"); listType = null; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const line = lines[i];
|
|
83
|
+
const trimmed = line.trim();
|
|
84
|
+
|
|
85
|
+
if (trimmed === "") { flushPara(); closeList(); continue; }
|
|
86
|
+
|
|
87
|
+
let m;
|
|
88
|
+
if ((m = trimmed.match(/^(#{1,3})\s+(.+)$/))) {
|
|
89
|
+
flushPara(); closeList();
|
|
90
|
+
const level = m[1].length;
|
|
91
|
+
out.push(`<h${level}>` + _renderInline(m[2]) + `</h${level}>`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if ((m = trimmed.match(/^[-*]\s+(.+)$/))) {
|
|
95
|
+
flushPara(); openList("ul");
|
|
96
|
+
out.push("<li>" + _renderInline(m[1]) + "</li>");
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if ((m = trimmed.match(/^\d+\.\s+(.+)$/))) {
|
|
100
|
+
flushPara(); openList("ol");
|
|
101
|
+
out.push("<li>" + _renderInline(m[1]) + "</li>");
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (listType) closeList();
|
|
105
|
+
paraBuf.push(trimmed);
|
|
106
|
+
}
|
|
107
|
+
flushPara(); closeList();
|
|
108
|
+
return out.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _stripFrontmatter(content) {
|
|
112
|
+
if (!content || !content.startsWith("---")) return content || "";
|
|
113
|
+
const m = content.match(/^---\s*\n[\s\S]*?\n---\s*\n?/);
|
|
114
|
+
return m ? content.slice(m[0].length) : content;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function _humanBytes(n) {
|
|
118
|
+
if (!n || n < 0) return "0 B";
|
|
119
|
+
const units = ["B", "KB", "MB"];
|
|
120
|
+
let i = 0;
|
|
121
|
+
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++; }
|
|
122
|
+
return (i === 0 ? n.toFixed(0) : n.toFixed(2)) + " " + units[i];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// โโ Data loading โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
126
|
+
|
|
127
|
+
async function _loadProfile() {
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetch("/api/profile");
|
|
130
|
+
const data = await res.json();
|
|
131
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "Load failed");
|
|
132
|
+
_data.user = data.user;
|
|
133
|
+
_data.soul = data.soul;
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error("[Profile] load profile failed", e);
|
|
136
|
+
_data.user = null;
|
|
137
|
+
_data.soul = null;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function _loadMemories() {
|
|
142
|
+
try {
|
|
143
|
+
const res = await fetch("/api/memories");
|
|
144
|
+
const data = await res.json();
|
|
145
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "Load failed");
|
|
146
|
+
_data.memories = data.memories || [];
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.error("[Profile] load memories failed", e);
|
|
149
|
+
_data.memories = [];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// โโ Rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
154
|
+
|
|
155
|
+
function _renderIdentitySection(kind) {
|
|
156
|
+
const file = _data[kind];
|
|
157
|
+
const wrap = $(`profile-${kind}-body`);
|
|
158
|
+
const status = $(`profile-${kind}-status`);
|
|
159
|
+
const pathEl = $(`profile-${kind}-path`);
|
|
160
|
+
if (!wrap) return;
|
|
161
|
+
|
|
162
|
+
if (!file) {
|
|
163
|
+
wrap.innerHTML = `<div class="profile-empty">${_t("profile.loadFail")}</div>`;
|
|
164
|
+
if (status) { status.textContent = ""; status.className = "profile-status"; }
|
|
165
|
+
if (pathEl) pathEl.textContent = "";
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
wrap.innerHTML = _renderMarkdown(file.content || "")
|
|
170
|
+
|| `<div class="profile-empty">${_t("profile.emptyContent")}</div>`;
|
|
171
|
+
if (pathEl) pathEl.textContent = file.path || "";
|
|
172
|
+
if (status) {
|
|
173
|
+
status.textContent = file.is_default
|
|
174
|
+
? _t("profile.statusDefault")
|
|
175
|
+
: _t("profile.statusCustom");
|
|
176
|
+
status.className = "profile-status "
|
|
177
|
+
+ (file.is_default ? "profile-status-default" : "profile-status-custom");
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function _renderMemories() {
|
|
182
|
+
const list = $("memories-list");
|
|
183
|
+
const summary = $("memories-summary");
|
|
184
|
+
if (!list) return;
|
|
185
|
+
|
|
186
|
+
if (summary) {
|
|
187
|
+
summary.textContent = _data.memories.length
|
|
188
|
+
? _t("memories.summary", { count: _data.memories.length })
|
|
189
|
+
: _t("memories.emptyHint");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (_data.memories.length === 0) {
|
|
193
|
+
list.innerHTML = `<div class="profile-empty">${_t("memories.empty")}</div>`;
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
list.innerHTML = "";
|
|
198
|
+
_data.memories.forEach(m => list.appendChild(_buildMemoryCard(m)));
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function _buildMemoryCard(m) {
|
|
202
|
+
const card = document.createElement("div");
|
|
203
|
+
card.className = "memory-card";
|
|
204
|
+
card.dataset.filename = m.filename;
|
|
205
|
+
|
|
206
|
+
const topic = m.topic || m.filename;
|
|
207
|
+
const desc = m.description || "";
|
|
208
|
+
const updated = m.updated_at || "";
|
|
209
|
+
const size = _humanBytes(m.size || 0);
|
|
210
|
+
|
|
211
|
+
const head = document.createElement("div");
|
|
212
|
+
head.className = "memory-card-head";
|
|
213
|
+
head.innerHTML = `
|
|
214
|
+
<div class="memory-card-info">
|
|
215
|
+
<div class="memory-card-title" title="${_escapeHtml(m.filename)}">${_escapeHtml(topic)}</div>
|
|
216
|
+
${desc ? `<div class="memory-card-desc">${_escapeHtml(desc)}</div>` : ""}
|
|
217
|
+
<div class="memory-card-meta">
|
|
218
|
+
<span class="memory-filename">${_escapeHtml(m.filename)}</span>
|
|
219
|
+
<span>${_escapeHtml(updated)}</span>
|
|
220
|
+
<span>${size}</span>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
<div class="memory-card-actions">
|
|
224
|
+
<button class="btn-memory-curate" title="${_t("memories.curateTitle")}">
|
|
225
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
226
|
+
<path d="M12 20h9"/>
|
|
227
|
+
<path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
|
228
|
+
</svg>
|
|
229
|
+
<span>${_t("memories.curate")}</span>
|
|
230
|
+
</button>
|
|
231
|
+
<button class="btn-memory-delete" title="${_t("memories.deleteTitle")}">
|
|
232
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
233
|
+
<polyline points="3 6 5 6 21 6"/>
|
|
234
|
+
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
|
|
235
|
+
<path d="M10 11v6"/><path d="M14 11v6"/>
|
|
236
|
+
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>
|
|
237
|
+
</svg>
|
|
238
|
+
<span>${_t("memories.delete")}</span>
|
|
239
|
+
</button>
|
|
240
|
+
<button class="btn-memory-expand" title="${_t("memories.expandTitle")}" aria-expanded="false">
|
|
241
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
242
|
+
<polyline points="6 9 12 15 18 9"/>
|
|
243
|
+
</svg>
|
|
244
|
+
</button>
|
|
245
|
+
</div>`;
|
|
246
|
+
card.appendChild(head);
|
|
247
|
+
|
|
248
|
+
// Collapsible body โ rendered lazily on first expand.
|
|
249
|
+
const body = document.createElement("div");
|
|
250
|
+
body.className = "memory-card-body";
|
|
251
|
+
body.style.display = "none";
|
|
252
|
+
card.appendChild(body);
|
|
253
|
+
|
|
254
|
+
head.querySelector(".btn-memory-curate")
|
|
255
|
+
.addEventListener("click", (e) => { e.stopPropagation(); _curateMemory(m); });
|
|
256
|
+
|
|
257
|
+
head.querySelector(".btn-memory-delete")
|
|
258
|
+
.addEventListener("click", (e) => { e.stopPropagation(); _deleteMemory(m); });
|
|
259
|
+
|
|
260
|
+
const expandBtn = head.querySelector(".btn-memory-expand");
|
|
261
|
+
function toggle() {
|
|
262
|
+
const open = body.style.display !== "none";
|
|
263
|
+
if (open) {
|
|
264
|
+
body.style.display = "none";
|
|
265
|
+
expandBtn.setAttribute("aria-expanded", "false");
|
|
266
|
+
expandBtn.classList.remove("expanded");
|
|
267
|
+
} else {
|
|
268
|
+
if (!body.dataset.loaded) {
|
|
269
|
+
body.innerHTML = `<div class="memory-card-loading">${_t("memories.loading")}</div>`;
|
|
270
|
+
fetch("/api/memories/" + encodeURIComponent(m.filename))
|
|
271
|
+
.then(r => r.json())
|
|
272
|
+
.then(d => {
|
|
273
|
+
if (!d.ok) throw new Error(d.error || "Load failed");
|
|
274
|
+
const stripped = _stripFrontmatter(d.content || "");
|
|
275
|
+
body.innerHTML = _renderMarkdown(stripped)
|
|
276
|
+
|| `<div class="profile-empty">${_t("profile.emptyContent")}</div>`;
|
|
277
|
+
body.dataset.loaded = "1";
|
|
278
|
+
})
|
|
279
|
+
.catch(err => {
|
|
280
|
+
body.innerHTML = `<div class="profile-empty">${_escapeHtml(err.message)}</div>`;
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
body.style.display = "";
|
|
284
|
+
expandBtn.setAttribute("aria-expanded", "true");
|
|
285
|
+
expandBtn.classList.add("expanded");
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
expandBtn.addEventListener("click", (e) => { e.stopPropagation(); toggle(); });
|
|
289
|
+
// Clicking the info area also toggles, for a larger hit-target.
|
|
290
|
+
head.querySelector(".memory-card-info")
|
|
291
|
+
.addEventListener("click", toggle);
|
|
292
|
+
|
|
293
|
+
return card;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// โโ Tabs โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
297
|
+
|
|
298
|
+
function _switchTab(tab) {
|
|
299
|
+
if (!tab || tab === _activeTab) return;
|
|
300
|
+
_activeTab = tab;
|
|
301
|
+
|
|
302
|
+
document.querySelectorAll(".profile-tab").forEach(el => {
|
|
303
|
+
const isActive = el.dataset.tab === tab;
|
|
304
|
+
el.classList.toggle("active", isActive);
|
|
305
|
+
el.setAttribute("aria-selected", isActive ? "true" : "false");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
["soul", "user", "memories"].forEach(name => {
|
|
309
|
+
const pane = $(`profile-pane-${name}`);
|
|
310
|
+
if (!pane) return;
|
|
311
|
+
const isActive = name === tab;
|
|
312
|
+
pane.classList.toggle("active", isActive);
|
|
313
|
+
pane.style.display = isActive ? "" : "none";
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// โโ Actions โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
318
|
+
|
|
319
|
+
// Curate one of the identity files via /onboard scope:<soul|user>.
|
|
320
|
+
async function _curateProfile(scope) {
|
|
321
|
+
const btn = $(`btn-profile-curate-${scope}`);
|
|
322
|
+
if (btn) btn.disabled = true;
|
|
323
|
+
try {
|
|
324
|
+
const lang = (I18n && I18n.lang) ? I18n.lang() : "en";
|
|
325
|
+
const sessionName = _t(
|
|
326
|
+
scope === "soul" ? "profile.curateName.soul" : "profile.curateName.user"
|
|
327
|
+
);
|
|
328
|
+
const res = await fetch("/api/sessions", {
|
|
329
|
+
method: "POST",
|
|
330
|
+
headers: { "Content-Type": "application/json" },
|
|
331
|
+
body: JSON.stringify({ name: sessionName, source: "onboard" })
|
|
332
|
+
});
|
|
333
|
+
const data = await res.json();
|
|
334
|
+
const session = data.session;
|
|
335
|
+
if (!session) throw new Error("No session returned");
|
|
336
|
+
|
|
337
|
+
Sessions.add(session);
|
|
338
|
+
Sessions.renderList();
|
|
339
|
+
Sessions.setPendingMessage(
|
|
340
|
+
session.id,
|
|
341
|
+
`/onboard scope:${scope} lang:${lang}`
|
|
342
|
+
);
|
|
343
|
+
Sessions.select(session.id);
|
|
344
|
+
} catch (e) {
|
|
345
|
+
console.error("[Profile] curate profile failed", e);
|
|
346
|
+
alert(_t("profile.curateFail") + ": " + e.message);
|
|
347
|
+
if (btn) btn.disabled = false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Curate a single memory โ /onboard path:<abs> session.
|
|
352
|
+
async function _curateMemory(m) {
|
|
353
|
+
const absPath = m.path || ("~/.clacky/memories/" + m.filename);
|
|
354
|
+
try {
|
|
355
|
+
const name = _t("memories.curateName") + " ยท " + (m.topic || m.filename);
|
|
356
|
+
const res = await fetch("/api/sessions", {
|
|
357
|
+
method: "POST",
|
|
358
|
+
headers: { "Content-Type": "application/json" },
|
|
359
|
+
body: JSON.stringify({ name, source: "onboard" })
|
|
360
|
+
});
|
|
361
|
+
const data = await res.json();
|
|
362
|
+
const session = data.session;
|
|
363
|
+
if (!session) throw new Error("No session returned");
|
|
364
|
+
|
|
365
|
+
Sessions.add(session);
|
|
366
|
+
Sessions.renderList();
|
|
367
|
+
Sessions.setPendingMessage(session.id, `/onboard path:${absPath}`);
|
|
368
|
+
Sessions.select(session.id);
|
|
369
|
+
} catch (e) {
|
|
370
|
+
console.error("[Profile] curate memory failed", e);
|
|
371
|
+
alert(_t("memories.curateFail") + ": " + e.message);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Delete a memory directly. Backend uses `trash` semantics so it lands in
|
|
376
|
+
// File Recall and can still be recovered.
|
|
377
|
+
async function _deleteMemory(m) {
|
|
378
|
+
const label = m.topic || m.filename;
|
|
379
|
+
if (!confirm(_t("memories.confirmDelete", { name: label }))) return;
|
|
380
|
+
|
|
381
|
+
try {
|
|
382
|
+
const res = await fetch(
|
|
383
|
+
"/api/memories/" + encodeURIComponent(m.filename),
|
|
384
|
+
{ method: "DELETE" }
|
|
385
|
+
);
|
|
386
|
+
const data = await res.json().catch(() => ({}));
|
|
387
|
+
if (!res.ok || !data.ok) {
|
|
388
|
+
throw new Error(data.error || `HTTP ${res.status}`);
|
|
389
|
+
}
|
|
390
|
+
// Optimistic local remove + re-render.
|
|
391
|
+
_data.memories = _data.memories.filter(x => x.filename !== m.filename);
|
|
392
|
+
_renderMemories();
|
|
393
|
+
} catch (e) {
|
|
394
|
+
console.error("[Profile] delete memory failed", e);
|
|
395
|
+
alert(_t("memories.deleteFail") + ": " + e.message);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// โโ Wiring โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
400
|
+
|
|
401
|
+
function _wire() {
|
|
402
|
+
if (_wired) return;
|
|
403
|
+
_wired = true;
|
|
404
|
+
|
|
405
|
+
// Tabs
|
|
406
|
+
document.querySelectorAll(".profile-tab").forEach(el => {
|
|
407
|
+
el.addEventListener("click", () => _switchTab(el.dataset.tab));
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Per-tab curate buttons
|
|
411
|
+
const soulBtn = $("btn-profile-curate-soul");
|
|
412
|
+
if (soulBtn) soulBtn.addEventListener("click", () => _curateProfile("soul"));
|
|
413
|
+
const userBtn = $("btn-profile-curate-user");
|
|
414
|
+
if (userBtn) userBtn.addEventListener("click", () => _curateProfile("user"));
|
|
415
|
+
|
|
416
|
+
// Memories list reload
|
|
417
|
+
const refreshMemBtn = $("btn-memories-refresh-list");
|
|
418
|
+
if (refreshMemBtn) refreshMemBtn.addEventListener("click", () => _loadAndRender());
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
async function _loadAndRender() {
|
|
422
|
+
await Promise.all([_loadProfile(), _loadMemories()]);
|
|
423
|
+
_renderIdentitySection("soul");
|
|
424
|
+
_renderIdentitySection("user");
|
|
425
|
+
_renderMemories();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// โโ Public API โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
onPanelShow() {
|
|
432
|
+
_wire();
|
|
433
|
+
// Re-enable curate buttons on every panel entry โ they may have been
|
|
434
|
+
// disabled by a prior click that navigated away.
|
|
435
|
+
["soul", "user"].forEach(s => {
|
|
436
|
+
const b = $(`btn-profile-curate-${s}`);
|
|
437
|
+
if (b) b.disabled = false;
|
|
438
|
+
});
|
|
439
|
+
_loadAndRender();
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
})();
|