misakanet 2.40.0 → 2.41.1
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/.codex-plugin/plugin.json +1 -1
- package/README.ja.md +8 -0
- package/README.md +89 -8
- package/README.zh-CN.md +9 -2
- package/index.js +70 -0
- package/lib/client.js +1068 -59
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +8 -3
package/lib/client.js
CHANGED
|
@@ -107,10 +107,308 @@ window.__ModuleLoader__.load({
|
|
|
107
107
|
* could take. (`kind` is what `openTab` names; `id` is the implementation's identity.)
|
|
108
108
|
*/
|
|
109
109
|
var PANEL_KIND = "misakanet-panel";
|
|
110
|
+
var ROW_ID = "misakanet-mcp";
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Localization. The dictionaries live in this file rather than in `locale/*.json`: those files are the
|
|
114
|
+
* *package metadata* the host reads for the plugin page's title and description (`dsh-app-boot`'s
|
|
115
|
+
* `dictionariesOf`), while UI copy goes through `ctx.locale`. Both matter, and they are different
|
|
116
|
+
* mechanisms.
|
|
117
|
+
*
|
|
118
|
+
* `T` is a module-level binding rather than a prop: the host's `bind` keeps a stable identity and reads
|
|
119
|
+
* the active locale on every call, so a component that re-renders after a locale switch gets the new
|
|
120
|
+
* language from the same function.
|
|
121
|
+
*/
|
|
122
|
+
var NS = PANEL_ID;
|
|
123
|
+
var en = {
|
|
124
|
+
"guide.description": "What this session asked, what came back, and what you filed",
|
|
125
|
+
"panel.asked": "What this session asked",
|
|
126
|
+
"panel.asked.none": "No MisakaNet search has run in this session yet.",
|
|
127
|
+
"panel.reports": "Reports you filed",
|
|
128
|
+
"panel.reports.none": "Nothing filed from this session.",
|
|
129
|
+
"panel.trust": "How much these lessons are trusted",
|
|
130
|
+
"panel.trust.none": "No lesson surfaced yet.",
|
|
131
|
+
"panel.activity": "Your activity",
|
|
132
|
+
"panel.voice": "Voice",
|
|
133
|
+
"panel.voice.on": "Voice cues: on",
|
|
134
|
+
"panel.voice.off": "Voice cues: off",
|
|
135
|
+
"overlay.searching": "Searching…",
|
|
136
|
+
"overlay.empty": "Type what broke after /misakanet, e.g. /misakanet pip install timeout",
|
|
137
|
+
"overlay.none": "No lesson covers this yet. Ask the agent to file it (misakanet_submit_intake), or file it at misakanet.org — that is how the next person finds it here.",
|
|
138
|
+
"overlay.failed": "Search failed: {error} — the library is at misakanet.org; nothing was sent but the query.",
|
|
139
|
+
"overlay.footnote": "Reads are anonymous. This overlay sends only the query to misakanet.org; your votes and reports are the only other things a page ever sends.",
|
|
140
|
+
"overlay.close": "Close",
|
|
141
|
+
"settings.voice": "Play voice cues in this browser",
|
|
142
|
+
"settings.voice.note": "(the same switch the panel shows)",
|
|
143
|
+
"settings.density": "How much MisakaNet shows",
|
|
144
|
+
"settings.density.compact": "compact — title and domain",
|
|
145
|
+
"settings.density.full": "full — plus lesson id and description",
|
|
146
|
+
"settings.endpoint": "MCP endpoint",
|
|
147
|
+
"settings.save": "Save",
|
|
148
|
+
"settings.saving": "Saving…",
|
|
149
|
+
"settings.saved": "Saved — applies on the next host start.",
|
|
150
|
+
"settings.refused": "The host refused the write.",
|
|
151
|
+
"settings.writeFailed": "Write failed: {error}",
|
|
152
|
+
"settings.readonly": "This host does not accept writes for the row from here.",
|
|
153
|
+
"settings.patchHint": "The MCP endpoint, transport and timeout live in the profile's cordis.patch.yml, entry id: {row}; this host exposes no writable form for them.",
|
|
154
|
+
"settings.local": "The two switches above are kept in this browser and never uploaded.",
|
|
155
|
+
"footer.action": "Copy session summary",
|
|
156
|
+
"footer.copied": "Copied",
|
|
157
|
+
"footer.nothing": "Nothing yet",
|
|
158
|
+
"footer.failed": "Copy failed",
|
|
159
|
+
"toast.one": "1 lesson",
|
|
160
|
+
"toast.many": "{n} lessons",
|
|
161
|
+
"unit.search": "search",
|
|
162
|
+
"unit.searches": "searches",
|
|
163
|
+
"unit.lesson": "lesson",
|
|
164
|
+
"unit.lessons": "lessons",
|
|
165
|
+
"unit.vote": "vote",
|
|
166
|
+
"unit.votes": "votes",
|
|
167
|
+
"unit.report": "report",
|
|
168
|
+
"unit.reports": "reports",
|
|
169
|
+
"panel.asked.searching": "searching…",
|
|
170
|
+
"panel.asked.noMatch": "no lesson matched",
|
|
171
|
+
"panel.asked.top": "· top: ",
|
|
172
|
+
"panel.asked.noQuery": "(no query text)",
|
|
173
|
+
"panel.asked.nothingFound": "nothing found",
|
|
174
|
+
"panel.asked.voteHelpful": "Helpful",
|
|
175
|
+
"panel.asked.voteNotHelpful": "not helpful",
|
|
176
|
+
"panel.asked.voteNone": "not voted",
|
|
177
|
+
"panel.asked.voteNoLesson": "no lesson",
|
|
178
|
+
"panel.counters": "this session · counters local to this browser · no account, nothing uploaded but your votes and reports",
|
|
179
|
+
"panel.report.title": "MisakaNet report",
|
|
180
|
+
"panel.report.coveredBy": "covered by ",
|
|
181
|
+
"panel.report.similarity": " (similarity ",
|
|
182
|
+
"panel.report.pending": "pending report",
|
|
183
|
+
"panel.report.recheck": " is re-checked when that text is submitted again — by the agent, not by this page.",
|
|
184
|
+
"intake.filing": "filing",
|
|
185
|
+
"intake.waiting": "filed · waiting",
|
|
186
|
+
"intake.answered": "answered",
|
|
187
|
+
"intake.alreadyHave": "not filed — already covered",
|
|
188
|
+
"intake.cited": "cited by a lesson",
|
|
189
|
+
"intake.submitted": "submitted",
|
|
190
|
+
"intake.noText": "(text not captured)",
|
|
191
|
+
"panel.trust.checking": "checking…",
|
|
192
|
+
"panel.trust.unavailable": "unavailable",
|
|
193
|
+
"panel.trust.human": "human confirmation",
|
|
194
|
+
"panel.trust.none": "no level",
|
|
195
|
+
"panel.trust.used": " · used ",
|
|
196
|
+
"panel.trust.footnote": "The level is the lesson's own stored evidence. The count is other people's confirmations, not yours: one makes the reuse event appear, the second is what agents read as E4.",
|
|
197
|
+
"panel.activity.session": "this session: ",
|
|
198
|
+
"panel.activity.browser": "on this browser: ",
|
|
199
|
+
"panel.activity.reused": " reused · ",
|
|
200
|
+
"panel.activity.filed": " filed · ",
|
|
201
|
+
"panel.activity.conversion": "conversion",
|
|
202
|
+
"panel.activity.scope": "The browser column survives a reload; the session column is rebuilt from this conversation's transcript and starts again in a new session.",
|
|
203
|
+
"panel.voice.cue": "The cue is the server's own choice for the last search (",
|
|
204
|
+
"panel.voice.cueTail": "), not the UI's guess. Browser playback only, and off by default — nothing plays until you turn it on or press play.",
|
|
205
|
+
"panel.voice.none": "No cue yet: the server names one on the first search of the session.",
|
|
206
|
+
"panel.voice.play": "Play “",
|
|
207
|
+
"panel.voice.hook": "The local hook is a different thing: it is installed with `--voice` and mutes with MISAKANET_VOICE=0, plays through this machine's own player, and can also raise a desktop notification. A page cannot read or change it, so this panel does not report its state.",
|
|
208
|
+
"panel.footnote": "Every number above comes from this conversation's own transcript rows plus one public GET per lesson (/api/helpful). The trust counts are other people's confirmations, not yours. Reloading the window loses the session rows; the browser counters stay. Receipts shown here are what the agent's own calls returned — a pending report is re-checked when the agent asks again, not by this page.",
|
|
209
|
+
"main.footnote": "The left column opens this page; the session-scoped counters live in the session's right-sidebar MisakaNet tab. The numbers here are what this browser has contributed.",
|
|
210
|
+
"tool.rawResult": "raw result",
|
|
211
|
+
"tool.readFailed": "result could not be read",
|
|
212
|
+
"tool.helpfulTitle": "Sends this lesson's id to misakanet.org as reuse evidence. Sends nothing else.",
|
|
213
|
+
"tool.notHelpfulTitle": "Sends this lesson's id and the search text, so the gap gets counted.",
|
|
214
|
+
"tool.helpfulHint": "Helpful sends the lesson id · not-helpful also sends the search text (gap stats)",
|
|
215
|
+
"tool.helpful": "👍 Helpful",
|
|
216
|
+
"tool.notHelpful": "👎 Not what I needed",
|
|
217
|
+
"tool.untitled": "(untitled)",
|
|
218
|
+
"tool.brand": "MisakaNet:",
|
|
219
|
+
"settings.rowReadonly": "Shown read-only: this host builds an editable form only for fields a plugin can change without reloading, and this row's settings (endpoint, transport, timeout) apply on the next host start. Edit them in the profile's patch layer — `cordis.patch.yml`, entry id: {row} — next to the {what} the host already has.",
|
|
220
|
+
"settings.rowNoValues": "No values reported by this host yet; the row runs the bundle patch's defaults.",
|
|
221
|
+
"settings.noWrites": "This host does not accept writes here (read-only).",
|
|
222
|
+
"settings.invalidJson": "invalid JSON",
|
|
223
|
+
"settings.what.form": "form",
|
|
224
|
+
"settings.what.row": "row",
|
|
225
|
+
"panel.activity.withLesson": " with a lesson · ",
|
|
226
|
+
"footer.session": "session: ",
|
|
227
|
+
"footer.actionHint": "Copy the MisakaNet summary of the session that last used it",
|
|
228
|
+
"settings.copyPatch": "Copy patch snippet",
|
|
229
|
+
"settings.copied": "Copied",
|
|
230
|
+
"settings.copyFailed": "Copy failed",
|
|
231
|
+
};
|
|
232
|
+
var zh = {
|
|
233
|
+
"guide.description": "本会话问过什么、拿回了什么、你报了什么",
|
|
234
|
+
"panel.asked": "本会话问过什么",
|
|
235
|
+
"panel.asked.none": "本会话还没有发起过 MisakaNet 检索。",
|
|
236
|
+
"panel.reports": "你报过的问题",
|
|
237
|
+
"panel.reports.none": "本会话没有报过任何问题。",
|
|
238
|
+
"panel.trust": "这些课程的可信度",
|
|
239
|
+
"panel.trust.none": "还没有命中过课程。",
|
|
240
|
+
"panel.activity": "你的活动",
|
|
241
|
+
"panel.voice": "语音",
|
|
242
|
+
"panel.voice.on": "语音提示:开",
|
|
243
|
+
"panel.voice.off": "语音提示:关",
|
|
244
|
+
"overlay.searching": "检索中……",
|
|
245
|
+
"overlay.empty": "在 /misakanet 后面写清楚出了什么问题,例如 /misakanet pip install 超时",
|
|
246
|
+
"overlay.none": "还没有课程覆盖这个问题。可以让 agent 报料(misakanet_submit_intake),或直接去 misakanet.org 提交 —— 下一个人就能在这里搜到它。",
|
|
247
|
+
"overlay.failed": "检索失败:{error} —— 课程库在 misakanet.org;这次只发出了查询本身。",
|
|
248
|
+
"overlay.footnote": "读取是匿名的。这个浮层只把查询发给 misakanet.org;除此之外,页面只会发出你的投票与报料。",
|
|
249
|
+
"overlay.close": "关闭",
|
|
250
|
+
"settings.voice": "在本浏览器播放语音提示",
|
|
251
|
+
"settings.voice.note": "(与面板上那个开关是同一个)",
|
|
252
|
+
"settings.density": "MisakaNet 显示多少内容",
|
|
253
|
+
"settings.density.compact": "精简 —— 标题与领域",
|
|
254
|
+
"settings.density.full": "完整 —— 另加课程 id 与描述",
|
|
255
|
+
"settings.endpoint": "MCP 端点",
|
|
256
|
+
"settings.save": "保存",
|
|
257
|
+
"settings.saving": "保存中……",
|
|
258
|
+
"settings.saved": "已保存 —— 下次宿主启动时生效。",
|
|
259
|
+
"settings.refused": "宿主拒绝了这次写入。",
|
|
260
|
+
"settings.writeFailed": "写入失败:{error}",
|
|
261
|
+
"settings.readonly": "本宿主不允许从这里写入该行。",
|
|
262
|
+
"settings.patchHint": "MCP 端点、传输方式与超时写在 profile 的 cordis.patch.yml 里,条目 id: {row};本宿主没有为它们提供可写表单。",
|
|
263
|
+
"settings.local": "上面两个开关只保存在本浏览器,不会上传。",
|
|
264
|
+
"footer.action": "复制本会话摘要",
|
|
265
|
+
"footer.copied": "已复制",
|
|
266
|
+
"footer.nothing": "还没有内容",
|
|
267
|
+
"footer.failed": "复制失败",
|
|
268
|
+
"toast.one": "命中 1 篇课程",
|
|
269
|
+
"toast.many": "命中 {n} 篇课程",
|
|
270
|
+
"unit.search": "次检索",
|
|
271
|
+
"unit.searches": "次检索",
|
|
272
|
+
"unit.lesson": "篇",
|
|
273
|
+
"unit.lessons": "篇",
|
|
274
|
+
"unit.vote": "票",
|
|
275
|
+
"unit.votes": "票",
|
|
276
|
+
"unit.report": "条报料",
|
|
277
|
+
"unit.reports": "条报料",
|
|
278
|
+
"panel.asked.searching": "检索中……",
|
|
279
|
+
"panel.asked.noMatch": "没有命中任何课程",
|
|
280
|
+
"panel.asked.top": "· 最佳:",
|
|
281
|
+
"panel.asked.noQuery": "(没记下查询原文)",
|
|
282
|
+
"panel.asked.nothingFound": "没有命中",
|
|
283
|
+
"panel.asked.voteHelpful": "有用",
|
|
284
|
+
"panel.asked.voteNotHelpful": "没用",
|
|
285
|
+
"panel.asked.voteNone": "未投票",
|
|
286
|
+
"panel.asked.voteNoLesson": "无课程",
|
|
287
|
+
"panel.counters": "本会话 · 计数只存在本浏览器 · 没有账号,除了你的投票与报料,不上传任何东西",
|
|
288
|
+
"panel.report.title": "MisakaNet 报料",
|
|
289
|
+
"panel.report.coveredBy": "已被 ",
|
|
290
|
+
"panel.report.similarity": "(相似度 ",
|
|
291
|
+
"panel.report.pending": "待处理报料",
|
|
292
|
+
"panel.report.recheck": " 会在同一段文本被再次提交时复查 —— 由 agent 触发,不是这个页面。",
|
|
293
|
+
"intake.filing": "提交中",
|
|
294
|
+
"intake.waiting": "已提交 · 等待中",
|
|
295
|
+
"intake.answered": "已回答",
|
|
296
|
+
"intake.alreadyHave": "未提交 —— 已被覆盖",
|
|
297
|
+
"intake.cited": "已被某篇课程引用",
|
|
298
|
+
"intake.submitted": "已提交",
|
|
299
|
+
"intake.noText": "(没抓到原文)",
|
|
300
|
+
"panel.trust.checking": "核验中……",
|
|
301
|
+
"panel.trust.unavailable": "不可用",
|
|
302
|
+
"panel.trust.human": "人工确认",
|
|
303
|
+
"panel.trust.none": "无等级",
|
|
304
|
+
"panel.trust.used": " · 已复用 ",
|
|
305
|
+
"panel.trust.footnote": "等级来自课程自己存的证据;计数是别人的确认,不是你的:前者让复用事件出现,后者才是 agent 读到的 E4。",
|
|
306
|
+
"panel.activity.session": "本会话:",
|
|
307
|
+
"panel.activity.browser": "本浏览器:",
|
|
308
|
+
"panel.activity.reused": " 次复用 · ",
|
|
309
|
+
"panel.activity.filed": " 条报料 · ",
|
|
310
|
+
"panel.activity.conversion": "转化",
|
|
311
|
+
"panel.activity.scope": "浏览器那一列能跨刷新保留;会话那一列由本次对话的转录重建,换一个会话就从零开始。",
|
|
312
|
+
"panel.voice.cue": "提示音是服务端为上一次检索自己选的(",
|
|
313
|
+
"panel.voice.cueTail": "),不是界面猜的。只在本浏览器播放,且默认关闭 —— 你不打开或点播放,它不会响。",
|
|
314
|
+
"panel.voice.none": "还没有提示音:服务端会在本会话第一次检索时给出。",
|
|
315
|
+
"panel.voice.play": "播放“",
|
|
316
|
+
"panel.voice.hook": "本地钩子是另一回事:用 --voice 安装,用 MISAKANET_VOICE=0 静音,通过本机自己的播放器发声,还能弹桌面通知。页面读不到也改不了它,所以这个面板不报告它的状态。",
|
|
317
|
+
"panel.footnote": "上面的数字都来自本次对话自己的转录,外加每篇课程一次公开 GET(/api/helpful)。可信计数是别人的确认,不是你自己的。重载窗口会丢掉会话行,浏览器计数会留下。这里的回执是 agent 自己调用返回的东西 —— 待处理报料会在 agent 再次提问时复查,而不是这个页面。",
|
|
318
|
+
"main.footnote": "左栏打开的就是这一页;会话级的计数在右栏的 MisakaNet 标签里。这里的数字是本浏览器贡献的部分。",
|
|
319
|
+
"tool.rawResult": "原始结果",
|
|
320
|
+
"tool.readFailed": "结果读不出来",
|
|
321
|
+
"tool.helpfulTitle": "把这篇课程的 id 发给 misakanet.org 作为复用证据,不发别的。",
|
|
322
|
+
"tool.notHelpfulTitle": "把课程 id 和检索文本一起发出去,好把缺口统计进去。",
|
|
323
|
+
"tool.helpfulHint": "「有用」只发课程 id ·「没用」还会发检索文本(用于缺口统计)",
|
|
324
|
+
"tool.helpful": "👍 有用",
|
|
325
|
+
"tool.notHelpful": "👎 不是我需要的",
|
|
326
|
+
"tool.untitled": "(无标题)",
|
|
327
|
+
"tool.brand": "MisakaNet:",
|
|
328
|
+
"settings.rowReadonly": "只读显示:这个宿主只为「改完不必重载」的字段生成可编辑表单,而本行的设置(端点、传输方式、超时)要在宿主下次启动时生效。请到 profile 的 patch 层修改 —— cordis.patch.yml,条目 id:{row} —— 就在宿主已有的{what}旁边。",
|
|
329
|
+
"settings.rowNoValues": "宿主还没有报告任何值;这一行用 bundle patch 里的默认值在跑。",
|
|
330
|
+
"settings.noWrites": "本宿主不允许在这里写入(只读)。",
|
|
331
|
+
"settings.invalidJson": "JSON 不合法",
|
|
332
|
+
"settings.what.form": "表单",
|
|
333
|
+
"settings.what.row": "条目",
|
|
334
|
+
"panel.activity.withLesson": " 篇命中课程 · ",
|
|
335
|
+
"footer.session": "会话:",
|
|
336
|
+
"footer.actionHint": "复制最近一次使用 MisakaNet 的那个会话的摘要",
|
|
337
|
+
"settings.copyPatch": "复制 patch 片段",
|
|
338
|
+
"settings.copied": "已复制",
|
|
339
|
+
"settings.copyFailed": "复制失败",
|
|
340
|
+
};
|
|
341
|
+
var T = function (key, params) { return localize(activeLocale(), key, params); };
|
|
342
|
+
var localeSubscribers = [];
|
|
343
|
+
var localeFace = null;
|
|
344
|
+
|
|
345
|
+
/** Substitute `{name}` placeholders; a missing key falls back to English, then to the key itself. */
|
|
346
|
+
function localize(locale, key, params) {
|
|
347
|
+
var dict = locale === "zh" ? zh : en;
|
|
348
|
+
var text = dict[key] !== undefined ? dict[key] : (en[key] !== undefined ? en[key] : key);
|
|
349
|
+
if (!params) return text;
|
|
350
|
+
return text.replace(/\{(\w+)\}/g, function (match, name) {
|
|
351
|
+
return params[name] === undefined ? match : String(params[name]);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function activeLocale() {
|
|
356
|
+
try {
|
|
357
|
+
if (localeFace && typeof localeFace.getLocale === "function") return localeFace.getLocale().active;
|
|
358
|
+
} catch (error) { /* fall through to the provisional default */ }
|
|
359
|
+
return "en";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function subscribeLocale(listener) {
|
|
363
|
+
localeSubscribers.push(listener);
|
|
364
|
+
return function () {
|
|
365
|
+
var at = localeSubscribers.indexOf(listener);
|
|
366
|
+
if (at >= 0) localeSubscribers.splice(at, 1);
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function notifyLocale() {
|
|
371
|
+
var list = localeSubscribers.slice();
|
|
372
|
+
for (var i = 0; i < list.length; i++) list[i]();
|
|
373
|
+
}
|
|
110
374
|
|
|
111
375
|
/** localStorage keys. Both scopes are this browser only — there is no account anywhere here. */
|
|
112
376
|
var STATS_KEY = "misakanet.panel.stats.v1";
|
|
113
377
|
var VOICE_KEY = "misakanet.panel.voice.v1";
|
|
378
|
+
var DENSITY_KEY = "misakanet.panel.density.v1";
|
|
379
|
+
var prefListeners = [];
|
|
380
|
+
|
|
381
|
+
/** Preferences are browser-scope, so their notification is too — not per session. */
|
|
382
|
+
function subscribePrefs(listener) {
|
|
383
|
+
prefListeners.push(listener);
|
|
384
|
+
return function () {
|
|
385
|
+
var at = prefListeners.indexOf(listener);
|
|
386
|
+
if (at >= 0) prefListeners.splice(at, 1);
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function notifyPrefs() {
|
|
391
|
+
Promise.resolve().then(function () {
|
|
392
|
+
var list = prefListeners.slice();
|
|
393
|
+
for (var i = 0; i < list.length; i++) list[i]();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/** How much a MisakaNet surface shows: `compact` keeps it to a title and its domain. */
|
|
398
|
+
function density() {
|
|
399
|
+
try {
|
|
400
|
+
return window.localStorage.getItem(DENSITY_KEY) === "full" ? "full" : "compact";
|
|
401
|
+
} catch (error) {
|
|
402
|
+
return "compact";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function setDensity(value) {
|
|
407
|
+
try {
|
|
408
|
+
window.localStorage.setItem(DENSITY_KEY, value === "full" ? "full" : "compact");
|
|
409
|
+
} catch (error) { /* nothing to remember it with */ }
|
|
410
|
+
notifyPrefs();
|
|
411
|
+
}
|
|
114
412
|
|
|
115
413
|
/** How many events one session keeps in memory. The panel is a log, not an archive. */
|
|
116
414
|
var LOG_LIMIT = 200;
|
|
@@ -129,8 +427,23 @@ window.__ModuleLoader__.load({
|
|
|
129
427
|
*/
|
|
130
428
|
var sessions = Object.create(null);
|
|
131
429
|
|
|
430
|
+
/**
|
|
431
|
+
* The session that moved last.
|
|
432
|
+
*
|
|
433
|
+
* The panel and the verdict row are session-scoped and receive a `sessionId`; the frame-wide seats
|
|
434
|
+
* (`shell.overlay`, `sidebar.footer.action`) receive none — "each action receives only the column
|
|
435
|
+
* state" is the footer's own wording. So the store remembers which session was touched most recently
|
|
436
|
+
* and those two surfaces speak about it.
|
|
437
|
+
*/
|
|
438
|
+
var lastSessionId = null;
|
|
439
|
+
|
|
440
|
+
function touchSession(sessionId) {
|
|
441
|
+
if (sessionId) lastSessionId = sessionId;
|
|
442
|
+
}
|
|
443
|
+
|
|
132
444
|
function sessionLog(sessionId) {
|
|
133
445
|
if (!sessionId) return null;
|
|
446
|
+
touchSession(sessionId);
|
|
134
447
|
if (!sessions[sessionId]) {
|
|
135
448
|
sessions[sessionId] = {
|
|
136
449
|
sessionId: sessionId,
|
|
@@ -199,6 +512,7 @@ window.__ModuleLoader__.load({
|
|
|
199
512
|
} catch (error) {
|
|
200
513
|
/* nothing to remember it with; the toggle still reports what it did */
|
|
201
514
|
}
|
|
515
|
+
notifyPrefs();
|
|
202
516
|
}
|
|
203
517
|
|
|
204
518
|
/**
|
|
@@ -247,6 +561,76 @@ window.__ModuleLoader__.load({
|
|
|
247
561
|
*/
|
|
248
562
|
var listeners = Object.create(null);
|
|
249
563
|
|
|
564
|
+
// The slash command's result overlay. It lives in the same per-session store as the counters, so
|
|
565
|
+
// switching sessions switches the overlay with everything else, and one `notify` redraws both.
|
|
566
|
+
var DEFAULT_MCP_URL = "https://misakanet.org/mcp";
|
|
567
|
+
|
|
568
|
+
var SEARCH_ENDPOINT = "https://misakanet.org/api/lessons";
|
|
569
|
+
var OVERLAY_LIMIT = 5;
|
|
570
|
+
var OVERLAY_TIMEOUT_MS = 8000;
|
|
571
|
+
|
|
572
|
+
function isJson(text) {
|
|
573
|
+
try { JSON.parse(text); return true; } catch (error) { return false; }
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function overlayOf(sessionId) {
|
|
577
|
+
var log = sessions[sessionId];
|
|
578
|
+
if (!log) {
|
|
579
|
+
log = sessions[sessionId] = { searches: [], intakes: [], votes: [], lessons: {} };
|
|
580
|
+
}
|
|
581
|
+
if (!log.overlay) {
|
|
582
|
+
log.overlay = { open: false, query: "", status: "idle", results: [], error: null };
|
|
583
|
+
}
|
|
584
|
+
return log.overlay;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function setOverlay(sessionId, patch) {
|
|
588
|
+
touchSession(sessionId);
|
|
589
|
+
notifyShell();
|
|
590
|
+
var state = overlayOf(sessionId);
|
|
591
|
+
for (var key in patch) {
|
|
592
|
+
if (Object.prototype.hasOwnProperty.call(patch, key)) state[key] = patch[key];
|
|
593
|
+
}
|
|
594
|
+
notify(sessionId);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Search from the composer, without the agent in the loop.
|
|
599
|
+
*
|
|
600
|
+
* `GET /api/lessons?q=` is public, CORS-open (measured 2026-10-01: `access-control-allow-origin: *`)
|
|
601
|
+
* and returns `{query, results:[{id, title, domain, path, tags, description, rank}]}` — the same rows
|
|
602
|
+
* the MCP tool returns, so the overlay needs no new backend. The request carries the query and
|
|
603
|
+
* nothing else; the comment says so where a reader will look for it.
|
|
604
|
+
*/
|
|
605
|
+
function runOverlaySearch(sessionId, query) {
|
|
606
|
+
query = String(query || "").trim();
|
|
607
|
+
if (!query) {
|
|
608
|
+
setOverlay(sessionId, { open: true, query: "", status: "idle", results: [], error: null });
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
setOverlay(sessionId, { open: true, query: query, status: "loading", results: [], error: null });
|
|
612
|
+
var timedOut = false;
|
|
613
|
+
var timer = setTimeout(function () { timedOut = true; }, OVERLAY_TIMEOUT_MS);
|
|
614
|
+
var url = SEARCH_ENDPOINT + "?q=" + encodeURIComponent(query) + "&limit=" + OVERLAY_LIMIT;
|
|
615
|
+
fetch(url, { headers: { accept: "application/json" } })
|
|
616
|
+
.then(function (response) {
|
|
617
|
+
clearTimeout(timer);
|
|
618
|
+
if (timedOut) return null;
|
|
619
|
+
if (!response.ok) throw new Error("HTTP " + response.status);
|
|
620
|
+
return response.json();
|
|
621
|
+
})
|
|
622
|
+
.then(function (data) {
|
|
623
|
+
if (timedOut || data === null) return;
|
|
624
|
+
setOverlay(sessionId, { status: "done", results: (data && data.results) || [], error: null });
|
|
625
|
+
})
|
|
626
|
+
.catch(function (error) {
|
|
627
|
+
clearTimeout(timer);
|
|
628
|
+
if (timedOut) return;
|
|
629
|
+
setOverlay(sessionId, { status: "error", results: [],
|
|
630
|
+
error: (error && error.message) || String(error) });
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
250
634
|
function subscribe(sessionId, listener) {
|
|
251
635
|
if (!sessionId) return function () {};
|
|
252
636
|
if (!listeners[sessionId]) listeners[sessionId] = [];
|
|
@@ -258,6 +642,31 @@ window.__ModuleLoader__.load({
|
|
|
258
642
|
};
|
|
259
643
|
}
|
|
260
644
|
|
|
645
|
+
var shellListeners = [];
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* The frame-wide seats (`shell.overlay`, `sidebar.footer.action`) receive no `sessionId`, and at mount
|
|
649
|
+
* there is no session to subscribe to yet — an effect that returned early on a null id would subscribe
|
|
650
|
+
* to nothing and never repaint. They subscribe to this instead, and the store announces here whenever
|
|
651
|
+
* session state moves.
|
|
652
|
+
*/
|
|
653
|
+
function subscribeShell(listener) {
|
|
654
|
+
shellListeners.push(listener);
|
|
655
|
+
return function () {
|
|
656
|
+
var at = shellListeners.indexOf(listener);
|
|
657
|
+
if (at >= 0) shellListeners.splice(at, 1);
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function notifyShell() {
|
|
662
|
+
Promise.resolve().then(function () {
|
|
663
|
+
var list = shellListeners.slice();
|
|
664
|
+
for (var i = 0; i < list.length; i++) {
|
|
665
|
+
try { list[i](); } catch (error) { /* one listener's failure is not the others' problem */ }
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
261
670
|
function notify(sessionId) {
|
|
262
671
|
if (!sessionId) return;
|
|
263
672
|
Promise.resolve().then(function () {
|
|
@@ -326,6 +735,7 @@ window.__ModuleLoader__.load({
|
|
|
326
735
|
log.lessons[entry.lessonId].last = entry.at;
|
|
327
736
|
}
|
|
328
737
|
return entry;
|
|
738
|
+
notifyShell();
|
|
329
739
|
}
|
|
330
740
|
|
|
331
741
|
/** Record a vote and retire the sighting it answered. */
|
|
@@ -343,6 +753,7 @@ window.__ModuleLoader__.load({
|
|
|
343
753
|
}
|
|
344
754
|
bumpStat("votes");
|
|
345
755
|
if (log) notify(log.sessionId);
|
|
756
|
+
notifyShell();
|
|
346
757
|
}
|
|
347
758
|
|
|
348
759
|
/**
|
|
@@ -355,7 +766,7 @@ window.__ModuleLoader__.load({
|
|
|
355
766
|
function intakeState(payload) {
|
|
356
767
|
if (!payload || typeof payload !== "object") return "unknown";
|
|
357
768
|
if (payload.converted) return "converted";
|
|
358
|
-
if (payload.answered) return "answered";
|
|
769
|
+
if (payload.answered) return T("intake.answered");
|
|
359
770
|
if (payload.already_have) return "already_have";
|
|
360
771
|
if (payload.pending || payload.duplicate || payload.submitted) return "pending";
|
|
361
772
|
return "unknown";
|
|
@@ -385,7 +796,7 @@ window.__ModuleLoader__.load({
|
|
|
385
796
|
callId: String(callId || ""),
|
|
386
797
|
at: Date.now(),
|
|
387
798
|
text: String(text || ""),
|
|
388
|
-
state: phase === "result" ? intakeState(payload) : "filing",
|
|
799
|
+
state: phase === "result" ? intakeState(payload) : T("intake.filing"),
|
|
389
800
|
receipt: String((payload && (payload.receipt || payload.note)) || ""),
|
|
390
801
|
issue: String((payload && (payload.intake_id || payload.previous_issue)) || ""),
|
|
391
802
|
answer: String((payload && payload.answer) || ""),
|
|
@@ -395,6 +806,7 @@ window.__ModuleLoader__.load({
|
|
|
395
806
|
if (entry.state === "converted") bumpStat("reportsConverted");
|
|
396
807
|
notify(log.sessionId);
|
|
397
808
|
return entry;
|
|
809
|
+
notifyShell();
|
|
398
810
|
}
|
|
399
811
|
|
|
400
812
|
/** Concatenate the text blocks of a result node; "" when there are none. */
|
|
@@ -519,15 +931,15 @@ window.__ModuleLoader__.load({
|
|
|
519
931
|
var head = [h("strong", { key: "brand" }, "MisakaNet")];
|
|
520
932
|
|
|
521
933
|
if (phase === "preparing") {
|
|
522
|
-
head.push(h("span", { key: "state", style: DIM }, " searching
|
|
934
|
+
head.push(h("span", { key: "state", style: DIM }, " " + T("panel.asked.searching")));
|
|
523
935
|
} else if (phase === "start") {
|
|
524
|
-
head.push(h("span", { key: "state" }, " " + (query || "searching
|
|
936
|
+
head.push(h("span", { key: "state" }, " " + (query || T("panel.asked.searching"))));
|
|
525
937
|
} else if (payload && payload.no_match) {
|
|
526
|
-
head.push(h("span", { key: "state" }, "
|
|
938
|
+
head.push(h("span", { key: "state" }, " " + T("panel.asked.noMatch")));
|
|
527
939
|
} else if (results.length) {
|
|
528
940
|
head.push(h("span", { key: "state" }, " " + results.length + (results.length === 1 ? " lesson" : " lessons")));
|
|
529
941
|
head.push(h("span", { key: "top", style: DIM }, " · top: "));
|
|
530
|
-
head.push(h("span", { key: "title" }, top.title || top.id || "
|
|
942
|
+
head.push(h("span", { key: "title" }, top.title || top.id || T("tool.untitled")));
|
|
531
943
|
// Only fields the DEFAULT payload actually carries. `detail: "compact"` is
|
|
532
944
|
// {id, title, problem, freshness, evidence_level}; `domain` arrives only at
|
|
533
945
|
// `detail: "summary"` and `path` only at `"full"`. The first version of this row promised
|
|
@@ -539,7 +951,7 @@ window.__ModuleLoader__.load({
|
|
|
539
951
|
var facts = [top.evidence_level, top.freshness].filter(Boolean).join(" · ");
|
|
540
952
|
if (facts) head.push(h("span", { key: "meta", style: DIM }, " (" + facts + ")"));
|
|
541
953
|
} else if (phase === "result") {
|
|
542
|
-
head.push(h("span", { key: "state", style: DIM }, "
|
|
954
|
+
head.push(h("span", { key: "state", style: DIM }, " " + T("tool.readFailed")));
|
|
543
955
|
}
|
|
544
956
|
|
|
545
957
|
var body = [h("div", { key: "body", style: BOX }, h("div", null, head))];
|
|
@@ -547,7 +959,7 @@ window.__ModuleLoader__.load({
|
|
|
547
959
|
if (raw) {
|
|
548
960
|
body.push(
|
|
549
961
|
h("details", { key: "raw" },
|
|
550
|
-
h("summary", { style: Object.assign({}, DIM, { cursor: "pointer", fontSize: "12px" }) }, "
|
|
962
|
+
h("summary", { style: Object.assign({}, DIM, { cursor: "pointer", fontSize: "12px" }) }, T("tool.rawResult")),
|
|
551
963
|
h("pre", { style: { fontSize: "11px", overflowX: "auto", whiteSpace: "pre-wrap" } }, raw))
|
|
552
964
|
);
|
|
553
965
|
}
|
|
@@ -618,24 +1030,24 @@ window.__ModuleLoader__.load({
|
|
|
618
1030
|
return h("span", {
|
|
619
1031
|
style: { fontSize: "12px", display: "inline-flex", flexWrap: "wrap", alignItems: "center", rowGap: "2px" },
|
|
620
1032
|
},
|
|
621
|
-
h("span", { style: Object.assign({}, DIM, { marginRight: "6px" }) }, "
|
|
1033
|
+
h("span", { style: Object.assign({}, DIM, { marginRight: "6px" }) }, T("tool.brand")),
|
|
622
1034
|
h("button", {
|
|
623
1035
|
type: "button",
|
|
624
1036
|
style: BUTTON,
|
|
625
|
-
title: "
|
|
1037
|
+
title: T("tool.helpfulTitle"),
|
|
626
1038
|
onClick: function () { send("helpful"); },
|
|
627
|
-
}, "
|
|
1039
|
+
}, T("tool.helpful")),
|
|
628
1040
|
h("button", {
|
|
629
1041
|
type: "button",
|
|
630
1042
|
style: BUTTON,
|
|
631
|
-
title: "
|
|
1043
|
+
title: T("tool.notHelpfulTitle"),
|
|
632
1044
|
onClick: function () { send("irrelevant"); },
|
|
633
|
-
}, "
|
|
1045
|
+
}, T("tool.notHelpful")),
|
|
634
1046
|
// Words, not the emoji, carry this line: an environment without an emoji font renders the
|
|
635
1047
|
// buttons as boxes, and a disclosure that turns into "□ sends the lesson id" is worse than
|
|
636
1048
|
// none. The button labels survive the same way.
|
|
637
1049
|
h("span", { style: Object.assign({}, DIM, { flexBasis: "100%" }) },
|
|
638
|
-
|
|
1050
|
+
T("tool.helpfulHint"))
|
|
639
1051
|
);
|
|
640
1052
|
}
|
|
641
1053
|
|
|
@@ -662,10 +1074,10 @@ window.__ModuleLoader__.load({
|
|
|
662
1074
|
answered: "answered",
|
|
663
1075
|
already_have: "not filed — the corpus already covers this",
|
|
664
1076
|
converted: "your report is cited by a lesson",
|
|
665
|
-
unknown: "submitted",
|
|
1077
|
+
unknown: T("intake.submitted"),
|
|
666
1078
|
};
|
|
667
1079
|
var head = [
|
|
668
|
-
h("strong", { key: "brand" }, "
|
|
1080
|
+
h("strong", { key: "brand" }, T("panel.report.title")),
|
|
669
1081
|
h("span", { key: "state", style: DIM }, " " + (labels[state] || state)),
|
|
670
1082
|
];
|
|
671
1083
|
var body = [h("div", { key: "head" }, head)];
|
|
@@ -678,7 +1090,7 @@ window.__ModuleLoader__.load({
|
|
|
678
1090
|
}
|
|
679
1091
|
if (payload && payload.lesson && payload.lesson.id) {
|
|
680
1092
|
body.push(h("div", { key: "lesson", style: DIM },
|
|
681
|
-
"
|
|
1093
|
+
T("panel.report.coveredBy") + payload.lesson.id + (payload.lesson.similarity ? T("panel.report.similarity") + payload.lesson.similarity + ")" : "")));
|
|
682
1094
|
}
|
|
683
1095
|
return h("div", { style: BOX }, body);
|
|
684
1096
|
}
|
|
@@ -708,6 +1120,8 @@ window.__ModuleLoader__.load({
|
|
|
708
1120
|
setRevision(function (n) { return n + 1; });
|
|
709
1121
|
});
|
|
710
1122
|
}, [sessionId]);
|
|
1123
|
+
react.useEffect(function () { return subscribePrefs(function () { setRevision(function (n) { return n + 1; }); }); }, []);
|
|
1124
|
+
react.useEffect(function () { return subscribeLocale(function () { setRevision(function (n) { return n + 1; }); }); }, []);
|
|
711
1125
|
|
|
712
1126
|
var log = sessionId ? sessions[sessionId] : null;
|
|
713
1127
|
var searches = log ? log.searches : [];
|
|
@@ -762,25 +1176,25 @@ window.__ModuleLoader__.load({
|
|
|
762
1176
|
rows.push(h("div", { key: "title", style: { fontSize: "15px", fontWeight: 600, display: "flex", alignItems: "center", gap: "6px" } },
|
|
763
1177
|
MisakanetGlyph({ size: 18 }), h("span", null, "MisakaNet")));
|
|
764
1178
|
rows.push(h("div", { key: "scope", style: DIM },
|
|
765
|
-
"
|
|
1179
|
+
T("panel.counters")));
|
|
766
1180
|
|
|
767
1181
|
rows.push(h("div", { key: "strip", style: Object.assign({}, BOX, { marginTop: "8px" }) },
|
|
768
|
-
count(searches.length, "search", "searches") + " · " + withLesson + " with a lesson · " +
|
|
769
|
-
noMatch + "
|
|
770
|
-
count(votes.length, "vote") + " (" + helpful + " helpful) · " +
|
|
1182
|
+
count(searches.length, T("unit.search"), T("unit.searches")) + " · " + withLesson + " with a lesson · " +
|
|
1183
|
+
noMatch + " " + T("panel.asked.nothingFound") + " · " + count(lessons.length, "lesson") + " surfaced · " +
|
|
1184
|
+
count(votes.length, T("unit.vote")) + " (" + helpful + " helpful) · " +
|
|
771
1185
|
count(intakes.length, "report") + " (" + converted + " became a lesson)"));
|
|
772
1186
|
|
|
773
1187
|
// ① what this session asked
|
|
774
|
-
rows.push(h("div", { key: "s1", style: { marginTop: "12px", fontWeight: 600 } }, "
|
|
1188
|
+
rows.push(h("div", { key: "s1", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.asked")));
|
|
775
1189
|
if (!searches.length) {
|
|
776
|
-
rows.push(h("div", { key: "s1e", style: DIM }, "
|
|
1190
|
+
rows.push(h("div", { key: "s1e", style: DIM }, T("panel.asked.none")));
|
|
777
1191
|
} else {
|
|
778
1192
|
var searchRows = searches.slice().reverse().slice(0, 20).map(function (entry) {
|
|
779
1193
|
var vote = entry.voted ? (entry.voteKind === "helpful" ? "Helpful" : "not helpful") : (entry.lessonId ? "not voted" : "no lesson");
|
|
780
1194
|
return h("div", { key: "q" + entry.callId + entry.at, style: { borderTop: "1px solid " + MUTED, padding: "4px 0" } },
|
|
781
1195
|
h("span", { style: DIM }, clockOf(entry.at) + " "),
|
|
782
|
-
h("span", {}, entry.query ||
|
|
783
|
-
h("span", { style: DIM }, " " + (entry.noMatch ? "
|
|
1196
|
+
h("span", {}, entry.query || T("panel.asked.noQuery")),
|
|
1197
|
+
h("span", { style: DIM }, " " + (entry.noMatch ? T("panel.asked.nothingFound") : count(entry.hits, T("unit.lesson")))),
|
|
784
1198
|
entry.title ? h("span", { style: DIM }, " · top: " + entry.title + (entry.evidence ? " (" + entry.evidence + ")" : "")) : null,
|
|
785
1199
|
h("span", { style: DIM }, " · " + vote));
|
|
786
1200
|
});
|
|
@@ -788,7 +1202,7 @@ window.__ModuleLoader__.load({
|
|
|
788
1202
|
}
|
|
789
1203
|
|
|
790
1204
|
// ② reports filed
|
|
791
|
-
rows.push(h("div", { key: "s2", style: { marginTop: "12px", fontWeight: 600 } }, "
|
|
1205
|
+
rows.push(h("div", { key: "s2", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.reports")));
|
|
792
1206
|
var openReports = intakes.filter(function (entry) {
|
|
793
1207
|
return entry.state !== "converted" && entry.state !== "answered" && entry.state !== "already_have";
|
|
794
1208
|
});
|
|
@@ -799,62 +1213,61 @@ window.__ModuleLoader__.load({
|
|
|
799
1213
|
// is not allowed to do that, so the panel says who can re-check instead of offering it —
|
|
800
1214
|
// and only for the reports that are actually still open.
|
|
801
1215
|
rows.push(h("div", { key: "s2h", style: DIM },
|
|
802
|
-
count(openReports.length, "pending
|
|
1216
|
+
count(openReports.length, T("panel.report.pending")) + T("panel.report.recheck")));
|
|
803
1217
|
}
|
|
804
1218
|
if (!intakes.length) {
|
|
805
|
-
rows.push(h("div", { key: "s2e", style: DIM }, "
|
|
1219
|
+
rows.push(h("div", { key: "s2e", style: DIM }, T("panel.reports.none")));
|
|
806
1220
|
} else {
|
|
807
1221
|
rows.push(h("div", { key: "s2r" }, intakes.slice().reverse().map(function (entry) {
|
|
808
1222
|
var labels = {
|
|
809
|
-
filing: "filing", pending: "
|
|
810
|
-
already_have: "
|
|
1223
|
+
filing: "filing", pending: T("intake.waiting"), answered: "answered",
|
|
1224
|
+
already_have: T("intake.alreadyHave"), converted: T("intake.cited"), unknown: "submitted",
|
|
811
1225
|
};
|
|
812
1226
|
var issue = String(entry.issue || "").replace(/^issue-/, "#").replace(/^.*\/issues\//, "#");
|
|
813
1227
|
return h("div", { key: "r" + entry.callId + entry.at, style: { borderTop: "1px solid " + MUTED, padding: "4px 0" } },
|
|
814
1228
|
h("span", { style: DIM }, clockOf(entry.at) + " "),
|
|
815
1229
|
issue ? h("span", { style: DIM }, issue + " ") : null,
|
|
816
1230
|
h("span", { style: DIM }, (labels[entry.state] || entry.state) + " "),
|
|
817
|
-
h("span", {}, entry.text ? (entry.text.length > 90 ? entry.text.slice(0, 90) + "…" : entry.text) :
|
|
1231
|
+
h("span", {}, entry.text ? (entry.text.length > 90 ? entry.text.slice(0, 90) + "…" : entry.text) : T("intake.noText")),
|
|
818
1232
|
entry.receipt ? h("div", { style: DIM }, entry.receipt) : null,
|
|
819
1233
|
entry.answer ? h("div", {}, entry.answer) : null);
|
|
820
1234
|
})));
|
|
821
1235
|
}
|
|
822
1236
|
|
|
823
1237
|
// ③ how much these lessons are trusted
|
|
824
|
-
rows.push(h("div", { key: "s3", style: { marginTop: "12px", fontWeight: 600 } }, "
|
|
1238
|
+
rows.push(h("div", { key: "s3", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.trust")));
|
|
825
1239
|
if (!lessons.length) {
|
|
826
|
-
rows.push(h("div", { key: "s3e", style: DIM }, "
|
|
1240
|
+
rows.push(h("div", { key: "s3e", style: DIM }, T("panel.trust.none")));
|
|
827
1241
|
} else {
|
|
828
1242
|
// The rule once, not per row: it is the same sentence for every lesson, and repeating it
|
|
829
1243
|
// four times buries the numbers it is there to explain.
|
|
830
1244
|
rows.push(h("div", { key: "s3rule", style: DIM },
|
|
831
|
-
"
|
|
832
|
-
"one makes the reuse event appear, the second is what agents read as E4."));
|
|
1245
|
+
T("panel.trust.footnote")));
|
|
833
1246
|
rows.push(h("div", { key: "s3r" }, lessons.map(function (lesson) {
|
|
834
1247
|
var confirmations = trust[lesson.lessonId];
|
|
835
|
-
var counted = confirmations === undefined ? "checking
|
|
836
|
-
: (confirmations === null ? "unavailable"
|
|
837
|
-
: count(confirmations, "human
|
|
1248
|
+
var counted = confirmations === undefined ? T("panel.trust.checking")
|
|
1249
|
+
: (confirmations === null ? T("panel.trust.unavailable")
|
|
1250
|
+
: count(confirmations, T("panel.trust.human")) + (confirmations >= 2 ? " → E4" : ""));
|
|
838
1251
|
return h("div", { key: "l" + lesson.lessonId, style: { borderTop: "1px solid " + MUTED, padding: "4px 0" } },
|
|
839
1252
|
h("span", {}, lesson.title || lesson.lessonId),
|
|
840
|
-
h("span", { style: DIM }, " " + (lesson.evidence || "
|
|
841
|
-
"
|
|
1253
|
+
h("span", { style: DIM }, " " + (lesson.evidence || T("panel.trust.none")) + " · " + counted +
|
|
1254
|
+
T("panel.trust.used") + lesson.count + "× · " + timeRange(lesson)));
|
|
842
1255
|
})));
|
|
843
1256
|
}
|
|
844
1257
|
|
|
845
1258
|
// ④ your activity, two scopes
|
|
846
|
-
rows.push(h("div", { key: "s4", style: { marginTop: "12px", fontWeight: 600 } }, "
|
|
1259
|
+
rows.push(h("div", { key: "s4", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.activity")));
|
|
847
1260
|
rows.push(h("div", { key: "s4r", style: BOX },
|
|
848
|
-
h("div", {}, "
|
|
849
|
-
"
|
|
850
|
-
count(converted, "conversion")),
|
|
851
|
-
h("div", {}, "
|
|
1261
|
+
h("div", {}, T("panel.activity.session") + count(searches.length, "search", "searches") + " · " + count(lessons.length, "lesson") +
|
|
1262
|
+
T("panel.activity.reused") + count(votes.length, "vote") + " · " + count(intakes.length, "report") + T("panel.activity.filed") +
|
|
1263
|
+
count(converted, T("panel.activity.conversion"))),
|
|
1264
|
+
h("div", {}, T("panel.activity.browser") + count(stats.searches || 0, T("unit.search"), T("unit.searches")) + " · " +
|
|
852
1265
|
count(stats.lessonsReused || 0, "lesson") + " reused · " + count(stats.votes || 0, "vote") + " · " +
|
|
853
1266
|
count(stats.reportsFiled || 0, "report") + " filed · " + count(stats.reportsConverted || 0, "conversion")),
|
|
854
|
-
h("div", { style: DIM }, "
|
|
1267
|
+
h("div", { style: DIM }, T("panel.activity.scope"))));
|
|
855
1268
|
|
|
856
1269
|
// ⑤ voice
|
|
857
|
-
rows.push(h("div", { key: "s5", style: { marginTop: "12px", fontWeight: 600 } }, "
|
|
1270
|
+
rows.push(h("div", { key: "s5", style: { marginTop: "12px", fontWeight: 600 } }, T("panel.voice")));
|
|
858
1271
|
rows.push(h("div", { key: "s5r", style: BOX },
|
|
859
1272
|
h("button", {
|
|
860
1273
|
type: "button",
|
|
@@ -864,22 +1277,19 @@ window.__ModuleLoader__.load({
|
|
|
864
1277
|
setVoiceEnabled(next);
|
|
865
1278
|
setVoice(next);
|
|
866
1279
|
},
|
|
867
|
-
}, voice ? "
|
|
1280
|
+
}, T(voice ? "panel.voice.on" : "panel.voice.off")),
|
|
868
1281
|
lastCue && h("button", {
|
|
869
1282
|
type: "button",
|
|
870
1283
|
style: BUTTON,
|
|
871
1284
|
onClick: function () { playCue(lastCue); },
|
|
872
|
-
}, "
|
|
1285
|
+
}, T("panel.voice.play") + lastCue + "”"),
|
|
873
1286
|
h("div", { style: DIM }, lastCue
|
|
874
|
-
? "
|
|
875
|
-
: "
|
|
876
|
-
h("div", { style: DIM }, "
|
|
1287
|
+
? T("panel.voice.cue") + lastCue + T("panel.voice.cueTail")
|
|
1288
|
+
: T("panel.voice.none")),
|
|
1289
|
+
h("div", { style: DIM }, T("panel.voice.hook"))));
|
|
877
1290
|
|
|
878
1291
|
rows.push(h("div", { key: "foot", style: Object.assign({}, DIM, { marginTop: "12px" }) },
|
|
879
|
-
"
|
|
880
|
-
"(/api/helpful). The trust counts are other people's confirmations, not yours. Reloading the window loses " +
|
|
881
|
-
"the session rows; the browser counters stay. Receipts shown here are what the agent's own calls returned — " +
|
|
882
|
-
"a pending report is re-checked when the agent asks again, not by this page."));
|
|
1292
|
+
T("panel.footnote")));
|
|
883
1293
|
|
|
884
1294
|
return h("div", { style: { fontSize: "12px", lineHeight: "1.7", padding: "4px 2px" } }, rows);
|
|
885
1295
|
}
|
|
@@ -912,6 +1322,515 @@ window.__ModuleLoader__.load({
|
|
|
912
1322
|
return h(react.Fragment, null, MisakanetGlyph({ size: 16 }), h("span", { style: { paddingLeft: "4px" } }, "MisakaNet"));
|
|
913
1323
|
}
|
|
914
1324
|
|
|
1325
|
+
/** One result row: the title, where it came from, and a link to read it. */
|
|
1326
|
+
function MisakanetOverlayRow(result) {
|
|
1327
|
+
var path = String(result && result.id ? result.id : "");
|
|
1328
|
+
var href = path ? "https://misakanet.org/lessons/" + path : "https://misakanet.org/";
|
|
1329
|
+
return h("a", {
|
|
1330
|
+
href: href, target: "_blank", rel: "noreferrer",
|
|
1331
|
+
style: { display: "block", padding: "7px 8px", borderRadius: "8px", textDecoration: "none",
|
|
1332
|
+
color: "inherit", border: "1px solid transparent" },
|
|
1333
|
+
}, h("div", { style: { display: "flex", alignItems: "baseline", gap: "6px" } },
|
|
1334
|
+
h("span", { style: { fontWeight: 600 } }, String(result && result.title ? result.title : "(untitled)")),
|
|
1335
|
+
result && result.domain ? h("span", { style: { opacity: 0.6, fontSize: "11px" } }, String(result.domain)) : null),
|
|
1336
|
+
density() === "full"
|
|
1337
|
+
? h("div", { style: { opacity: 0.65, fontSize: "11px", marginTop: "2px" } }, path) : null,
|
|
1338
|
+
density() === "full" && result && result.description
|
|
1339
|
+
? h("div", { style: { opacity: 0.6, fontSize: "11px", marginTop: "2px" } },
|
|
1340
|
+
String(result.description).replace(/\s+/g, " ").slice(0, 160))
|
|
1341
|
+
: null);
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
|
|
1345
|
+
/**
|
|
1346
|
+
* The row's configuration as a `cordis.patch.yml` entry.
|
|
1347
|
+
*
|
|
1348
|
+
* Both cards that show this row cannot write it: the plugin page builds forms only from `volatile()`
|
|
1349
|
+
* fields, and a settings row's namespace is not always exposed. What a reader needs at that point is the
|
|
1350
|
+
* text they can paste into their own patch layer, so this produces exactly that — the same document the
|
|
1351
|
+
* host's native editor points at.
|
|
1352
|
+
*/
|
|
1353
|
+
function patchSnippet(values) {
|
|
1354
|
+
var lines = ["- id: " + ROW_ID, " disabled: false", " config:"];
|
|
1355
|
+
var keys = Object.keys(values || {});
|
|
1356
|
+
for (var i = 0; i < keys.length; i++) {
|
|
1357
|
+
var name = keys[i];
|
|
1358
|
+
var value = values[name];
|
|
1359
|
+
if (value !== null && typeof value === "object") {
|
|
1360
|
+
lines.push(" " + name + ":");
|
|
1361
|
+
var inner = Object.keys(value);
|
|
1362
|
+
for (var j = 0; j < inner.length; j++) {
|
|
1363
|
+
lines.push(" " + inner[j] + ": " + yamlScalar(value[inner[j]]));
|
|
1364
|
+
}
|
|
1365
|
+
} else {
|
|
1366
|
+
lines.push(" " + name + ": " + yamlScalar(value));
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return lines.join("\n") + "\n";
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function yamlScalar(value) {
|
|
1373
|
+
if (typeof value === "boolean" || typeof value === "number") return String(value);
|
|
1374
|
+
if (value === null || value === undefined) return '""';
|
|
1375
|
+
var text = String(value);
|
|
1376
|
+
return /^[A-Za-z0-9._\/-]+$/.test(text) ? text : JSON.stringify(text);
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** Copy text and report it in the button's own label; never throws at the reader. */
|
|
1380
|
+
function copyText(text, onDone) {
|
|
1381
|
+
try {
|
|
1382
|
+
window.navigator.clipboard.writeText(text).then(function () { onDone(T("settings.copied")); },
|
|
1383
|
+
function () { onDone(T("settings.copyFailed")); });
|
|
1384
|
+
} catch (error) {
|
|
1385
|
+
onDone(T("settings.copyFailed"));
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* The frame-wide toast (`shell.overlay`): the layer is click-through, so an occupant opts back into
|
|
1391
|
+
* pointer events itself, and this card does that on its own root.
|
|
1392
|
+
*
|
|
1393
|
+
* It announces the one thing worth interrupting for — a `/misakanet` search in this session came back
|
|
1394
|
+
* with lessons — and then gets out of the way: once per query, dismissible, and gone after a few
|
|
1395
|
+
* seconds. The title links to the lesson so the click lands somewhere useful.
|
|
1396
|
+
*/
|
|
1397
|
+
var TOAST_MS = 12000;
|
|
1398
|
+
|
|
1399
|
+
function MisakanetShellToast() {
|
|
1400
|
+
var revisionState = react.useState(0);
|
|
1401
|
+
var bump = function () { setRevision(function (n) { return n + 1; }); };
|
|
1402
|
+
var setRevision = revisionState[1];
|
|
1403
|
+
var hiddenState = react.useState(null);
|
|
1404
|
+
var hidden = hiddenState[0];
|
|
1405
|
+
var setHidden = hiddenState[1];
|
|
1406
|
+
|
|
1407
|
+
react.useEffect(function () { return subscribeShell(bump); }, []);
|
|
1408
|
+
react.useEffect(function () { return subscribePrefs(bump); }, []);
|
|
1409
|
+
react.useEffect(function () { return subscribeLocale(bump); }, []);
|
|
1410
|
+
|
|
1411
|
+
var log = lastSessionId ? sessions[lastSessionId] : null;
|
|
1412
|
+
// The overlay's own state is nested (`log.overlay`), unlike the counters beside it.
|
|
1413
|
+
var overlay = log && log.overlay ? log.overlay : null;
|
|
1414
|
+
var query = overlay && overlay.status === "done" && overlay.results && overlay.results.length
|
|
1415
|
+
? overlay.query : null;
|
|
1416
|
+
react.useEffect(function () {
|
|
1417
|
+
if (!query) return undefined;
|
|
1418
|
+
var timer = setTimeout(function () { setHidden(query); }, TOAST_MS);
|
|
1419
|
+
return function () { clearTimeout(timer); };
|
|
1420
|
+
}, [query]);
|
|
1421
|
+
|
|
1422
|
+
if (!query || hidden === query) return null;
|
|
1423
|
+
var top = overlay.results[0] || {};
|
|
1424
|
+
var lessonId = top.id ? String(top.id) : "";
|
|
1425
|
+
return h("div", {
|
|
1426
|
+
style: { position: "fixed", right: "16px", bottom: "16px", zIndex: 40, maxWidth: "320px",
|
|
1427
|
+
pointerEvents: "auto", background: "Canvas", color: "CanvasText", border: "1px solid rgba(127,127,127,.35)",
|
|
1428
|
+
borderRadius: "8px", boxShadow: "0 6px 20px rgba(0,0,0,.22)", padding: "10px 12px", fontSize: "13px",
|
|
1429
|
+
display: "grid", gap: "6px" },
|
|
1430
|
+
}, h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } },
|
|
1431
|
+
MisakanetGlyph({ size: 16 }),
|
|
1432
|
+
h("span", { style: { fontWeight: 600 } }, "MisakaNet"),
|
|
1433
|
+
h("span", { style: { opacity: 0.6, fontSize: "11px" } },
|
|
1434
|
+
overlay.results.length === 1 ? T("toast.one") : T("toast.many", { n: overlay.results.length })),
|
|
1435
|
+
h("span", { style: { flex: 1 } }),
|
|
1436
|
+
h("button", { type: "button", title: T("overlay.close"), "aria-label": T("overlay.close"),
|
|
1437
|
+
style: { background: "transparent", border: 0, color: "inherit", cursor: "pointer", opacity: 0.7 },
|
|
1438
|
+
onClick: function () { setHidden(query); } }, "×")),
|
|
1439
|
+
lessonId
|
|
1440
|
+
? h("a", { href: "https://misakanet.org/lessons/" + encodeURIComponent(lessonId),
|
|
1441
|
+
target: "_blank", rel: "noreferrer", style: { color: "inherit", fontWeight: 600 } },
|
|
1442
|
+
String(top.title || lessonId))
|
|
1443
|
+
: h("span", null, String(top.title || "")),
|
|
1444
|
+
top.domain ? h("span", { style: { opacity: 0.65, fontSize: "11px" } }, String(top.domain)) : null);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* The sidebar footer action (`sidebar.footer.action`): copy this session's MisakaNet activity as a
|
|
1449
|
+
* summary, which is exactly what a bug report or a PR body wants to quote.
|
|
1450
|
+
*
|
|
1451
|
+
* It receives only the column state, so it reads the same most-recently-touched session the toast does,
|
|
1452
|
+
* and reports what it did in its own label rather than throwing a dialog at the reader.
|
|
1453
|
+
*/
|
|
1454
|
+
function MisakanetFooterAction() {
|
|
1455
|
+
var revisionState = react.useState(0);
|
|
1456
|
+
var bump = function () { setRevision(function (n) { return n + 1; }); };
|
|
1457
|
+
var setRevision = revisionState[1];
|
|
1458
|
+
var noteState = react.useState(null);
|
|
1459
|
+
var note = noteState[0];
|
|
1460
|
+
var setNote = noteState[1];
|
|
1461
|
+
|
|
1462
|
+
react.useEffect(function () { return subscribeShell(bump); }, []);
|
|
1463
|
+
react.useEffect(function () { return subscribePrefs(bump); }, []);
|
|
1464
|
+
react.useEffect(function () { return subscribeLocale(bump); }, []);
|
|
1465
|
+
|
|
1466
|
+
function summary() {
|
|
1467
|
+
var log = lastSessionId ? sessions[lastSessionId] : null;
|
|
1468
|
+
if (!log) return "";
|
|
1469
|
+
var searched = log.searches ? log.searches.length : 0;
|
|
1470
|
+
var withLesson = log.searches ? log.searches.filter(function (s) { return s.lessonId; }).length : 0;
|
|
1471
|
+
var ids = log.searches ? log.searches.map(function (s) { return s.lessonId; }).filter(Boolean) : [];
|
|
1472
|
+
var helpful = log.votes ? log.votes.filter(function (v) { return v && v.helpful; }).length : 0;
|
|
1473
|
+
var overlay = log.overlay || {};
|
|
1474
|
+
var lines = [
|
|
1475
|
+
"MisakaNet — this session",
|
|
1476
|
+
T("footer.session") + String(lastSessionId).replace(/^session-/, "").slice(0, 8),
|
|
1477
|
+
"tool searches: " + searched + " (" + withLesson + " with a lesson)",
|
|
1478
|
+
"lessons: " + (ids.length ? ids.join(", ") : "—"),
|
|
1479
|
+
"votes: " + (log.votes ? log.votes.length : 0) + " (helpful " + helpful + ")",
|
|
1480
|
+
"reports filed: " + (log.intakes ? log.intakes.length : 0),
|
|
1481
|
+
];
|
|
1482
|
+
// The `/misakanet` command never goes through the MCP tools, so its search is its own line
|
|
1483
|
+
// rather than being folded into a count that would then describe something else.
|
|
1484
|
+
if (overlay.query) {
|
|
1485
|
+
lines.push("composer search: '" + overlay.query + "' → " +
|
|
1486
|
+
(overlay.status === "done" ? (overlay.results || []).length + " results" : overlay.status));
|
|
1487
|
+
}
|
|
1488
|
+
return lines.join("\n");
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function copy() {
|
|
1492
|
+
var text = summary();
|
|
1493
|
+
if (!text) { setNote(T("footer.nothing")); return; }
|
|
1494
|
+
try {
|
|
1495
|
+
window.navigator.clipboard.writeText(text).then(function () { setNote(T("footer.copied")); },
|
|
1496
|
+
function () { setNote(T("footer.failed")); });
|
|
1497
|
+
} catch (error) {
|
|
1498
|
+
setNote(T("footer.failed"));
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
var label = note || T("footer.action");
|
|
1503
|
+
return h("button", { type: "button", title: T("footer.actionHint"), "aria-label": label, onClick: copy,
|
|
1504
|
+
style: { display: "inline-flex", alignItems: "center", gap: "6px", background: "transparent",
|
|
1505
|
+
border: 0, color: "inherit", cursor: "pointer", fontSize: "12px", padding: "4px 6px" },
|
|
1506
|
+
}, MisakanetGlyph({ size: 16 }), h("span", null, label));
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
/**
|
|
1510
|
+
* The settings row (`settings.general.item`, root scope, **no props from the owner**).
|
|
1511
|
+
*
|
|
1512
|
+
* The host's contract for this seat is explicit: "the section column only stacks rows, so a row draws
|
|
1513
|
+
* its own internals, including its label … copy, current value, and the write path are all yours".
|
|
1514
|
+
* So this renders its own header and its controls.
|
|
1515
|
+
*
|
|
1516
|
+
* Two of the three are browser preferences this page owns (voice cues, how much the surfaces show).
|
|
1517
|
+
* The third is the MCP endpoint, and it is the interesting one: the plugin page could not offer a form
|
|
1518
|
+
* for it, because the host builds those only from `volatile()` fields — but a settings row may hold the
|
|
1519
|
+
* namespace's `ConfigForm` itself and write through it. When the host exposes no such namespace, the row
|
|
1520
|
+
* says where the value lives instead of pretending to edit it.
|
|
1521
|
+
*/
|
|
1522
|
+
function MisakanetSettingsRow(props) {
|
|
1523
|
+
var face = (props && props.hooks) || {};
|
|
1524
|
+
var hostForm = face.hostForm;
|
|
1525
|
+
var revisionState = react.useState(0);
|
|
1526
|
+
var setRevision = revisionState[1];
|
|
1527
|
+
var bump = function () { setRevision(function (n) { return n + 1; }); };
|
|
1528
|
+
var urlState = react.useState(null);
|
|
1529
|
+
var url = urlState[0];
|
|
1530
|
+
var setUrl = urlState[1];
|
|
1531
|
+
var statusState = react.useState(null);
|
|
1532
|
+
var status = statusState[0];
|
|
1533
|
+
var setStatus = statusState[1];
|
|
1534
|
+
|
|
1535
|
+
react.useEffect(function () { return subscribePrefs(bump); }, []);
|
|
1536
|
+
react.useEffect(function () { return subscribeLocale(bump); }, []);
|
|
1537
|
+
react.useEffect(function () {
|
|
1538
|
+
if (!hostForm || typeof hostForm.subscribe !== "function") return undefined;
|
|
1539
|
+
return hostForm.subscribe(bump);
|
|
1540
|
+
}, [hostForm]);
|
|
1541
|
+
var snapshot = hostForm && typeof hostForm.getSnapshot === "function" ? hostForm.getSnapshot() : null;
|
|
1542
|
+
react.useEffect(function () {
|
|
1543
|
+
setUrl(snapshot && snapshot.value && typeof snapshot.value.url === "string" ? snapshot.value.url : null);
|
|
1544
|
+
}, [snapshot && snapshot.revision, snapshot && snapshot.status]);
|
|
1545
|
+
|
|
1546
|
+
var writable = Boolean(snapshot && snapshot.status === "ready" && snapshot.writable !== false);
|
|
1547
|
+
|
|
1548
|
+
function saveUrl() {
|
|
1549
|
+
if (!hostForm || !snapshot || typeof hostForm.set !== "function") return;
|
|
1550
|
+
setStatus(T("settings.saving"));
|
|
1551
|
+
hostForm.set("url", String(url || "")).then(function (accepted) {
|
|
1552
|
+
setStatus(accepted ? T("settings.saved") : T("settings.refused"));
|
|
1553
|
+
}, function (error) {
|
|
1554
|
+
setStatus(T("settings.writeFailed", { error: (error && error.message) || String(error) }));
|
|
1555
|
+
});
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
return h("div", { style: { display: "grid", gap: "8px", fontSize: "13px", padding: "2px 0" } },
|
|
1559
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: "6px" } },
|
|
1560
|
+
MisakanetGlyph({ size: 16 }), h("span", { style: { fontWeight: 600 } }, "MisakaNet")),
|
|
1561
|
+
h("label", { style: { display: "flex", alignItems: "center", gap: "8px" } },
|
|
1562
|
+
h("input", { type: "checkbox", checked: voiceEnabled(),
|
|
1563
|
+
onChange: function (event) { setVoiceEnabled(event.target.checked); } }),
|
|
1564
|
+
h("span", null, T("settings.voice")),
|
|
1565
|
+
h("span", { style: { opacity: 0.6, fontSize: "11px" } }, T("settings.voice.note"))),
|
|
1566
|
+
h("label", { style: { display: "flex", alignItems: "center", gap: "8px" } },
|
|
1567
|
+
h("span", null, T("settings.density")),
|
|
1568
|
+
h("select", { value: density(), onChange: function (event) { setDensity(event.target.value); } },
|
|
1569
|
+
h("option", { value: "compact" }, T("settings.density.compact")),
|
|
1570
|
+
h("option", { value: "full" }, T("settings.density.full")))),
|
|
1571
|
+
snapshot && snapshot.status === "ready"
|
|
1572
|
+
? h("div", { style: { display: "grid", gap: "6px" } },
|
|
1573
|
+
h("label", { style: { display: "flex", alignItems: "center", gap: "8px" } },
|
|
1574
|
+
h("span", { style: { width: "9rem", opacity: 0.8 } }, T("settings.endpoint")),
|
|
1575
|
+
h("input", { type: "text", value: url === null ? "" : url, disabled: !writable,
|
|
1576
|
+
style: { width: "22rem", maxWidth: "100%" },
|
|
1577
|
+
onChange: function (event) { setUrl(event.target.value); setStatus(null); } }),
|
|
1578
|
+
h("button", { type: "button", disabled: !writable, onClick: saveUrl,
|
|
1579
|
+
style: { cursor: writable ? "pointer" : "not-allowed" } }, T("settings.save")),
|
|
1580
|
+
status ? h("span", { style: { opacity: 0.75, fontSize: "12px" } }, status) : null),
|
|
1581
|
+
writable ? null : h("span", { style: { opacity: 0.6, fontSize: "11px" } },
|
|
1582
|
+
T("settings.readonly")))
|
|
1583
|
+
: h("span", { style: { display: "inline-flex", alignItems: "center", gap: "8px" } },
|
|
1584
|
+
h("span", { style: { opacity: 0.6, fontSize: "11px" } },
|
|
1585
|
+
T("settings.patchHint", { row: ROW_ID })),
|
|
1586
|
+
h("button", { type: "button", style: { cursor: "pointer" },
|
|
1587
|
+
onClick: function () {
|
|
1588
|
+
// this row has a `snapshot`, not the card's `resolved`: naming the wrong one threw
|
|
1589
|
+
// `resolved is not defined` in the live render, which is how it was caught
|
|
1590
|
+
copyText(patchSnippet(snapshot && snapshot.value ? snapshot.value : {}), setStatus);
|
|
1591
|
+
} },
|
|
1592
|
+
T("settings.copyPatch")),
|
|
1593
|
+
status ? h("span", { style: { fontSize: "11px", opacity: 0.8 } }, status) : null),
|
|
1594
|
+
h("span", { style: { opacity: 0.55, fontSize: "11px" } },
|
|
1595
|
+
T("settings.local")));
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* The MCP row's configuration card (`plugins.row.config`, key `<bundle>#<row id>`).
|
|
1600
|
+
*
|
|
1601
|
+
* The host owns the values and the writes: it hands `{view, form}` where `form.state` is the resolved
|
|
1602
|
+
* section (schema defaults composed with whatever the user stored) and `form.mutate(ops, revision)`
|
|
1603
|
+
* is the only write path. The fields are derived from the values rather than hard-coded, so the schema
|
|
1604
|
+
* in `index.js` stays the single source of truth for what the row accepts.
|
|
1605
|
+
*
|
|
1606
|
+
* `view: 'summary'` is what the plugin page puts on the row when the row has no description of its own;
|
|
1607
|
+
* `view: 'page'` is the form behind its configure control.
|
|
1608
|
+
*/
|
|
1609
|
+
function MisakanetRowConfig(props) {
|
|
1610
|
+
var view = props && props.view;
|
|
1611
|
+
var form = props && props.form;
|
|
1612
|
+
var snapshot = form && form.state ? form.state : null;
|
|
1613
|
+
var resolved = (snapshot && snapshot.value) || {};
|
|
1614
|
+
var draftState = react.useState(null);
|
|
1615
|
+
var draft = draftState[0];
|
|
1616
|
+
var setDraft = draftState[1];
|
|
1617
|
+
var rawState = react.useState({});
|
|
1618
|
+
var raw = rawState[0];
|
|
1619
|
+
var setRaw = rawState[1];
|
|
1620
|
+
var statusState = react.useState(null);
|
|
1621
|
+
var status = statusState[0];
|
|
1622
|
+
var setStatus = statusState[1];
|
|
1623
|
+
|
|
1624
|
+
react.useEffect(function () {
|
|
1625
|
+
try {
|
|
1626
|
+
setDraft(snapshot && snapshot.value ? JSON.parse(JSON.stringify(snapshot.value)) : null);
|
|
1627
|
+
setRaw({});
|
|
1628
|
+
} catch (error) { /* a non-JSON section is not editable here; the read-only note covers it */ }
|
|
1629
|
+
}, [snapshot && snapshot.revision, snapshot && snapshot.status]);
|
|
1630
|
+
|
|
1631
|
+
if (view === "summary") {
|
|
1632
|
+
return h("span", null, String(resolved.transport || "streamable-http") + " → " +
|
|
1633
|
+
String(resolved.url || DEFAULT_MCP_URL));
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
if (!form || !snapshot || snapshot.status !== "ready" || !draft) {
|
|
1637
|
+
// This is the expected state for this row, not a failure. The host builds a form only from
|
|
1638
|
+
// fields its schema marks `volatile()` — ones it can change *without remounting* the plugin
|
|
1639
|
+
// (read out of `dsh-settings`: `volatileForm(schema)` filters on `schema.meta.volatile`, and a
|
|
1640
|
+
// schema with none returns no form at all). An MCP endpoint or transport can only take effect
|
|
1641
|
+
// after a reload, so claiming volatility to win a form would be a lie about this plugin.
|
|
1642
|
+
// What the card can do honestly is show the values in force and say where they are edited.
|
|
1643
|
+
return h("div", { style: { margin: "4px 0", fontSize: "13px", display: "grid", gap: "6px" } },
|
|
1644
|
+
h("p", { style: { margin: 0, opacity: 0.75 } },
|
|
1645
|
+
T("settings.rowReadonly", { row: ROW_ID, what: form ? T("settings.what.form") : T("settings.what.row") })),
|
|
1646
|
+
// The host serves no form here, so hand the reader the text they can paste instead of a dead end.
|
|
1647
|
+
h("div", { style: { display: "inline-flex", alignItems: "center", gap: "8px" } },
|
|
1648
|
+
h("button", { type: "button", style: { cursor: "pointer" },
|
|
1649
|
+
onClick: function () { copyText(patchSnippet(resolved), setStatus); } },
|
|
1650
|
+
T("settings.copyPatch")),
|
|
1651
|
+
status ? h("span", { style: { fontSize: "11px", opacity: 0.8 } }, status) : null),
|
|
1652
|
+
Object.keys(resolved).length
|
|
1653
|
+
? h("table", { style: { borderCollapse: "collapse" } },
|
|
1654
|
+
Object.keys(resolved).map(function (name) {
|
|
1655
|
+
return h("tr", { key: name },
|
|
1656
|
+
h("td", { style: { padding: "2px 10px 2px 0", opacity: 0.7 } }, name),
|
|
1657
|
+
h("td", { style: { fontFamily: "monospace", fontSize: "12px" } }, JSON.stringify(resolved[name])));
|
|
1658
|
+
}))
|
|
1659
|
+
: h("p", { style: { margin: 0, opacity: 0.7 } },
|
|
1660
|
+
T("settings.rowNoValues")));
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
var fields = Object.keys(draft);
|
|
1664
|
+
var readOnly = snapshot.writable === false;
|
|
1665
|
+
|
|
1666
|
+
function change(name, value) {
|
|
1667
|
+
var next = JSON.parse(JSON.stringify(draft));
|
|
1668
|
+
next[name] = value;
|
|
1669
|
+
setDraft(next);
|
|
1670
|
+
setStatus(null);
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function save() {
|
|
1674
|
+
var ops = [];
|
|
1675
|
+
for (var i = 0; i < fields.length; i++) {
|
|
1676
|
+
var name = fields[i];
|
|
1677
|
+
if (JSON.stringify(resolved[name]) !== JSON.stringify(draft[name])) {
|
|
1678
|
+
ops.push({ op: "set", path: [name], value: draft[name] });
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (!ops.length) { setStatus({ kind: "ok", text: "Nothing changed." }); return; }
|
|
1682
|
+
setStatus({ kind: "ok", text: "Saving " + ops.length + " field(s)…" });
|
|
1683
|
+
form.mutate(ops, snapshot.revision).then(function (accepted) {
|
|
1684
|
+
setStatus(accepted
|
|
1685
|
+
? { kind: "ok", text: "Saved — the row applies it on the next host start." }
|
|
1686
|
+
: { kind: "error", text: "The host refused the write." });
|
|
1687
|
+
}, function (error) {
|
|
1688
|
+
setStatus({ kind: "error", text: "Write failed: " + ((error && error.message) || String(error)) });
|
|
1689
|
+
});
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
return h("div", { style: { fontSize: "13px", display: "grid", gap: "6px" } },
|
|
1693
|
+
fields.map(function (name) {
|
|
1694
|
+
var value = draft[name];
|
|
1695
|
+
var control;
|
|
1696
|
+
if (typeof value === "boolean") {
|
|
1697
|
+
control = h("input", { type: "checkbox", checked: value, disabled: readOnly,
|
|
1698
|
+
onChange: function (event) { change(name, event.target.checked); } });
|
|
1699
|
+
} else if (typeof value === "number") {
|
|
1700
|
+
control = h("input", { type: "number", value: String(value), disabled: readOnly,
|
|
1701
|
+
style: { width: "10rem" },
|
|
1702
|
+
onChange: function (event) { change(name, Number(event.target.value)); } });
|
|
1703
|
+
} else if (typeof value === "string") {
|
|
1704
|
+
control = h("input", { type: "text", value: value, disabled: readOnly,
|
|
1705
|
+
style: { width: "24rem", maxWidth: "100%" },
|
|
1706
|
+
onChange: function (event) { change(name, event.target.value); } });
|
|
1707
|
+
} else {
|
|
1708
|
+
var text = raw[name] !== undefined ? raw[name] : JSON.stringify(value);
|
|
1709
|
+
control = h("span", { style: { display: "inline-flex", alignItems: "center", gap: "6px" } },
|
|
1710
|
+
h("textarea", { value: text, disabled: readOnly, rows: 3,
|
|
1711
|
+
style: { width: "24rem", maxWidth: "100%", fontFamily: "monospace", fontSize: "12px" },
|
|
1712
|
+
onChange: function (event) {
|
|
1713
|
+
var next = {}; next[name] = event.target.value; setRaw(next);
|
|
1714
|
+
try { change(name, JSON.parse(event.target.value)); } catch (error) { /* keep the text */ }
|
|
1715
|
+
} }),
|
|
1716
|
+
raw[name] !== undefined && !isJson(raw[name])
|
|
1717
|
+
? h("span", { style: { fontSize: "11px", opacity: 0.7 } }, T("settings.invalidJson")) : null);
|
|
1718
|
+
}
|
|
1719
|
+
return h("label", { key: name, style: { display: "flex", alignItems: "center", gap: "8px" } },
|
|
1720
|
+
h("span", { style: { width: "11rem", opacity: 0.8 } }, name), control);
|
|
1721
|
+
}),
|
|
1722
|
+
readOnly
|
|
1723
|
+
? h("p", { style: { margin: 0, opacity: 0.7 } }, T("settings.noWrites"))
|
|
1724
|
+
: h("div", { style: { display: "flex", alignItems: "center", gap: "10px" } },
|
|
1725
|
+
h("button", { type: "button", onClick: save, style: { cursor: "pointer" } }, T("settings.save")),
|
|
1726
|
+
status ? h("span", { style: { opacity: 0.8 } }, status.text) : null));
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
/**
|
|
1730
|
+
* The `/misakanet` result overlay, rendered inside the resident composer card
|
|
1731
|
+
* (`conversation.input.overlay`, session scope).
|
|
1732
|
+
*
|
|
1733
|
+
* It is the one place in this half that asks the server for something the session does not already
|
|
1734
|
+
* have, so it also says what it sends: the query, anonymously, to `misakanet.org`.
|
|
1735
|
+
*/
|
|
1736
|
+
function MisakanetSearchOverlay(props) {
|
|
1737
|
+
var sessionId = props && props.sessionId;
|
|
1738
|
+
var revisionState = react.useState(0);
|
|
1739
|
+
var setRevision = revisionState[1];
|
|
1740
|
+
react.useEffect(function () {
|
|
1741
|
+
return subscribe(sessionId, function () { setRevision(function (n) { return n + 1; }); });
|
|
1742
|
+
}, [sessionId]);
|
|
1743
|
+
react.useEffect(function () { return subscribePrefs(function () { setRevision(function (n) { return n + 1; }); }); }, []);
|
|
1744
|
+
react.useEffect(function () { return subscribeLocale(function () { setRevision(function (n) { return n + 1; }); }); }, []);
|
|
1745
|
+
if (!sessionId) return null;
|
|
1746
|
+
var state = overlayOf(sessionId);
|
|
1747
|
+
if (!state.open) return null;
|
|
1748
|
+
|
|
1749
|
+
var body;
|
|
1750
|
+
if (state.status === "loading") {
|
|
1751
|
+
body = h("div", { style: { opacity: 0.7, fontSize: "12px", padding: "6px 2px" } }, T("overlay.searching"));
|
|
1752
|
+
} else if (state.status === "error") {
|
|
1753
|
+
body = h("div", { style: { fontSize: "12px", padding: "6px 2px" } },
|
|
1754
|
+
T("overlay.failed", { error: state.error }));
|
|
1755
|
+
} else if (state.status === "idle") {
|
|
1756
|
+
body = h("div", { style: { opacity: 0.7, fontSize: "12px", padding: "6px 2px" } },
|
|
1757
|
+
T("overlay.empty"));
|
|
1758
|
+
} else if (!state.results.length) {
|
|
1759
|
+
body = h("div", { style: { fontSize: "12px", padding: "6px 2px" } },
|
|
1760
|
+
T("overlay.none"));
|
|
1761
|
+
} else {
|
|
1762
|
+
body = h("div", null, state.results.map(MisakanetOverlayRow));
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
// `Canvas`/`CanvasText` are CSS system colours: they follow the host's light/dark theme without
|
|
1766
|
+
// this file knowing its variables, and the opaque background is what keeps the composer's own
|
|
1767
|
+
// text from showing through the card (measured: a transparent card overlapped the tool row).
|
|
1768
|
+
return h("div", {
|
|
1769
|
+
style: { border: "1px solid rgba(127,127,127,0.35)", borderRadius: "12px", padding: "8px 10px",
|
|
1770
|
+
margin: "6px 0", maxHeight: "320px", overflowY: "auto", fontSize: "13px",
|
|
1771
|
+
position: "relative", zIndex: 3, background: "Canvas", color: "CanvasText",
|
|
1772
|
+
boxShadow: "0 8px 24px rgba(0,0,0,0.18)" },
|
|
1773
|
+
}, h("div", { style: { display: "flex", alignItems: "center", gap: "6px", marginBottom: "4px" } },
|
|
1774
|
+
MisakanetGlyph({ size: 14 }),
|
|
1775
|
+
h("span", { style: { fontWeight: 600 } }, "MisakaNet"),
|
|
1776
|
+
state.query ? h("span", { style: { opacity: 0.6, fontSize: "12px" } }, "· " + state.query) : null,
|
|
1777
|
+
h("span", { style: { flex: 1 } }),
|
|
1778
|
+
h("button", {
|
|
1779
|
+
type: "button", title: T("overlay.close"),
|
|
1780
|
+
onClick: function () { setOverlay(sessionId, { open: false }); },
|
|
1781
|
+
style: { background: "none", border: "none", cursor: "pointer", color: "inherit", opacity: 0.7 },
|
|
1782
|
+
}, "×")),
|
|
1783
|
+
body,
|
|
1784
|
+
h("div", { style: { opacity: 0.55, fontSize: "11px", marginTop: "4px" } },
|
|
1785
|
+
T("overlay.footnote")));
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
/**
|
|
1789
|
+
* The `/misakanet` source for the host's slash pipeline.
|
|
1790
|
+
*
|
|
1791
|
+
* The pipeline owns the draft: picking the candidate claims the token (so the composer says what is
|
|
1792
|
+
* about to happen) and Enter then hands us the argument through `submit`. We answer with the
|
|
1793
|
+
* overlay instead of consuming the submit, which is why this is a claim and not an insert.
|
|
1794
|
+
*/
|
|
1795
|
+
function misakanetTrigger(lastSession) {
|
|
1796
|
+
return {
|
|
1797
|
+
trigger: "/",
|
|
1798
|
+
name: "misakanet",
|
|
1799
|
+
order: 5,
|
|
1800
|
+
candidates: function (session, request) {
|
|
1801
|
+
if (session && session.sessionId) lastSession.id = session.sessionId;
|
|
1802
|
+
if (!request || request.position !== "leading") return Promise.resolve([]);
|
|
1803
|
+
var query = String(request.query || "").trim().toLowerCase();
|
|
1804
|
+
if (query !== "" && "misakanet".indexOf(query) !== 0) return Promise.resolve([]);
|
|
1805
|
+
return Promise.resolve([{
|
|
1806
|
+
name: "misakanet",
|
|
1807
|
+
label: "MisakaNet search",
|
|
1808
|
+
description: "Search the failure-lesson library without leaving the composer",
|
|
1809
|
+
icon: MisakanetGlyph,
|
|
1810
|
+
section: "MisakaNet",
|
|
1811
|
+
}]);
|
|
1812
|
+
},
|
|
1813
|
+
onPick: function () {
|
|
1814
|
+
return { claim: {
|
|
1815
|
+
name: "misakanet",
|
|
1816
|
+
token: "/misakanet ",
|
|
1817
|
+
hint: "what broke?",
|
|
1818
|
+
submit: function (args) {
|
|
1819
|
+
var sessionId = lastSession.id;
|
|
1820
|
+
var query = String(args || "").trim();
|
|
1821
|
+
if (!query) {
|
|
1822
|
+
runOverlaySearch(sessionId, "");
|
|
1823
|
+
return Promise.resolve({ kind: "error",
|
|
1824
|
+
text: "Type what broke after /misakanet — e.g. /misakanet pip install timeout" });
|
|
1825
|
+
}
|
|
1826
|
+
runOverlaySearch(sessionId, query);
|
|
1827
|
+
return Promise.resolve({ kind: "success" });
|
|
1828
|
+
},
|
|
1829
|
+
} };
|
|
1830
|
+
},
|
|
1831
|
+
};
|
|
1832
|
+
}
|
|
1833
|
+
|
|
915
1834
|
/**
|
|
916
1835
|
* The left column's icon seat (`sidebar.panellist`, root scope → it does not come and go with a
|
|
917
1836
|
* session). The owner supplies `{size, active}` and owns the button: the id we register here is the
|
|
@@ -938,8 +1857,7 @@ window.__ModuleLoader__.load({
|
|
|
938
1857
|
return h("div", { style: { height: "100%", overflowY: "auto", padding: "28px 32px 48px" } },
|
|
939
1858
|
h("div", { style: { maxWidth: "860px", margin: "0 auto" } },
|
|
940
1859
|
h("p", { style: { margin: "0 0 14px", color: "inherit", opacity: 0.7, fontSize: "13px", lineHeight: "1.5" } },
|
|
941
|
-
"
|
|
942
|
-
"right-sidebar MisakaNet tab. The numbers here are what this browser has contributed."),
|
|
1860
|
+
T("main.footnote")),
|
|
943
1861
|
h(MisakanetPanel, { sessionId: props && props.sessionId })));
|
|
944
1862
|
}
|
|
945
1863
|
|
|
@@ -1014,6 +1932,97 @@ window.__ModuleLoader__.load({
|
|
|
1014
1932
|
{ name: "main", key: PANEL_ID },
|
|
1015
1933
|
MisakanetMainView, "main panel");
|
|
1016
1934
|
|
|
1935
|
+
// The composer: a result overlay above the input, and the `/misakanet` source that opens it.
|
|
1936
|
+
// The source is a *service* inject, not a slot — the slash pipeline owns the draft, so the
|
|
1937
|
+
// interesting contract is `registerSource`, and a host without that service keeps every other
|
|
1938
|
+
// seat instead of failing activation.
|
|
1939
|
+
// The plugin page's row: the key is `<bundle>#<row id>` as the patch declares it. Without the
|
|
1940
|
+
// host's `form` the card renders the resolved values read-only, which is also what a host that
|
|
1941
|
+
// serves no schema for the row gets.
|
|
1942
|
+
// Two keys, because the plugin page looks a form up twice with different names: the bundle's own
|
|
1943
|
+
// config is keyed by the **package name** (`ItemDetail` → `formFor(item.id)`), while a row's is
|
|
1944
|
+
// keyed by the **row id** (`RowDetail` → `formFor(row.rowId)`). Whichever namespace the host
|
|
1945
|
+
// actually documents, one of these finds it — and the card renders read-only values when neither
|
|
1946
|
+
// does, so the entry is never a dead end.
|
|
1947
|
+
// The dictionaries and the binding. `bind` keeps a stable identity and reads the active locale on
|
|
1948
|
+
// every call, so one binding serves every component; the subscription is what lets an already
|
|
1949
|
+
// rendered surface switch language without a reload.
|
|
1950
|
+
step("dictionaries", function () {
|
|
1951
|
+
if (typeof ctx.inject !== "function") throw new Error("ctx.inject is not available");
|
|
1952
|
+
return ctx.inject(["locale"], function (scoped) {
|
|
1953
|
+
var locale = scoped && (scoped.locale ||
|
|
1954
|
+
(typeof scoped.get === "function" ? scoped.get("locale") : undefined));
|
|
1955
|
+
if (!locale || typeof locale.register !== "function" || typeof locale.bind !== "function") {
|
|
1956
|
+
return undefined; // no locale service: the surfaces stay in English, the source text
|
|
1957
|
+
}
|
|
1958
|
+
localeFace = locale;
|
|
1959
|
+
T = locale.bind(NS);
|
|
1960
|
+
var stops = [];
|
|
1961
|
+
if (typeof locale.subscribe === "function") {
|
|
1962
|
+
stops.push(locale.subscribe(function () { notifyLocale(); }));
|
|
1963
|
+
}
|
|
1964
|
+
return ctx.effect(function () {
|
|
1965
|
+
var disposer = locale.register(NS, { en: en, zh: zh });
|
|
1966
|
+
return function () {
|
|
1967
|
+
for (var i = 0; i < stops.length; i++) {
|
|
1968
|
+
if (typeof stops[i] === "function") stops[i]();
|
|
1969
|
+
}
|
|
1970
|
+
if (typeof disposer === "function") disposer();
|
|
1971
|
+
};
|
|
1972
|
+
}, "misakanet: dictionaries");
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
// The two frame-wide seats. Both are root scope and receive no `sessionId`, which is why the store
|
|
1977
|
+
// tracks the most recently touched session for them.
|
|
1978
|
+
registerWhenDeclared("shell.overlay",
|
|
1979
|
+
{ name: "shell.overlay", id: "misakanet-lesson-toast", order: 30, label: function () { return "MisakaNet"; } },
|
|
1980
|
+
MisakanetShellToast, "shell overlay");
|
|
1981
|
+
registerWhenDeclared("sidebar.footer.action",
|
|
1982
|
+
{ name: "sidebar.footer.action", id: "misakanet-summary", order: 40,
|
|
1983
|
+
label: function () { return T("footer.action"); } },
|
|
1984
|
+
MisakanetFooterAction, "footer action");
|
|
1985
|
+
// The settings row. `configForms` is a service, so it is read defensively: a host without it keeps
|
|
1986
|
+
// the row's browser preferences and loses only the endpoint field.
|
|
1987
|
+
step("settings row", function () {
|
|
1988
|
+
var face = {};
|
|
1989
|
+
try {
|
|
1990
|
+
if (ctx.configForms && typeof ctx.configForms.get === "function") {
|
|
1991
|
+
var form = ctx.configForms.get(ROW_ID);
|
|
1992
|
+
if (form && typeof form.getSnapshot === "function" && typeof form.set === "function") {
|
|
1993
|
+
face.hostForm = form;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1996
|
+
} catch (error) { /* the row still carries its two browser preferences */ }
|
|
1997
|
+
return ctx.effect(function () {
|
|
1998
|
+
return ctx.slots.inject("settings.general.item", function () {
|
|
1999
|
+
return ctx.slots.register({
|
|
2000
|
+
name: "settings.general.item", id: PANEL_ID, order: 50,
|
|
2001
|
+
inject: function () { return { hooks: face }; },
|
|
2002
|
+
}, MisakanetSettingsRow);
|
|
2003
|
+
});
|
|
2004
|
+
}, "misakanet: settings row");
|
|
2005
|
+
});
|
|
2006
|
+
registerWhenDeclared("plugins.bundle.config",
|
|
2007
|
+
{ name: "plugins.bundle.config", key: "misakanet" },
|
|
2008
|
+
MisakanetRowConfig, "bundle config");
|
|
2009
|
+
registerWhenDeclared("plugins.row.config",
|
|
2010
|
+
{ name: "plugins.row.config", key: "misakanet#misakanet-mcp" },
|
|
2011
|
+
MisakanetRowConfig, "row config");
|
|
2012
|
+
registerWhenDeclared("conversation.input.overlay",
|
|
2013
|
+
{ name: "conversation.input.overlay", id: "misakanet-search", order: 20 },
|
|
2014
|
+
MisakanetSearchOverlay, "search overlay");
|
|
2015
|
+
step("slash command", function () {
|
|
2016
|
+
if (typeof ctx.inject !== "function") throw new Error("ctx.inject is not available");
|
|
2017
|
+
var lastSession = { id: null };
|
|
2018
|
+
return ctx.inject(["inputTriggers"], function (scoped) {
|
|
2019
|
+
var triggers = scoped && (scoped.inputTriggers ||
|
|
2020
|
+
(typeof scoped.get === "function" ? scoped.get("inputTriggers") : undefined));
|
|
2021
|
+
if (!triggers || typeof triggers.registerSource !== "function") return undefined;
|
|
2022
|
+
return triggers.registerSource(misakanetTrigger(lastSession));
|
|
2023
|
+
});
|
|
2024
|
+
});
|
|
2025
|
+
|
|
1017
2026
|
// The right sidebar ships from 0.1.5 on, so this rides a deferred service inject: on a line
|
|
1018
2027
|
// without the registry the callback never runs, the fiber never pends, and the conversation tab
|
|
1019
2028
|
// above is the whole plugin. Inside, the registry is re-proved and every disposer is collected.
|
|
@@ -1041,7 +2050,7 @@ window.__ModuleLoader__.load({
|
|
|
1041
2050
|
id: "panel",
|
|
1042
2051
|
order: 30,
|
|
1043
2052
|
title: function () { return "MisakaNet"; },
|
|
1044
|
-
description: function () { return "
|
|
2053
|
+
description: function () { return T("guide.description"); },
|
|
1045
2054
|
icon: MisakanetGlyph,
|
|
1046
2055
|
}],
|
|
1047
2056
|
}));
|