openclacky 1.3.1 → 1.3.3
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 +44 -0
- data/Dockerfile +3 -0
- data/README.md +1 -1
- data/README_JA.md +237 -0
- data/lib/clacky/agent/session_serializer.rb +65 -11
- data/lib/clacky/agent/time_machine.rb +247 -26
- data/lib/clacky/agent.rb +12 -1
- data/lib/clacky/agent_config.rb +14 -2
- data/lib/clacky/brand_config.rb +1 -1
- data/lib/clacky/default_agents/_panels/git/panel.js +201 -0
- data/lib/clacky/default_agents/_panels/time_machine/panel.js +640 -0
- data/lib/clacky/default_agents/coding/profile.yml +3 -0
- data/lib/clacky/default_agents/coding/webui/.gitkeep +0 -0
- data/lib/clacky/default_skills/cron-task-creator/SKILL.md +1 -1
- data/lib/clacky/default_skills/extend-openclacky/SKILL.md +6 -4
- data/lib/clacky/default_skills/media-gen/SKILL.md +30 -6
- data/lib/clacky/media/openai_compat.rb +64 -1
- data/lib/clacky/media/output_dir.rb +43 -0
- data/lib/clacky/message_history.rb +9 -0
- data/lib/clacky/server/channel/channel_manager.rb +26 -0
- data/lib/clacky/server/git_panel.rb +115 -0
- data/lib/clacky/server/http_server.rb +521 -13
- data/lib/clacky/server/server_master.rb +6 -4
- data/lib/clacky/utils/environment_detector.rb +16 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky/web/app.css +512 -60
- data/lib/clacky/web/app.js +30 -7
- data/lib/clacky/web/components/code-editor.js +197 -0
- data/lib/clacky/web/{notify.js → components/notify.js} +1 -1
- data/lib/clacky/web/core/aside.js +112 -0
- data/lib/clacky/web/core/ext.js +387 -0
- data/lib/clacky/web/features/backup/store.js +92 -0
- data/lib/clacky/web/features/backup/view.js +94 -0
- data/lib/clacky/web/features/billing/store.js +163 -0
- data/lib/clacky/web/{billing.js → features/billing/view.js} +134 -242
- data/lib/clacky/web/features/brand/store.js +110 -0
- data/lib/clacky/web/{brand.js → features/brand/view.js} +49 -199
- data/lib/clacky/web/features/channels/store.js +103 -0
- data/lib/clacky/web/{channels.js → features/channels/view.js} +50 -127
- data/lib/clacky/web/features/creator/store.js +81 -0
- data/lib/clacky/web/{creator.js → features/creator/view.js} +53 -102
- data/lib/clacky/web/features/mcp/store.js +158 -0
- data/lib/clacky/web/{mcp.js → features/mcp/view.js} +57 -134
- data/lib/clacky/web/features/model-tester/store.js +77 -0
- data/lib/clacky/web/features/model-tester/view.js +7 -0
- data/lib/clacky/web/features/profile/store.js +170 -0
- data/lib/clacky/web/{profile.js → features/profile/view.js} +94 -144
- data/lib/clacky/web/features/share/store.js +145 -0
- data/lib/clacky/web/{share.js → features/share/view.js} +66 -202
- data/lib/clacky/web/features/skills/store.js +303 -0
- data/lib/clacky/web/features/skills/view.js +550 -0
- data/lib/clacky/web/features/tasks/store.js +135 -0
- data/lib/clacky/web/features/tasks/view.js +241 -0
- data/lib/clacky/web/features/trash/store.js +242 -0
- data/lib/clacky/web/{trash.js → features/trash/view.js} +102 -293
- data/lib/clacky/web/features/version/store.js +165 -0
- data/lib/clacky/web/features/version/view.js +323 -0
- data/lib/clacky/web/features/workspace/store.js +99 -0
- data/lib/clacky/web/features/workspace/view.js +305 -0
- data/lib/clacky/web/i18n.js +60 -6
- data/lib/clacky/web/index.html +117 -57
- data/lib/clacky/web/sessions.js +221 -25
- data/lib/clacky/web/settings.js +121 -25
- data/lib/clacky/web/skills.js +3 -821
- data/lib/clacky/web/vendor/codemirror/codemirror.min.js +29 -0
- data/lib/clacky.rb +1 -0
- metadata +45 -20
- data/lib/clacky/web/backup.js +0 -119
- data/lib/clacky/web/model-tester.js +0 -66
- data/lib/clacky/web/tasks.js +0 -365
- data/lib/clacky/web/version.js +0 -449
- data/lib/clacky/web/workspace.js +0 -212
- /data/lib/clacky/web/{notify.mp3 → assets/notify.mp3} +0 -0
- /data/lib/clacky/web/{datepicker.js → components/datepicker.js} +0 -0
- /data/lib/clacky/web/{onboard.js → components/onboard.js} +0 -0
- /data/lib/clacky/web/{sidebar.js → components/sidebar.js} +0 -0
- /data/lib/clacky/web/{marked.min.js → vendor/marked/marked.min.js} +0 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// ── Profile · store — Soul/User/memories data + curate/delete network ─────
|
|
2
|
+
//
|
|
3
|
+
// Owns the profile data (soul / user identity files, memories list) and the
|
|
4
|
+
// network calls: load profile, load memories, fetch one memory, delete a
|
|
5
|
+
// memory, and open agent-led curate sessions. It never renders.
|
|
6
|
+
//
|
|
7
|
+
// Emits store events the view reacts to; mirrors them to the extension bus via
|
|
8
|
+
// Clacky.ext.emit.
|
|
9
|
+
//
|
|
10
|
+
// `Profile` stays the single public facade.
|
|
11
|
+
//
|
|
12
|
+
// Depends on: Sessions, I18n, Clacky.ext.
|
|
13
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
const ProfileStore = (() => {
|
|
16
|
+
let _data = { user: null, soul: null, memories: [] };
|
|
17
|
+
|
|
18
|
+
const _listeners = {};
|
|
19
|
+
|
|
20
|
+
function _on(event, handler) {
|
|
21
|
+
(_listeners[event] ||= []).push(handler);
|
|
22
|
+
return () => {
|
|
23
|
+
const list = _listeners[event];
|
|
24
|
+
const i = list ? list.indexOf(handler) : -1;
|
|
25
|
+
if (i >= 0) list.splice(i, 1);
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function _emit(event, payload) {
|
|
30
|
+
(_listeners[event] || []).forEach((h) => h(payload));
|
|
31
|
+
if (window.Clacky && Clacky.ext) Clacky.ext.emit(event, payload);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const state = {
|
|
35
|
+
get user() { return _data.user; },
|
|
36
|
+
get soul() { return _data.soul; },
|
|
37
|
+
get memories() { return _data.memories; },
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
async function _loadProfile() {
|
|
41
|
+
try {
|
|
42
|
+
const res = await fetch("/api/profile");
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "Load failed");
|
|
45
|
+
_data.user = data.user;
|
|
46
|
+
_data.soul = data.soul;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.error("[Profile] load profile failed", e);
|
|
49
|
+
_data.user = null;
|
|
50
|
+
_data.soul = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function _loadMemories() {
|
|
55
|
+
try {
|
|
56
|
+
const res = await fetch("/api/memories");
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "Load failed");
|
|
59
|
+
_data.memories = data.memories || [];
|
|
60
|
+
} catch (e) {
|
|
61
|
+
console.error("[Profile] load memories failed", e);
|
|
62
|
+
_data.memories = [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function _openCurateSession(name, command) {
|
|
67
|
+
const res = await fetch("/api/sessions", {
|
|
68
|
+
method: "POST",
|
|
69
|
+
headers: { "Content-Type": "application/json" },
|
|
70
|
+
body: JSON.stringify({ name, source: "onboard" })
|
|
71
|
+
});
|
|
72
|
+
const data = await res.json();
|
|
73
|
+
const session = data.session;
|
|
74
|
+
if (!session) throw new Error("No session returned");
|
|
75
|
+
|
|
76
|
+
Sessions.add(session);
|
|
77
|
+
Sessions.renderList();
|
|
78
|
+
Sessions.setPendingMessage(session.id, command);
|
|
79
|
+
Sessions.select(session.id);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const Profile = {
|
|
83
|
+
on: _on,
|
|
84
|
+
state,
|
|
85
|
+
|
|
86
|
+
async loadAll() {
|
|
87
|
+
await Promise.all([_loadProfile(), _loadMemories()]);
|
|
88
|
+
_emit("profile:changed");
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
/** Fetch one memory's raw content. Returns { ok, content, error }. */
|
|
92
|
+
async fetchMemory(filename) {
|
|
93
|
+
try {
|
|
94
|
+
const res = await fetch("/api/memories/" + encodeURIComponent(filename));
|
|
95
|
+
const data = await res.json();
|
|
96
|
+
if (!data.ok) throw new Error(data.error || "Load failed");
|
|
97
|
+
return { ok: true, content: data.content || "" };
|
|
98
|
+
} catch (e) {
|
|
99
|
+
return { ok: false, error: e.message };
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
/** Delete a memory (trash semantics). Returns { ok, error }. */
|
|
104
|
+
async deleteMemory(filename) {
|
|
105
|
+
try {
|
|
106
|
+
const res = await fetch("/api/memories/" + encodeURIComponent(filename), { method: "DELETE" });
|
|
107
|
+
const data = await res.json().catch(() => ({}));
|
|
108
|
+
if (!res.ok || !data.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
109
|
+
_data.memories = _data.memories.filter(x => x.filename !== filename);
|
|
110
|
+
_emit("profile:memoriesChanged");
|
|
111
|
+
return { ok: true };
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.error("[Profile] delete memory failed", e);
|
|
114
|
+
return { ok: false, error: e.message };
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
/** Open an /onboard scope:<soul|user> curate session. */
|
|
119
|
+
async curateProfile(scope) {
|
|
120
|
+
const lang = (I18n && I18n.lang) ? I18n.lang() : "en";
|
|
121
|
+
const sessionName = (I18n && I18n.t)
|
|
122
|
+
? I18n.t(scope === "soul" ? "profile.curateName.soul" : "profile.curateName.user")
|
|
123
|
+
: scope;
|
|
124
|
+
await _openCurateSession(sessionName, `/onboard scope:${scope} lang:${lang}`);
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
/** Update SOUL.md or USER.md content. Returns { ok, error }. */
|
|
128
|
+
async updateProfile(kind, content) {
|
|
129
|
+
const res = await fetch("/api/profile", {
|
|
130
|
+
method: "PUT",
|
|
131
|
+
headers: { "Content-Type": "application/json" },
|
|
132
|
+
body: JSON.stringify({ kind, content })
|
|
133
|
+
});
|
|
134
|
+
const data = await res.json();
|
|
135
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "Save failed");
|
|
136
|
+
await Profile.loadAll();
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
/** Update a memory file's content. Returns { ok, error }. */
|
|
140
|
+
async updateMemory(filename, content) {
|
|
141
|
+
try {
|
|
142
|
+
const res = await fetch("/api/memories/" + encodeURIComponent(filename), {
|
|
143
|
+
method: "PUT",
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
body: JSON.stringify({ content })
|
|
146
|
+
});
|
|
147
|
+
const data = await res.json();
|
|
148
|
+
if (!res.ok || !data.ok) throw new Error(data.error || "Save failed");
|
|
149
|
+
await _loadMemories();
|
|
150
|
+
_emit("profile:memoriesChanged");
|
|
151
|
+
return { ok: true };
|
|
152
|
+
} catch (e) {
|
|
153
|
+
console.error("[Profile] update memory failed", e);
|
|
154
|
+
return { ok: false, error: e.message };
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
/** Open an /onboard path:<abs> curate session for a memory. */
|
|
159
|
+
async curateMemory(m) {
|
|
160
|
+
const absPath = m.path || ("~/.clacky/memories/" + m.filename);
|
|
161
|
+
const base = (I18n && I18n.t) ? I18n.t("memories.curateName") : "Curate";
|
|
162
|
+
const name = base + " · " + (m.topic || m.filename);
|
|
163
|
+
await _openCurateSession(name, `/onboard path:${absPath}`);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return Profile;
|
|
168
|
+
})();
|
|
169
|
+
|
|
170
|
+
const Profile = ProfileStore;
|
|
@@ -1,28 +1,17 @@
|
|
|
1
|
-
//
|
|
1
|
+
// ── Profile · view — markdown render, tabs, memory cards, DOM wiring ───────
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
3
|
+
// Owns the safe Markdown renderer, identity/memory rendering, tab switching,
|
|
4
|
+
// and all DOM wiring. Reads through ProfileStore.state; load/curate/delete go
|
|
5
|
+
// through store actions. Confirm dialogs and alerts (UI concerns) live here.
|
|
5
6
|
//
|
|
6
|
-
//
|
|
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.
|
|
7
|
+
// Augments the `Profile` facade with onPanelShow.
|
|
12
8
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
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.
|
|
9
|
+
// Depends on: ProfileStore, I18n.
|
|
10
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
21
11
|
|
|
22
|
-
const
|
|
12
|
+
const ProfileView = (() => {
|
|
23
13
|
let _wired = false;
|
|
24
14
|
let _activeTab = "soul";
|
|
25
|
-
let _data = { user: null, soul: null, memories: [] };
|
|
26
15
|
|
|
27
16
|
function $(id) { return document.getElementById(id); }
|
|
28
17
|
|
|
@@ -30,11 +19,8 @@ const Profile = (() => {
|
|
|
30
19
|
return (I18n && I18n.t) ? I18n.t(key, args) : key;
|
|
31
20
|
}
|
|
32
21
|
|
|
33
|
-
// ── Minimal safe Markdown renderer
|
|
34
|
-
//
|
|
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.
|
|
22
|
+
// ── Minimal safe Markdown renderer ──────────────────────────────────────
|
|
23
|
+
// HTML-escapes first, so raw Markdown can never inject script/style/events.
|
|
38
24
|
|
|
39
25
|
function _escapeHtml(s) {
|
|
40
26
|
return String(s ?? "")
|
|
@@ -46,7 +32,6 @@ const Profile = (() => {
|
|
|
46
32
|
}
|
|
47
33
|
|
|
48
34
|
function _renderInline(text) {
|
|
49
|
-
// Inline: **bold**, *em*, `code`. Text is already HTML-escaped by caller.
|
|
50
35
|
return text
|
|
51
36
|
.replace(/`([^`]+)`/g, "<code>$1</code>")
|
|
52
37
|
.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>")
|
|
@@ -59,7 +44,7 @@ const Profile = (() => {
|
|
|
59
44
|
const lines = escaped.split(/\r?\n/);
|
|
60
45
|
|
|
61
46
|
const out = [];
|
|
62
|
-
let listType = null;
|
|
47
|
+
let listType = null;
|
|
63
48
|
let paraBuf = [];
|
|
64
49
|
|
|
65
50
|
function flushPara() {
|
|
@@ -122,38 +107,10 @@ const Profile = (() => {
|
|
|
122
107
|
return (i === 0 ? n.toFixed(0) : n.toFixed(2)) + " " + units[i];
|
|
123
108
|
}
|
|
124
109
|
|
|
125
|
-
// ──
|
|
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 ────────────────────────────────────────────────────────
|
|
110
|
+
// ── Rendering ────────────────────────────────────────────────────────────
|
|
154
111
|
|
|
155
112
|
function _renderIdentitySection(kind) {
|
|
156
|
-
const file =
|
|
113
|
+
const file = ProfileStore.state[kind];
|
|
157
114
|
const wrap = $(`profile-${kind}-body`);
|
|
158
115
|
const status = $(`profile-${kind}-status`);
|
|
159
116
|
const pathEl = $(`profile-${kind}-path`);
|
|
@@ -183,19 +140,20 @@ const Profile = (() => {
|
|
|
183
140
|
const summary = $("memories-summary");
|
|
184
141
|
if (!list) return;
|
|
185
142
|
|
|
143
|
+
const memories = ProfileStore.state.memories;
|
|
186
144
|
if (summary) {
|
|
187
|
-
summary.textContent =
|
|
188
|
-
? _t("memories.summary", { count:
|
|
145
|
+
summary.textContent = memories.length
|
|
146
|
+
? _t("memories.summary", { count: memories.length })
|
|
189
147
|
: _t("memories.emptyHint");
|
|
190
148
|
}
|
|
191
149
|
|
|
192
|
-
if (
|
|
150
|
+
if (memories.length === 0) {
|
|
193
151
|
list.innerHTML = `<div class="profile-empty">${_t("memories.empty")}</div>`;
|
|
194
152
|
return;
|
|
195
153
|
}
|
|
196
154
|
|
|
197
155
|
list.innerHTML = "";
|
|
198
|
-
|
|
156
|
+
memories.forEach(m => list.appendChild(_buildMemoryCard(m)));
|
|
199
157
|
}
|
|
200
158
|
|
|
201
159
|
function _buildMemoryCard(m) {
|
|
@@ -222,11 +180,19 @@ const Profile = (() => {
|
|
|
222
180
|
</div>
|
|
223
181
|
<div class="memory-card-actions">
|
|
224
182
|
<button class="btn-memory-curate" title="${_t("memories.curateTitle")}">
|
|
183
|
+
<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">
|
|
184
|
+
<path d="M15 4V2"/><path d="M15 16v-2"/><path d="M8 9h2"/><path d="M20 9h2"/>
|
|
185
|
+
<path d="M17.8 11.8 19 13"/><path d="M15 9h.01"/><path d="M17.8 6.2 19 5"/>
|
|
186
|
+
<path d="m3 21 9-9"/><path d="M12.2 6.2 11 5"/>
|
|
187
|
+
</svg>
|
|
188
|
+
<span>${_t("memories.curate")}</span>
|
|
189
|
+
</button>
|
|
190
|
+
<button class="btn-memory-edit" title="${_t("memories.edit")}">
|
|
225
191
|
<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
192
|
<path d="M12 20h9"/>
|
|
227
193
|
<path d="M16.5 3.5a2.121 2.121 0 1 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
|
|
228
194
|
</svg>
|
|
229
|
-
<span>${_t("memories.
|
|
195
|
+
<span>${_t("memories.edit")}</span>
|
|
230
196
|
</button>
|
|
231
197
|
<button class="btn-memory-delete" title="${_t("memories.deleteTitle")}">
|
|
232
198
|
<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">
|
|
@@ -245,12 +211,14 @@ const Profile = (() => {
|
|
|
245
211
|
</div>`;
|
|
246
212
|
card.appendChild(head);
|
|
247
213
|
|
|
248
|
-
// Collapsible body — rendered lazily on first expand.
|
|
249
214
|
const body = document.createElement("div");
|
|
250
215
|
body.className = "memory-card-body";
|
|
251
216
|
body.style.display = "none";
|
|
252
217
|
card.appendChild(body);
|
|
253
218
|
|
|
219
|
+
head.querySelector(".btn-memory-edit")
|
|
220
|
+
.addEventListener("click", (e) => { e.stopPropagation(); _editMemory(m); });
|
|
221
|
+
|
|
254
222
|
head.querySelector(".btn-memory-curate")
|
|
255
223
|
.addEventListener("click", (e) => { e.stopPropagation(); _curateMemory(m); });
|
|
256
224
|
|
|
@@ -267,18 +235,16 @@ const Profile = (() => {
|
|
|
267
235
|
} else {
|
|
268
236
|
if (!body.dataset.loaded) {
|
|
269
237
|
body.innerHTML = `<div class="memory-card-loading">${_t("memories.loading")}</div>`;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
body.innerHTML = `<div class="profile-empty">${_escapeHtml(err.message)}</div>`;
|
|
281
|
-
});
|
|
238
|
+
Profile.fetchMemory(m.filename).then(res => {
|
|
239
|
+
if (!res.ok) {
|
|
240
|
+
body.innerHTML = `<div class="profile-empty">${_escapeHtml(res.error)}</div>`;
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
const stripped = _stripFrontmatter(res.content);
|
|
244
|
+
body.innerHTML = _renderMarkdown(stripped)
|
|
245
|
+
|| `<div class="profile-empty">${_t("profile.emptyContent")}</div>`;
|
|
246
|
+
body.dataset.loaded = "1";
|
|
247
|
+
});
|
|
282
248
|
}
|
|
283
249
|
body.style.display = "";
|
|
284
250
|
expandBtn.setAttribute("aria-expanded", "true");
|
|
@@ -286,14 +252,12 @@ const Profile = (() => {
|
|
|
286
252
|
}
|
|
287
253
|
}
|
|
288
254
|
expandBtn.addEventListener("click", (e) => { e.stopPropagation(); toggle(); });
|
|
289
|
-
|
|
290
|
-
head.querySelector(".memory-card-info")
|
|
291
|
-
.addEventListener("click", toggle);
|
|
255
|
+
head.querySelector(".memory-card-info").addEventListener("click", toggle);
|
|
292
256
|
|
|
293
257
|
return card;
|
|
294
258
|
}
|
|
295
259
|
|
|
296
|
-
// ── Tabs
|
|
260
|
+
// ── Tabs ─────────────────────────────────────────────────────────────────
|
|
297
261
|
|
|
298
262
|
function _switchTab(tab) {
|
|
299
263
|
if (!tab || tab === _activeTab) return;
|
|
@@ -314,33 +278,13 @@ const Profile = (() => {
|
|
|
314
278
|
});
|
|
315
279
|
}
|
|
316
280
|
|
|
317
|
-
// ── Actions
|
|
281
|
+
// ── Actions (UI side, delegating to store) ───────────────────────────────
|
|
318
282
|
|
|
319
|
-
// Curate one of the identity files via /onboard scope:<soul|user>.
|
|
320
283
|
async function _curateProfile(scope) {
|
|
321
284
|
const btn = $(`btn-profile-curate-${scope}`);
|
|
322
285
|
if (btn) btn.disabled = true;
|
|
323
286
|
try {
|
|
324
|
-
|
|
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);
|
|
287
|
+
await Profile.curateProfile(scope);
|
|
344
288
|
} catch (e) {
|
|
345
289
|
console.error("[Profile] curate profile failed", e);
|
|
346
290
|
alert(_t("profile.curateFail") + ": " + e.message);
|
|
@@ -348,52 +292,49 @@ const Profile = (() => {
|
|
|
348
292
|
}
|
|
349
293
|
}
|
|
350
294
|
|
|
351
|
-
|
|
295
|
+
async function _editMemory(m) {
|
|
296
|
+
const res = await Profile.fetchMemory(m.filename);
|
|
297
|
+
if (!res.ok) { alert(_t("memories.curateFail") + ": " + res.error); return; }
|
|
298
|
+
|
|
299
|
+
CodeEditor.open({
|
|
300
|
+
content: res.content,
|
|
301
|
+
title: m.topic || m.filename,
|
|
302
|
+
language: "markdown",
|
|
303
|
+
onSave: async (newContent) => {
|
|
304
|
+
const r = await Profile.updateMemory(m.filename, newContent);
|
|
305
|
+
if (!r.ok) throw new Error(r.error);
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
352
310
|
async function _curateMemory(m) {
|
|
353
|
-
const absPath = m.path || ("~/.clacky/memories/" + m.filename);
|
|
354
311
|
try {
|
|
355
|
-
|
|
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);
|
|
312
|
+
await Profile.curateMemory(m);
|
|
369
313
|
} catch (e) {
|
|
370
314
|
console.error("[Profile] curate memory failed", e);
|
|
371
315
|
alert(_t("memories.curateFail") + ": " + e.message);
|
|
372
316
|
}
|
|
373
317
|
}
|
|
374
318
|
|
|
375
|
-
// Delete a memory directly. Backend uses `trash` semantics so it lands in
|
|
376
|
-
// File Recall and can still be recovered.
|
|
377
319
|
async function _deleteMemory(m) {
|
|
378
320
|
const label = m.topic || m.filename;
|
|
379
321
|
if (!confirm(_t("memories.confirmDelete", { name: label }))) return;
|
|
322
|
+
const res = await Profile.deleteMemory(m.filename);
|
|
323
|
+
if (!res.ok) alert(_t("memories.deleteFail") + ": " + res.error);
|
|
324
|
+
}
|
|
380
325
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
} catch (e) {
|
|
394
|
-
console.error("[Profile] delete memory failed", e);
|
|
395
|
-
alert(_t("memories.deleteFail") + ": " + e.message);
|
|
396
|
-
}
|
|
326
|
+
function _editProfile(kind) {
|
|
327
|
+
const file = ProfileStore.state[kind];
|
|
328
|
+
if (!file) return;
|
|
329
|
+
|
|
330
|
+
const title = kind === "soul" ? _t("profile.whoIAm") : _t("profile.whoYouAre");
|
|
331
|
+
|
|
332
|
+
CodeEditor.open({
|
|
333
|
+
content: file.content || "",
|
|
334
|
+
title,
|
|
335
|
+
language: "markdown",
|
|
336
|
+
onSave: (newContent) => Profile.updateProfile(kind, newContent)
|
|
337
|
+
});
|
|
397
338
|
}
|
|
398
339
|
|
|
399
340
|
// ── Wiring ───────────────────────────────────────────────────────────
|
|
@@ -402,41 +343,50 @@ const Profile = (() => {
|
|
|
402
343
|
if (_wired) return;
|
|
403
344
|
_wired = true;
|
|
404
345
|
|
|
405
|
-
// Tabs
|
|
406
346
|
document.querySelectorAll(".profile-tab").forEach(el => {
|
|
407
347
|
el.addEventListener("click", () => _switchTab(el.dataset.tab));
|
|
408
348
|
});
|
|
409
349
|
|
|
410
|
-
// Per-tab curate buttons
|
|
411
350
|
const soulBtn = $("btn-profile-curate-soul");
|
|
412
351
|
if (soulBtn) soulBtn.addEventListener("click", () => _curateProfile("soul"));
|
|
413
352
|
const userBtn = $("btn-profile-curate-user");
|
|
414
353
|
if (userBtn) userBtn.addEventListener("click", () => _curateProfile("user"));
|
|
415
354
|
|
|
355
|
+
// Per-tab direct edit buttons
|
|
356
|
+
const editSoulBtn = $("btn-profile-edit-soul");
|
|
357
|
+
if (editSoulBtn) editSoulBtn.addEventListener("click", () => _editProfile("soul"));
|
|
358
|
+
const editUserBtn = $("btn-profile-edit-user");
|
|
359
|
+
if (editUserBtn) editUserBtn.addEventListener("click", () => _editProfile("user"));
|
|
360
|
+
|
|
416
361
|
// Memories list reload
|
|
417
362
|
const refreshMemBtn = $("btn-memories-refresh-list");
|
|
418
|
-
if (refreshMemBtn) refreshMemBtn.addEventListener("click", () =>
|
|
363
|
+
if (refreshMemBtn) refreshMemBtn.addEventListener("click", () => Profile.loadAll());
|
|
419
364
|
}
|
|
420
365
|
|
|
421
|
-
|
|
422
|
-
await Promise.all([_loadProfile(), _loadMemories()]);
|
|
366
|
+
function _renderAll() {
|
|
423
367
|
_renderIdentitySection("soul");
|
|
424
368
|
_renderIdentitySection("user");
|
|
425
369
|
_renderMemories();
|
|
426
370
|
}
|
|
427
371
|
|
|
428
|
-
|
|
372
|
+
function _subscribe() {
|
|
373
|
+
Profile.on("profile:changed", _renderAll);
|
|
374
|
+
Profile.on("profile:memoriesChanged", _renderMemories);
|
|
375
|
+
}
|
|
429
376
|
|
|
430
|
-
|
|
377
|
+
const viewApi = {
|
|
431
378
|
onPanelShow() {
|
|
432
379
|
_wire();
|
|
433
|
-
// Re-enable curate buttons on every panel entry — they may have been
|
|
434
|
-
// disabled by a prior click that navigated away.
|
|
435
380
|
["soul", "user"].forEach(s => {
|
|
436
381
|
const b = $(`btn-profile-curate-${s}`);
|
|
437
382
|
if (b) b.disabled = false;
|
|
438
383
|
});
|
|
439
|
-
|
|
384
|
+
Profile.loadAll();
|
|
440
385
|
}
|
|
441
386
|
};
|
|
387
|
+
|
|
388
|
+
return { init: _subscribe, api: viewApi };
|
|
442
389
|
})();
|
|
390
|
+
|
|
391
|
+
Object.assign(Profile, ProfileView.api);
|
|
392
|
+
ProfileView.init();
|