@loicngr/kobo 1.11.8 → 1.11.10
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/AGENTS.md +2 -1
- package/CHANGELOG.md +9 -0
- package/README.md +2 -2
- package/dist/server/db/migrations.js +155 -19
- package/dist/server/db/schema.js +93 -0
- package/dist/server/index.js +35 -26
- package/dist/server/routes/dev-server.js +10 -8
- package/dist/server/routes/documents.js +33 -3
- package/dist/server/routes/health.js +16 -15
- package/dist/server/routes/images.js +17 -2
- package/dist/server/routes/workspaces.js +64 -49
- package/dist/server/services/agent/engines/codex/engine.js +5 -1
- package/dist/server/services/agent/orchestrator.js +8 -1
- package/dist/server/services/auto-loop-service.js +1 -1
- package/dist/server/services/change-source-branch-service.js +43 -3
- package/dist/server/services/dev-server-service.js +55 -41
- package/dist/server/services/forge/bitbucket/provider.js +6 -11
- package/dist/server/services/forge/github/provider.js +10 -9
- package/dist/server/services/forge/gitlab/provider.js +11 -13
- package/dist/server/services/git-stats-service.js +8 -6
- package/dist/server/services/image-service.js +34 -5
- package/dist/server/services/notion-service.js +45 -5
- package/dist/server/services/pr-watcher-service.js +20 -9
- package/dist/server/services/search-service.js +53 -40
- package/dist/server/services/websocket-service.js +8 -3
- package/dist/server/services/workspace-service.js +81 -10
- package/dist/server/services/worktree-purge-service.js +1 -1
- package/dist/server/utils/git-ops.js +89 -22
- package/dist/server/utils/mcp-client.js +4 -0
- package/dist/server/utils/safe-path.js +14 -0
- package/dist/server/utils/script-runner.js +16 -1
- package/package.json +22 -22
- package/src/client/dist/pwa/assets/{ActivityFeed-D9EfbRqv.css → ActivityFeed-BrjeLlhi.css} +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-UEAexGjc.js +8 -0
- package/src/client/dist/pwa/assets/ChangelogPage-XSYwu9XA.js +1 -0
- package/src/client/dist/pwa/assets/{ClosePopup-_rbZbpfb.js → ClosePopup-dpcrW9DC.js} +1 -1
- package/src/client/dist/pwa/assets/CreatePage-BK__rjvR.js +2 -0
- package/src/client/dist/pwa/assets/CreatePage-QwtfYunr.css +1 -0
- package/src/client/dist/pwa/assets/{DiffViewer-CRrDdYBa.css → DiffViewer-CS3P9Ngi.css} +1 -1
- package/src/client/dist/pwa/assets/DiffViewer-DnFN6Sc_.js +8 -0
- package/src/client/dist/pwa/assets/HealthPage-DVrZfMI6.js +1 -0
- package/src/client/dist/pwa/assets/MainLayout-D_3nFaCG.js +19 -0
- package/src/client/dist/pwa/assets/MainLayout-u-AM87m3.css +1 -0
- package/src/client/dist/pwa/assets/QBadge-sMJZ_eZm.js +1 -0
- package/src/client/dist/pwa/assets/QBanner-DedhGMcj.js +1 -0
- package/src/client/dist/pwa/assets/QBtnToggle-BgfN19l8.js +1 -0
- package/src/client/dist/pwa/assets/QChip-EQGYShN0.js +1 -0
- package/src/client/dist/pwa/assets/QIcon--SvgE-nS.js +1 -0
- package/src/client/dist/pwa/assets/QList-Be_FbTA3.js +1 -0
- package/src/client/dist/pwa/assets/QPage-DuIsRmYu.js +1 -0
- package/src/client/dist/pwa/assets/QSelect-CtxxliRU.js +36 -0
- package/src/client/dist/pwa/assets/QSeparator-BdyRE-AM.js +1 -0
- package/src/client/dist/pwa/assets/QSpace-D9UEvM99.js +1 -0
- package/src/client/dist/pwa/assets/QSpinnerDots-B4RmFJUj.js +1 -0
- package/src/client/dist/pwa/assets/QTooltip-HEIFh5w9.js +1 -0
- package/src/client/dist/pwa/assets/SearchPage-1B5HOf90.js +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-BXiWr2ba.css +1 -0
- package/src/client/dist/pwa/assets/SettingsPage-v8f4rrCt.js +16 -0
- package/src/client/dist/pwa/assets/TouchPan-lynWXY69.js +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-ChnyJ3xI.css +1 -0
- package/src/client/dist/pwa/assets/WorkspacePage-Vjzi8YnW.js +4 -0
- package/src/client/dist/pwa/assets/codicon-Brq4_Ui5.ttf +0 -0
- package/src/client/dist/pwa/assets/{css.worker-CBlhdqTa.js → css.worker-JKZQyld4.js} +38 -34
- package/src/client/dist/pwa/assets/cssMode-cg8iVI-i.js +1 -0
- package/src/client/dist/pwa/assets/documents-Dlx8yiSO.js +1 -0
- package/src/client/dist/pwa/assets/editor-vZnmUXVO.css +1 -0
- package/src/client/dist/pwa/assets/editor.api-CfaJRkrX.js +847 -0
- package/src/client/dist/pwa/assets/editor.worker-D-m8IBMa.js +30 -0
- package/src/client/dist/pwa/assets/expand-template-D3LZldj1.js +1 -0
- package/src/client/dist/pwa/assets/formatters-D0dU6VcA.js +1 -0
- package/src/client/dist/pwa/assets/{freemarker2-DczqHd5F.js → freemarker2-B_8OIClK.js} +1 -1
- package/src/client/dist/pwa/assets/handlebars-CaYi_uxU.js +1 -0
- package/src/client/dist/pwa/assets/html-BnCUDUl1.js +1 -0
- package/src/client/dist/pwa/assets/html.worker-BUr90sI4.js +506 -0
- package/src/client/dist/pwa/assets/htmlMode-BZjKGnQF.js +1 -0
- package/src/client/dist/pwa/assets/i18n-B6SDs7Lm.js +1 -0
- package/src/client/dist/pwa/assets/index-CK1nk7qr.css +1 -0
- package/src/client/dist/pwa/assets/index-CoOsPyyG.js +82 -0
- package/src/client/dist/pwa/assets/{javascript-Bs83z_W4.js → javascript-DkSfoeHU.js} +1 -1
- package/src/client/dist/pwa/assets/json.worker-BOODjG9U.js +62 -0
- package/src/client/dist/pwa/assets/{jsonMode-Bc-8bzJi.js → jsonMode-yKXxfx1V.js} +3 -3
- package/src/client/dist/pwa/assets/{kobo-commands-B2kM0INN.js → kobo-commands-vn1IQxLn.js} +1 -1
- package/src/client/dist/pwa/assets/layout-CiQpugQB.js +1 -0
- package/src/client/dist/pwa/assets/liquid-Dlhysv3U.js +1 -0
- package/src/client/dist/pwa/assets/lspLanguageFeatures-CeYSQFUz.js +4 -0
- package/src/client/dist/pwa/assets/mdx-B8iME5NA.js +1 -0
- package/src/client/dist/pwa/assets/network-auth-j2Drut6q.js +1 -0
- package/src/client/dist/pwa/assets/pending-diff-open-B4RYz7G_.js +1 -0
- package/src/client/dist/pwa/assets/permissionModes-6jhegAm-.js +1 -0
- package/src/client/dist/pwa/assets/{project-color-DIFZ95_g.js → project-color-e3OdFi9X.js} +1 -1
- package/src/client/dist/pwa/assets/python-CbFbC0ZR.js +1 -0
- package/src/client/dist/pwa/assets/razor-7c2NCdFI.js +1 -0
- package/src/client/dist/pwa/assets/reactivity.esm-bundler-DtSZKRQB.js +1 -0
- package/src/client/dist/pwa/assets/render-chat-markdown-Fer8kY38.js +67 -0
- package/src/client/dist/pwa/assets/runtime-dom.esm-bundler-DgevF6KV.js +4 -0
- package/src/client/dist/pwa/assets/{ts.worker-PmaSgaZk.js → ts.worker-C-oGa8JT.js} +199 -195
- package/src/client/dist/pwa/assets/{tsMode-DOsxzgZi.js → tsMode-BmShKjbx.js} +1 -1
- package/src/client/dist/pwa/assets/typescript-Dhu7Upbs.js +1 -0
- package/src/client/dist/pwa/assets/use-checkbox-SrkgiJho.js +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-CTZuBZOi.css +1 -0
- package/src/client/dist/pwa/assets/use-onboarding-fe4sPCNf.js +2 -0
- package/src/client/dist/pwa/assets/use-quasar-CeHlTrGg.js +1 -0
- package/src/client/dist/pwa/assets/vs-B-xsceNQ.js +56 -0
- package/src/client/dist/pwa/assets/vs-JJSFJJVJ.css +1 -0
- package/src/client/dist/pwa/assets/vue-i18n-C3W5i7h6.js +3 -0
- package/src/client/dist/pwa/assets/workers-zVMTXVoC.js +1 -0
- package/src/client/dist/pwa/assets/xml-D64wpCtR.js +1 -0
- package/src/client/dist/pwa/assets/yaml-CLSRKwfx.js +1 -0
- package/src/client/dist/pwa/index.html +8 -8
- package/src/client/dist/pwa/manifest.json +1 -1
- package/src/client/dist/pwa/sw.js +1 -1
- package/src/client/dist/pwa/assets/ActivityFeed-BkkyAw81.js +0 -8
- package/src/client/dist/pwa/assets/ChangelogPage-BlvhBx6o.js +0 -1
- package/src/client/dist/pwa/assets/CreatePage-B_JcmRfU.css +0 -1
- package/src/client/dist/pwa/assets/CreatePage-RSyoQwD7.js +0 -2
- package/src/client/dist/pwa/assets/DiffViewer-BSNxNex2.js +0 -8
- package/src/client/dist/pwa/assets/HealthPage-CUD1kipV.js +0 -1
- package/src/client/dist/pwa/assets/MainLayout-BCLgsn0C.js +0 -37
- package/src/client/dist/pwa/assets/MainLayout-DlS2wxkK.css +0 -1
- package/src/client/dist/pwa/assets/QBadge-DV5kn4mL.js +0 -1
- package/src/client/dist/pwa/assets/QBanner-CmfaeizG.js +0 -1
- package/src/client/dist/pwa/assets/QBtnGroup-C442-yBb.js +0 -1
- package/src/client/dist/pwa/assets/QBtnToggle-xg3LYW7D.js +0 -1
- package/src/client/dist/pwa/assets/QChip-DhUfnmbz.js +0 -1
- package/src/client/dist/pwa/assets/QIcon-C6E-oEjI.js +0 -1
- package/src/client/dist/pwa/assets/QInput-BF8x4RTv.js +0 -1
- package/src/client/dist/pwa/assets/QList-D5EKdFPn.js +0 -1
- package/src/client/dist/pwa/assets/QPage-D3rjwA1l.js +0 -1
- package/src/client/dist/pwa/assets/QSelect-BhyT0k2N.js +0 -36
- package/src/client/dist/pwa/assets/QSeparator-DWTCQxsa.js +0 -1
- package/src/client/dist/pwa/assets/QSpace-ezNrZkuj.js +0 -1
- package/src/client/dist/pwa/assets/QSpinnerDots-DIV68-cV.js +0 -1
- package/src/client/dist/pwa/assets/QTooltip-ybbFi3KK.js +0 -1
- package/src/client/dist/pwa/assets/SearchPage-Bb7TG3Vo.js +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-C2qe4xKu.css +0 -1
- package/src/client/dist/pwa/assets/SettingsPage-CjPnnn60.js +0 -16
- package/src/client/dist/pwa/assets/ThinkingItem-B1RKyCIS.js +0 -1
- package/src/client/dist/pwa/assets/ThinkingItem-Ckk4K1O-.css +0 -1
- package/src/client/dist/pwa/assets/TouchPan-DzZPYIol.js +0 -1
- package/src/client/dist/pwa/assets/WorkspacePage-6Rgjarvf.js +0 -4
- package/src/client/dist/pwa/assets/WorkspacePage-Bgxg_Rua.css +0 -1
- package/src/client/dist/pwa/assets/build-path-tree-KRACVkD3.js +0 -1
- package/src/client/dist/pwa/assets/chunk-QTnfLwEv.js +0 -1
- package/src/client/dist/pwa/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/src/client/dist/pwa/assets/cssMode-CQsB6mFO.js +0 -1
- package/src/client/dist/pwa/assets/documents-C7efBF5t.js +0 -1
- package/src/client/dist/pwa/assets/editor-BFebQ78e.css +0 -1
- package/src/client/dist/pwa/assets/editor.api2-C58pkxmw.js +0 -872
- package/src/client/dist/pwa/assets/editor.main-BhLqjdq4.js +0 -6
- package/src/client/dist/pwa/assets/editor.worker-Cu3tR8iJ.js +0 -26
- package/src/client/dist/pwa/assets/expand-template-BA-hhG5v.js +0 -1
- package/src/client/dist/pwa/assets/formatters-DugWNOEs.js +0 -1
- package/src/client/dist/pwa/assets/handlebars-DmA7twi8.js +0 -1
- package/src/client/dist/pwa/assets/html-DtZDrViU.js +0 -1
- package/src/client/dist/pwa/assets/html.worker-BKBrNna1.js +0 -502
- package/src/client/dist/pwa/assets/htmlMode-DO9_qUdT.js +0 -1
- package/src/client/dist/pwa/assets/i18n-CipNqHTm.js +0 -1
- package/src/client/dist/pwa/assets/index-BxspWSDs.js +0 -84
- package/src/client/dist/pwa/assets/index-CwYR3iHt.css +0 -1
- package/src/client/dist/pwa/assets/json.worker-BaLYt9Ss.js +0 -58
- package/src/client/dist/pwa/assets/layout-C6frIxKZ.js +0 -1
- package/src/client/dist/pwa/assets/liquid-Dnxe0NUB.js +0 -1
- package/src/client/dist/pwa/assets/lspLanguageFeatures-qDBRYovw.js +0 -4
- package/src/client/dist/pwa/assets/mdx-C1oke9bq.js +0 -1
- package/src/client/dist/pwa/assets/monaco.contribution-BbFzrpO3.js +0 -2
- package/src/client/dist/pwa/assets/network-auth-Bj1LS5R7.js +0 -1
- package/src/client/dist/pwa/assets/pending-diff-open-C3voSObg.js +0 -1
- package/src/client/dist/pwa/assets/permissionModes-DMuXGtZu.js +0 -1
- package/src/client/dist/pwa/assets/python-Dgq-bJSM.js +0 -1
- package/src/client/dist/pwa/assets/razor-Bms4CQGF.js +0 -1
- package/src/client/dist/pwa/assets/render-chat-markdown-BRSTe4je.js +0 -66
- package/src/client/dist/pwa/assets/typescript-CzOp_GqX.js +0 -1
- package/src/client/dist/pwa/assets/use-checkbox-E9SV7iH5.js +0 -1
- package/src/client/dist/pwa/assets/use-onboarding-B3l7mx48.css +0 -1
- package/src/client/dist/pwa/assets/use-onboarding-BGy3sOAN.js +0 -2
- package/src/client/dist/pwa/assets/use-quasar-DsmzxifG.js +0 -1
- package/src/client/dist/pwa/assets/vue.runtime.esm-bundler-JZnIeD9D.js +0 -3
- package/src/client/dist/pwa/assets/workers-0fozwhvS.js +0 -1
- package/src/client/dist/pwa/assets/xml-CPa01uHm.js +0 -1
- package/src/client/dist/pwa/assets/yaml-Cw3fzSuh.js +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getDb } from '../db/index.js';
|
|
2
2
|
/** Event types that carry user-authored or assistant-authored readable text. */
|
|
3
|
-
const SEARCHABLE_TYPES = ['user:message', 'agent:output'];
|
|
3
|
+
const SEARCHABLE_TYPES = ['user:message', 'agent:event', 'agent:output'];
|
|
4
4
|
const SNIPPET_CONTEXT = 100; // chars on each side of the match
|
|
5
5
|
/**
|
|
6
6
|
* Extract the readable text content of a ws_events payload, or `null` when
|
|
@@ -29,6 +29,9 @@ function extractReadableText(type, payload) {
|
|
|
29
29
|
return parts.length > 0 ? parts.join('\n') : null;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
if (type === 'agent:event' && p.kind === 'message:text') {
|
|
33
|
+
return typeof p.text === 'string' ? p.text : null;
|
|
34
|
+
}
|
|
32
35
|
return null;
|
|
33
36
|
}
|
|
34
37
|
function buildSnippet(text, matchIndex, query) {
|
|
@@ -56,49 +59,59 @@ export function searchEvents(query, options = {}) {
|
|
|
56
59
|
const db = getDb();
|
|
57
60
|
const typePlaceholders = SEARCHABLE_TYPES.map(() => '?').join(', ');
|
|
58
61
|
const archiveFilter = includeArchived ? '' : 'AND w.archived_at IS NULL';
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
// realistic datasets, without scanning the entire table.
|
|
62
|
-
const dbLimit = Math.max(limit * 3, 100);
|
|
63
|
-
const rows = db
|
|
64
|
-
.prepare(`SELECT e.id, e.workspace_id, e.session_id, e.type, e.created_at, e.payload,
|
|
65
|
-
w.name AS workspace_name, w.archived_at
|
|
66
|
-
FROM ws_events e
|
|
67
|
-
JOIN workspaces w ON e.workspace_id = w.id
|
|
68
|
-
WHERE e.type IN (${typePlaceholders})
|
|
69
|
-
AND e.payload LIKE ?
|
|
70
|
-
${archiveFilter}
|
|
71
|
-
ORDER BY e.created_at DESC
|
|
72
|
-
LIMIT ?`)
|
|
73
|
-
.all(...SEARCHABLE_TYPES, likePattern, dbLimit);
|
|
62
|
+
const escapedQuery = trimmed.replace(/[\\%_]/g, '\\$&');
|
|
63
|
+
const likePattern = `%${escapedQuery}%`;
|
|
74
64
|
const needle = trimmed.toLowerCase();
|
|
75
65
|
const results = [];
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
const batchSize = Math.min(Math.max(limit * 3, 100), 500);
|
|
67
|
+
const statement = db.prepare(`SELECT e.id, e.workspace_id, e.session_id, e.type, e.created_at, e.payload,
|
|
68
|
+
w.name AS workspace_name, w.archived_at
|
|
69
|
+
FROM ws_events e
|
|
70
|
+
JOIN workspaces w ON e.workspace_id = w.id
|
|
71
|
+
WHERE e.type IN (${typePlaceholders})
|
|
72
|
+
AND e.payload LIKE ? ESCAPE '\\'
|
|
73
|
+
${archiveFilter}
|
|
74
|
+
AND (? IS NULL OR e.created_at < ? OR (e.created_at = ? AND e.id < ?))
|
|
75
|
+
ORDER BY e.created_at DESC, e.id DESC
|
|
76
|
+
LIMIT ?`);
|
|
77
|
+
let cursorCreatedAt = null;
|
|
78
|
+
let cursorId = null;
|
|
79
|
+
while (results.length < limit) {
|
|
80
|
+
const rows = statement.all(...SEARCHABLE_TYPES, likePattern, cursorCreatedAt, cursorCreatedAt, cursorCreatedAt, cursorId, batchSize);
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(row.payload);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const text = extractReadableText(row.type, parsed);
|
|
90
|
+
if (!text)
|
|
91
|
+
continue;
|
|
92
|
+
const idx = text.toLowerCase().indexOf(needle);
|
|
93
|
+
if (idx < 0)
|
|
94
|
+
continue;
|
|
95
|
+
results.push({
|
|
96
|
+
eventId: row.id,
|
|
97
|
+
sessionId: row.session_id,
|
|
98
|
+
workspaceId: row.workspace_id,
|
|
99
|
+
workspaceName: row.workspace_name,
|
|
100
|
+
archived: row.archived_at !== null,
|
|
101
|
+
type: row.type,
|
|
102
|
+
timestamp: row.created_at,
|
|
103
|
+
snippet: buildSnippet(text, idx, trimmed),
|
|
104
|
+
});
|
|
105
|
+
if (results.length >= limit)
|
|
106
|
+
break;
|
|
83
107
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (idx < 0)
|
|
89
|
-
continue;
|
|
90
|
-
results.push({
|
|
91
|
-
eventId: row.id,
|
|
92
|
-
sessionId: row.session_id,
|
|
93
|
-
workspaceId: row.workspace_id,
|
|
94
|
-
workspaceName: row.workspace_name,
|
|
95
|
-
archived: row.archived_at !== null,
|
|
96
|
-
type: row.type,
|
|
97
|
-
timestamp: row.created_at,
|
|
98
|
-
snippet: buildSnippet(text, idx, trimmed),
|
|
99
|
-
});
|
|
100
|
-
if (results.length >= limit)
|
|
108
|
+
if (rows.length < batchSize)
|
|
109
|
+
break;
|
|
110
|
+
const last = rows.at(-1);
|
|
111
|
+
if (!last)
|
|
101
112
|
break;
|
|
113
|
+
cursorCreatedAt = last.created_at;
|
|
114
|
+
cursorId = last.id;
|
|
102
115
|
}
|
|
103
116
|
return results;
|
|
104
117
|
}
|
|
@@ -109,16 +109,18 @@ export function handleConnection(ws) {
|
|
|
109
109
|
export function emit(workspaceId, type, payload, sessionId) {
|
|
110
110
|
const id = nanoid();
|
|
111
111
|
const createdAt = new Date().toISOString();
|
|
112
|
+
let replayable = false;
|
|
112
113
|
// Best-effort persist — don't let FK violation (deleted workspace) break the broadcast
|
|
113
114
|
try {
|
|
114
115
|
const db = getDb();
|
|
115
116
|
db.prepare('INSERT INTO ws_events (id, workspace_id, type, payload, session_id, created_at) VALUES (?, ?, ?, ?, ?, ?)').run(id, workspaceId, type, JSON.stringify(payload), sessionId ?? null, createdAt);
|
|
117
|
+
replayable = true;
|
|
116
118
|
}
|
|
117
119
|
catch (err) {
|
|
118
120
|
console.error(`[websocket-service] Failed to persist event (workspace=${workspaceId}, type=${type}):`, err);
|
|
119
121
|
}
|
|
120
122
|
// Build the event object to send
|
|
121
|
-
const event = { id, workspaceId, type, payload, sessionId, createdAt };
|
|
123
|
+
const event = { id, workspaceId, type, payload, sessionId, createdAt, replayable };
|
|
122
124
|
const message = JSON.stringify(event);
|
|
123
125
|
// Broadcast to subscribed clients. Wrap `.send` in try/catch so a dropped
|
|
124
126
|
// client doesn't throw and abort delivery to the remaining subscribers.
|
|
@@ -145,7 +147,7 @@ export function emit(workspaceId, type, payload, sessionId) {
|
|
|
145
147
|
export function emitEphemeral(workspaceId, type, payload) {
|
|
146
148
|
const id = nanoid();
|
|
147
149
|
const createdAt = new Date().toISOString();
|
|
148
|
-
const event = { id, workspaceId, type, payload, createdAt };
|
|
150
|
+
const event = { id, workspaceId, type, payload, createdAt, replayable: false };
|
|
149
151
|
const message = JSON.stringify(event);
|
|
150
152
|
let sendErrorLogged = false;
|
|
151
153
|
for (const [ws, subs] of clients) {
|
|
@@ -188,12 +190,14 @@ export function handleSyncRequest(ws, lastEventId, workspaceIds) {
|
|
|
188
190
|
// up. This keeps first-paint fast on long-lived workspaces with tens of
|
|
189
191
|
// thousands of events without ever deleting anything from the DB.
|
|
190
192
|
const INITIAL_WINDOW = 300;
|
|
193
|
+
let mode = 'snapshot';
|
|
191
194
|
if (lastEventId) {
|
|
192
195
|
// Resume path: replay every event strictly after the cursor (delta
|
|
193
196
|
// since last seen). If the cursor is stale/unknown, fall back to the
|
|
194
197
|
// recent window rather than streaming the entire history.
|
|
195
198
|
const lastRow = db.prepare('SELECT rowid FROM ws_events WHERE id = ?').get(lastEventId);
|
|
196
199
|
if (lastRow) {
|
|
200
|
+
mode = 'delta';
|
|
197
201
|
rows = db
|
|
198
202
|
.prepare(`SELECT * FROM ws_events WHERE workspace_id IN (${placeholders}) AND rowid > ? ORDER BY rowid ASC`)
|
|
199
203
|
.all(...resolvedIds, lastRow.rowid);
|
|
@@ -232,9 +236,10 @@ export function handleSyncRequest(ws, lastEventId, workspaceIds) {
|
|
|
232
236
|
payload: parsedPayload,
|
|
233
237
|
sessionId: row.session_id ?? undefined,
|
|
234
238
|
createdAt: row.created_at,
|
|
239
|
+
replayable: true,
|
|
235
240
|
};
|
|
236
241
|
});
|
|
237
|
-
ws.send(JSON.stringify({ type: 'sync:response', payload: { events } }));
|
|
242
|
+
ws.send(JSON.stringify({ type: 'sync:response', payload: { events, mode } }));
|
|
238
243
|
}
|
|
239
244
|
// ── Utilities ──────────────────────────────────────────────────────────────────
|
|
240
245
|
/**
|
|
@@ -165,20 +165,29 @@ export function updateWorkspaceStatus(id, status) {
|
|
|
165
165
|
return getWorkspace(id);
|
|
166
166
|
}
|
|
167
167
|
const WORKSPACE_NAME_MAX_LENGTH = 200;
|
|
168
|
-
|
|
169
|
-
export function updateWorkspaceName(id, name) {
|
|
170
|
-
// Strip control characters (incl. newlines) and collapse whitespace
|
|
168
|
+
function sanitizeWorkspaceName(name) {
|
|
171
169
|
const noControl = Array.from(name, (c) => {
|
|
172
170
|
const code = c.charCodeAt(0);
|
|
173
171
|
return code < 32 || code === 127 ? ' ' : c;
|
|
174
172
|
}).join('');
|
|
175
173
|
const sanitized = noControl.replace(/\s+/g, ' ').trim();
|
|
176
|
-
if (!sanitized)
|
|
174
|
+
if (!sanitized)
|
|
177
175
|
throw new Error('Workspace name cannot be empty');
|
|
178
|
-
}
|
|
179
176
|
if (sanitized.length > WORKSPACE_NAME_MAX_LENGTH) {
|
|
180
177
|
throw new Error(`Workspace name cannot exceed ${WORKSPACE_NAME_MAX_LENGTH} characters`);
|
|
181
178
|
}
|
|
179
|
+
return sanitized;
|
|
180
|
+
}
|
|
181
|
+
function normalizeWorkspaceDescription(description) {
|
|
182
|
+
const trimmed = description == null ? null : description.trim();
|
|
183
|
+
if (trimmed !== null && trimmed.length > 200) {
|
|
184
|
+
throw new Error(`Description must be 200 characters or fewer (got ${trimmed.length})`);
|
|
185
|
+
}
|
|
186
|
+
return trimmed && trimmed.length > 0 ? trimmed : null;
|
|
187
|
+
}
|
|
188
|
+
/** Update a workspace's display name. */
|
|
189
|
+
export function updateWorkspaceName(id, name) {
|
|
190
|
+
const sanitized = sanitizeWorkspaceName(name);
|
|
182
191
|
const db = getDb();
|
|
183
192
|
const now = new Date().toISOString();
|
|
184
193
|
const result = db.prepare('UPDATE workspaces SET name = ?, updated_at = ? WHERE id = ?').run(sanitized, now, id);
|
|
@@ -311,11 +320,7 @@ export function updateAgentPermissionMode(id, mode) {
|
|
|
311
320
|
* does not exist.
|
|
312
321
|
*/
|
|
313
322
|
export function updateWorkspaceDescription(id, description) {
|
|
314
|
-
const
|
|
315
|
-
if (trimmed !== null && trimmed.length > 200) {
|
|
316
|
-
throw new Error(`Description must be 200 characters or fewer (got ${trimmed.length})`);
|
|
317
|
-
}
|
|
318
|
-
const stored = trimmed && trimmed.length > 0 ? trimmed : null;
|
|
323
|
+
const stored = normalizeWorkspaceDescription(description);
|
|
319
324
|
const db = getDb();
|
|
320
325
|
const result = db
|
|
321
326
|
.prepare('UPDATE workspaces SET description = ?, updated_at = ? WHERE id = ?')
|
|
@@ -326,6 +331,72 @@ export function updateWorkspaceDescription(id, description) {
|
|
|
326
331
|
emitEphemeral(id, 'workspace:description-updated', { description: stored });
|
|
327
332
|
return getWorkspace(id);
|
|
328
333
|
}
|
|
334
|
+
/** Validate and persist a multi-field API update with one atomic SQL statement. */
|
|
335
|
+
export function updateWorkspaceFields(id, fields) {
|
|
336
|
+
const workspace = getWorkspace(id);
|
|
337
|
+
if (!workspace)
|
|
338
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
339
|
+
const assignments = [];
|
|
340
|
+
const values = [];
|
|
341
|
+
if (fields.model !== undefined) {
|
|
342
|
+
if (typeof fields.model !== 'string' || !fields.model.trim())
|
|
343
|
+
throw new Error('model must be a non-empty string');
|
|
344
|
+
assignments.push('model = ?');
|
|
345
|
+
values.push(fields.model.trim());
|
|
346
|
+
}
|
|
347
|
+
if (fields.reasoningEffort !== undefined) {
|
|
348
|
+
if (typeof fields.reasoningEffort !== 'string' || !fields.reasoningEffort.trim()) {
|
|
349
|
+
throw new Error('reasoningEffort must be a non-empty string');
|
|
350
|
+
}
|
|
351
|
+
assignments.push('reasoning_effort = ?');
|
|
352
|
+
values.push(fields.reasoningEffort.trim());
|
|
353
|
+
}
|
|
354
|
+
if (fields.agentPermissionMode !== undefined) {
|
|
355
|
+
if (!['plan', 'bypass', 'strict', 'interactive'].includes(fields.agentPermissionMode)) {
|
|
356
|
+
throw new Error('Invalid agentPermissionMode');
|
|
357
|
+
}
|
|
358
|
+
const legacyMode = fields.agentPermissionMode === 'plan' ? 'plan' : 'auto-accept';
|
|
359
|
+
const legacyProfile = fields.agentPermissionMode === 'plan' ? 'bypass' : fields.agentPermissionMode;
|
|
360
|
+
assignments.push('agent_permission_mode = ?', 'permission_mode = ?', 'permission_profile = ?');
|
|
361
|
+
values.push(fields.agentPermissionMode, legacyMode, legacyProfile);
|
|
362
|
+
}
|
|
363
|
+
if (fields.status !== undefined) {
|
|
364
|
+
const allowed = VALID_TRANSITIONS[workspace.status];
|
|
365
|
+
if (!allowed || (fields.status !== workspace.status && !allowed.includes(fields.status))) {
|
|
366
|
+
throw new Error(`Invalid status transition from '${workspace.status}' to '${fields.status}'. Allowed: ${allowed?.join(', ') ?? ''}`);
|
|
367
|
+
}
|
|
368
|
+
assignments.push('status = ?');
|
|
369
|
+
values.push(fields.status);
|
|
370
|
+
}
|
|
371
|
+
if (fields.name !== undefined) {
|
|
372
|
+
if (typeof fields.name !== 'string')
|
|
373
|
+
throw new Error('name must be a string');
|
|
374
|
+
assignments.push('name = ?');
|
|
375
|
+
values.push(sanitizeWorkspaceName(fields.name));
|
|
376
|
+
}
|
|
377
|
+
const hasDescription = Object.hasOwn(fields, 'description');
|
|
378
|
+
let storedDescription;
|
|
379
|
+
if (hasDescription) {
|
|
380
|
+
if (fields.description !== null && typeof fields.description !== 'string') {
|
|
381
|
+
throw new Error('description must be a string or null');
|
|
382
|
+
}
|
|
383
|
+
storedDescription = normalizeWorkspaceDescription(fields.description ?? null);
|
|
384
|
+
assignments.push('description = ?');
|
|
385
|
+
values.push(storedDescription);
|
|
386
|
+
}
|
|
387
|
+
if (assignments.length === 0)
|
|
388
|
+
return workspace;
|
|
389
|
+
assignments.push('updated_at = ?');
|
|
390
|
+
values.push(new Date().toISOString(), id);
|
|
391
|
+
const result = getDb()
|
|
392
|
+
.prepare(`UPDATE workspaces SET ${assignments.join(', ')} WHERE id = ?`)
|
|
393
|
+
.run(...values);
|
|
394
|
+
if (result.changes === 0)
|
|
395
|
+
throw new Error(`Workspace '${id}' not found`);
|
|
396
|
+
if (hasDescription)
|
|
397
|
+
emitEphemeral(id, 'workspace:description-updated', { description: storedDescription ?? null });
|
|
398
|
+
return getWorkspace(id);
|
|
399
|
+
}
|
|
329
400
|
/**
|
|
330
401
|
* Update a workspace's agent-side description (≤ 200 chars after trim).
|
|
331
402
|
* Empty string (after trim) or `null` clears the column.
|
|
@@ -24,7 +24,7 @@ export async function purgeWorktree(workspaceId) {
|
|
|
24
24
|
console.error(`[purge] stopAgent failed for '${workspace.name}':`, msg);
|
|
25
25
|
}
|
|
26
26
|
try {
|
|
27
|
-
devServerService.stopDevServer(workspaceId);
|
|
27
|
+
await devServerService.stopDevServer(workspaceId);
|
|
28
28
|
}
|
|
29
29
|
catch (err) {
|
|
30
30
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -3,6 +3,7 @@ import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
const execFileAsync = promisify(execFileCb);
|
|
6
|
+
const READ_ONLY_GIT_TIMEOUT_MS = 15_000;
|
|
6
7
|
function git(repoPath, args) {
|
|
7
8
|
// `trimEnd` (not `trim`): some git outputs are column-aligned and the LEADING
|
|
8
9
|
// space carries information. The classic case is `git status --porcelain`,
|
|
@@ -13,6 +14,15 @@ function git(repoPath, args) {
|
|
|
13
14
|
// always appends) still goes — that's what every caller expects.
|
|
14
15
|
return execFileSync('git', args, { cwd: repoPath, encoding: 'utf-8' }).trimEnd();
|
|
15
16
|
}
|
|
17
|
+
async function gitAsync(repoPath, args, timeout = READ_ONLY_GIT_TIMEOUT_MS) {
|
|
18
|
+
const { stdout } = await execFileAsync('git', args, {
|
|
19
|
+
cwd: repoPath,
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
timeout,
|
|
22
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
23
|
+
});
|
|
24
|
+
return stdout.trimEnd();
|
|
25
|
+
}
|
|
16
26
|
/** Return the name of the currently checked-out branch. */
|
|
17
27
|
export function getCurrentBranch(repoPath) {
|
|
18
28
|
return git(repoPath, ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
@@ -899,31 +909,33 @@ export function writeFileInWorktree(absPath, content) {
|
|
|
899
909
|
/** Parse `git status --porcelain` into counts of staged, modified, and untracked files. */
|
|
900
910
|
export function getWorkingTreeStatus(repoPath) {
|
|
901
911
|
try {
|
|
902
|
-
|
|
903
|
-
let staged = 0;
|
|
904
|
-
let modified = 0;
|
|
905
|
-
let untracked = 0;
|
|
906
|
-
for (const line of output.split('\n')) {
|
|
907
|
-
if (!line)
|
|
908
|
-
continue;
|
|
909
|
-
const x = line[0]; // index status
|
|
910
|
-
const y = line[1]; // worktree status
|
|
911
|
-
if (x === '?' && y === '?') {
|
|
912
|
-
untracked++;
|
|
913
|
-
}
|
|
914
|
-
else {
|
|
915
|
-
if (x !== ' ' && x !== '?')
|
|
916
|
-
staged++;
|
|
917
|
-
if (y !== ' ' && y !== '?')
|
|
918
|
-
modified++;
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
return { staged, modified, untracked };
|
|
912
|
+
return parseWorkingTreeStatus(git(repoPath, ['status', '--porcelain']));
|
|
922
913
|
}
|
|
923
914
|
catch {
|
|
924
915
|
return { staged: 0, modified: 0, untracked: 0 };
|
|
925
916
|
}
|
|
926
917
|
}
|
|
918
|
+
function parseWorkingTreeStatus(output) {
|
|
919
|
+
let staged = 0;
|
|
920
|
+
let modified = 0;
|
|
921
|
+
let untracked = 0;
|
|
922
|
+
for (const line of output.split('\n')) {
|
|
923
|
+
if (!line)
|
|
924
|
+
continue;
|
|
925
|
+
const x = line[0];
|
|
926
|
+
const y = line[1];
|
|
927
|
+
if (x === '?' && y === '?') {
|
|
928
|
+
untracked++;
|
|
929
|
+
}
|
|
930
|
+
else {
|
|
931
|
+
if (x !== ' ' && x !== '?')
|
|
932
|
+
staged++;
|
|
933
|
+
if (y !== ' ' && y !== '?')
|
|
934
|
+
modified++;
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return { staged, modified, untracked };
|
|
938
|
+
}
|
|
927
939
|
/**
|
|
928
940
|
* List uncommitted working-tree files with their status, parsed from
|
|
929
941
|
* `git status --porcelain`. Same classification rule as getWorkingTreeStatus.
|
|
@@ -1052,6 +1064,57 @@ export function getWorkingTreeDiffStats(repoPath) {
|
|
|
1052
1064
|
}
|
|
1053
1065
|
// ── Async versions ───────────────────────────────────────────────────────────
|
|
1054
1066
|
// Non-blocking alternatives for hot paths (route handlers).
|
|
1067
|
+
async function resolveBaseAsync(repoPath, base) {
|
|
1068
|
+
try {
|
|
1069
|
+
await gitAsync(repoPath, ['rev-parse', '--verify', `origin/${base}`]);
|
|
1070
|
+
return `origin/${base}`;
|
|
1071
|
+
}
|
|
1072
|
+
catch {
|
|
1073
|
+
return base;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
/** Non-blocking commit count for request and polling hot paths. */
|
|
1077
|
+
export async function getCommitCountAsync(repoPath, base, head) {
|
|
1078
|
+
try {
|
|
1079
|
+
const ref = await resolveBaseAsync(repoPath, base);
|
|
1080
|
+
const output = await gitAsync(repoPath, ['rev-list', '--count', `${ref}..${head}`]);
|
|
1081
|
+
return parseInt(output, 10) || 0;
|
|
1082
|
+
}
|
|
1083
|
+
catch {
|
|
1084
|
+
return 0;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
/** Non-blocking behind count for request and polling hot paths. */
|
|
1088
|
+
export async function getCommitsBehindAsync(repoPath, base, head) {
|
|
1089
|
+
try {
|
|
1090
|
+
const ref = await resolveBaseAsync(repoPath, base);
|
|
1091
|
+
const output = await gitAsync(repoPath, ['rev-list', '--count', `${head}..${ref}`]);
|
|
1092
|
+
const count = parseInt(output, 10);
|
|
1093
|
+
return Number.isFinite(count) ? count : 0;
|
|
1094
|
+
}
|
|
1095
|
+
catch {
|
|
1096
|
+
return 0;
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
/** Non-blocking diff summary for request and polling hot paths. */
|
|
1100
|
+
export async function getStructuredDiffStatsBetweenAsync(repoPath, base, head) {
|
|
1101
|
+
try {
|
|
1102
|
+
const ref = await resolveBaseAsync(repoPath, base);
|
|
1103
|
+
return parseDiffShortstat(await gitAsync(repoPath, ['diff', '--shortstat', `${ref}...${head}`]));
|
|
1104
|
+
}
|
|
1105
|
+
catch {
|
|
1106
|
+
return { filesChanged: 0, insertions: 0, deletions: 0 };
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
/** Non-blocking working-tree summary for request and polling hot paths. */
|
|
1110
|
+
export async function getWorkingTreeStatusAsync(repoPath) {
|
|
1111
|
+
try {
|
|
1112
|
+
return parseWorkingTreeStatus(await gitAsync(repoPath, ['status', '--porcelain']));
|
|
1113
|
+
}
|
|
1114
|
+
catch {
|
|
1115
|
+
return { staged: 0, modified: 0, untracked: 0 };
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1055
1118
|
/**
|
|
1056
1119
|
* Async version of `getUnpushedCount`. Same `origin/<workingBranch>` semantic:
|
|
1057
1120
|
* returns `-1` when the remote ref does not exist (never pushed), `0` when
|
|
@@ -1060,7 +1123,10 @@ export function getWorkingTreeDiffStats(repoPath) {
|
|
|
1060
1123
|
export async function getUnpushedCountAsync(repoPath, workingBranch) {
|
|
1061
1124
|
const remoteRef = `origin/${workingBranch}`;
|
|
1062
1125
|
try {
|
|
1063
|
-
await execFileAsync('git', ['rev-parse', '--verify', remoteRef], {
|
|
1126
|
+
await execFileAsync('git', ['rev-parse', '--verify', remoteRef], {
|
|
1127
|
+
cwd: repoPath,
|
|
1128
|
+
timeout: READ_ONLY_GIT_TIMEOUT_MS,
|
|
1129
|
+
});
|
|
1064
1130
|
}
|
|
1065
1131
|
catch {
|
|
1066
1132
|
return -1; // branch never pushed (no remote ref)
|
|
@@ -1069,6 +1135,7 @@ export async function getUnpushedCountAsync(repoPath, workingBranch) {
|
|
|
1069
1135
|
const { stdout } = await execFileAsync('git', ['rev-list', `${remoteRef}..HEAD`, '--count'], {
|
|
1070
1136
|
cwd: repoPath,
|
|
1071
1137
|
encoding: 'utf-8',
|
|
1138
|
+
timeout: READ_ONLY_GIT_TIMEOUT_MS,
|
|
1072
1139
|
});
|
|
1073
1140
|
return parseInt(stdout.trim(), 10) || 0;
|
|
1074
1141
|
}
|
|
@@ -1086,7 +1153,7 @@ export async function getUnpushedCountAsync(repoPath, workingBranch) {
|
|
|
1086
1153
|
*/
|
|
1087
1154
|
export async function fetchSourceBranchAsync(repoPath, branch, remote = 'origin') {
|
|
1088
1155
|
try {
|
|
1089
|
-
await execFileAsync('git', ['fetch', remote, branch], { cwd: repoPath });
|
|
1156
|
+
await execFileAsync('git', ['fetch', remote, branch], { cwd: repoPath, timeout: 30_000 });
|
|
1090
1157
|
}
|
|
1091
1158
|
catch (err) {
|
|
1092
1159
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -58,6 +58,10 @@ export function spawnMcpProcess(command, args, env) {
|
|
|
58
58
|
const mcpProcess = spawn(command, args, {
|
|
59
59
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
60
60
|
env,
|
|
61
|
+
// Detached: `command` is frequently `npx`, which spawns its own `node`
|
|
62
|
+
// child to run the actual MCP server. Without this, `mcpProcess.kill()`
|
|
63
|
+
// only reaches the `npx` wrapper and can leave the real server running.
|
|
64
|
+
detached: true,
|
|
61
65
|
});
|
|
62
66
|
// Defensive default 'error' listener: a ChildProcess that emits 'error'
|
|
63
67
|
// without any listener crashes the whole Node process (Unhandled 'error').
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
export function isPathInside(rootPath, candidatePath) {
|
|
4
|
+
const relative = path.relative(rootPath, candidatePath);
|
|
5
|
+
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
6
|
+
}
|
|
7
|
+
/** Resolve two existing paths and reject candidates that escape through a symlinked parent. */
|
|
8
|
+
export function resolveExistingPathInside(rootPath, candidatePath) {
|
|
9
|
+
const realRoot = fs.realpathSync(rootPath);
|
|
10
|
+
const realCandidate = fs.realpathSync(candidatePath);
|
|
11
|
+
if (!isPathInside(realRoot, realCandidate))
|
|
12
|
+
throw new Error('Path escapes allowed root');
|
|
13
|
+
return realCandidate;
|
|
14
|
+
}
|
|
@@ -27,9 +27,24 @@ export function runScript(opts) {
|
|
|
27
27
|
PROJECT_PATH: env?.projectPath ?? '',
|
|
28
28
|
},
|
|
29
29
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
30
|
+
// Detached so this is the leader of its own process group: a script
|
|
31
|
+
// that backgrounds a child (`npm run dev &`) would otherwise leave
|
|
32
|
+
// that child running after a timeout kill, since SIGKILL on `proc`
|
|
33
|
+
// alone only reaches the `bash` process itself.
|
|
34
|
+
detached: true,
|
|
30
35
|
});
|
|
31
36
|
const timeout = setTimeout(() => {
|
|
32
|
-
|
|
37
|
+
// Kill the whole process group (-pid), not just `proc` — otherwise a
|
|
38
|
+
// backgrounded child of the script survives the timeout as an orphan.
|
|
39
|
+
try {
|
|
40
|
+
if (proc.pid)
|
|
41
|
+
process.kill(-proc.pid, 'SIGKILL');
|
|
42
|
+
else
|
|
43
|
+
proc.kill('SIGKILL');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
proc.kill('SIGKILL');
|
|
47
|
+
}
|
|
33
48
|
// Destroy pipes so the 'close' event fires immediately even if
|
|
34
49
|
// child processes (e.g. sleep) inherited the file descriptors.
|
|
35
50
|
proc.stdout?.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loicngr/kobo",
|
|
3
|
-
"version": "1.11.
|
|
3
|
+
"version": "1.11.10",
|
|
4
4
|
"description": "Kōbō — multi-workspace agent manager for Claude Code. Orchestrates isolated git worktrees with dev servers, Notion integration, and MCP tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "GPL-3.0-or-later",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"quasar"
|
|
46
46
|
],
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": ">=24.15.0"
|
|
49
49
|
},
|
|
50
50
|
"scripts": {
|
|
51
51
|
"dev": "KOBO_ENFORCE_LOCAL_HOME=1 KOBO_HOME=./data PORT=3300 tsx watch src/server/index.ts",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"build:client": "cd src/client && npx quasar build -m pwa",
|
|
55
55
|
"build:server": "npx tsc && chmod +x dist/server/index.js",
|
|
56
56
|
"build": "npm run build:client && npm run build:server",
|
|
57
|
-
"install-all": "npm install && cd src/client && npm install",
|
|
57
|
+
"install-all": "npm install && cd src/client && npm install && cd src-pwa && npm install",
|
|
58
58
|
"start": "node dist/server/index.js",
|
|
59
59
|
"test": "vitest run --config vitest.config.ts --root .",
|
|
60
60
|
"test:watch": "vitest --config vitest.config.ts --root .",
|
|
@@ -67,32 +67,32 @@
|
|
|
67
67
|
"prepublishOnly": "npm run build"
|
|
68
68
|
},
|
|
69
69
|
"dependencies": {
|
|
70
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
71
|
-
"@emnapi/core": "^1.11.
|
|
72
|
-
"@emnapi/runtime": "^1.11.
|
|
73
|
-
"@hono/node-server": "^2.0
|
|
74
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
75
|
-
"@openai/codex": "^0.
|
|
76
|
-
"better-sqlite3": "^
|
|
77
|
-
"cron-parser": "^5.
|
|
78
|
-
"hono": "^4.
|
|
79
|
-
"nanoid": "^
|
|
70
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.226",
|
|
71
|
+
"@emnapi/core": "^1.11.3",
|
|
72
|
+
"@emnapi/runtime": "^1.11.3",
|
|
73
|
+
"@hono/node-server": "^2.1.0",
|
|
74
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
75
|
+
"@openai/codex": "^0.147.0",
|
|
76
|
+
"better-sqlite3": "^13.0.3",
|
|
77
|
+
"cron-parser": "^5.7.0",
|
|
78
|
+
"hono": "^4.13.1",
|
|
79
|
+
"nanoid": "^6.0.1",
|
|
80
80
|
"node-pty": "^1.1.0",
|
|
81
|
-
"ws": "^8.21.
|
|
81
|
+
"ws": "^8.21.3"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@biomejs/biome": "2.
|
|
85
|
-
"@types/better-sqlite3": "^
|
|
86
|
-
"@types/node": "^26.
|
|
84
|
+
"@biomejs/biome": "2.5.7",
|
|
85
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
86
|
+
"@types/node": "^26.2.0",
|
|
87
87
|
"@types/ws": "^8.18.1",
|
|
88
|
-
"@vitest/runner": "^4.1.
|
|
89
|
-
"concurrently": "^10.0.
|
|
90
|
-
"tsx": "^4.
|
|
88
|
+
"@vitest/runner": "^4.1.10",
|
|
89
|
+
"concurrently": "^10.0.4",
|
|
90
|
+
"tsx": "^4.23.11",
|
|
91
91
|
"typescript": "^6.0.3",
|
|
92
|
-
"vitest": "^4.1.
|
|
92
|
+
"vitest": "^4.1.10"
|
|
93
93
|
},
|
|
94
94
|
"overrides": {
|
|
95
|
-
"@hono/node-server": "^2.0
|
|
95
|
+
"@hono/node-server": "^2.1.0",
|
|
96
96
|
"shell-quote": "^1.10.0"
|
|
97
97
|
}
|
|
98
98
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
.markdown-message[data-v-1b7bd8ca]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-1b7bd8ca] *{max-width:100%}.markdown-message[data-v-1b7bd8ca] p{margin:0 0 .5em}.markdown-message[data-v-1b7bd8ca] p:last-child{margin-bottom:0}.markdown-message[data-v-1b7bd8ca] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-1b7bd8ca] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-1b7bd8ca] pre code{background:0 0;padding:0}.markdown-message[data-v-1b7bd8ca] ul,.markdown-message[data-v-1b7bd8ca] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-1b7bd8ca] li{margin:.15em 0}.markdown-message[data-v-1b7bd8ca] a{color:#7986cb;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] .document-link{color:#9fa8da;cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-1b7bd8ca] .document-link:hover{color:#c5cae9;-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] h1,.markdown-message[data-v-1b7bd8ca] h2,.markdown-message[data-v-1b7bd8ca] h3,.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-1b7bd8ca] h1{font-size:1.25em}.markdown-message[data-v-1b7bd8ca] h2{font-size:1.15em}.markdown-message[data-v-1b7bd8ca] h3{font-size:1.08em}.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{font-size:1em}.markdown-message[data-v-1b7bd8ca] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-1b7bd8ca] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-1b7bd8ca] th,.markdown-message[data-v-1b7bd8ca] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-fc912366]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-fc912366]{color:#bbb;cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366],.tool-row--toggleable .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366]:hover,.tool-row--toggleable .tool-header[data-v-fc912366]:hover{background:#ffffff08}.tool-icon[data-v-fc912366]{color:#9fbce0;flex-shrink:0}.tool-name[data-v-fc912366]{color:#d0d0d0;flex-shrink:0;font-weight:600}.tool-arg[data-v-fc912366],.tool-path[data-v-fc912366]{color:#999;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-fc912366]{flex-shrink:0}.tool-path[data-v-fc912366],.tool-arg[data-v-fc912366]{flex:1}.tool-stat-add[data-v-fc912366]{color:#66bb6a;flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-fc912366]{color:#ef5350;flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-fc912366]{background:#0003;border-radius:4px;max-height:400px;margin-top:4px;padding:8px 0;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-fc912366]{white-space:pre;color:#bbb;padding:0 12px}.diff-sign[data-v-fc912366]{color:#555;-webkit-user-select:none;user-select:none;width:14px;display:inline-block}.diff-add[data-v-fc912366]{color:#c8e6c9;background:#66bb6a1a}.diff-add .diff-sign[data-v-fc912366]{color:#66bb6a}.diff-del[data-v-fc912366]{color:#ffcdd2;background:#ef53501a}.diff-del .diff-sign[data-v-fc912366]{color:#ef5350}.tool-output[data-v-fc912366]{color:#aaa;white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;overflow:auto}.markdown-message[data-v-f34be4c5]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-f34be4c5] *{max-width:100%}.markdown-message[data-v-f34be4c5] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-f34be4c5] code{word-break:break-all}.markdown-message[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-message[data-v-f34be4c5] p:last-child{margin-bottom:0}.markdown-message[data-v-f34be4c5] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-f34be4c5] h1,.markdown-message[data-v-f34be4c5] h2,.markdown-message[data-v-f34be4c5] h3,.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-f34be4c5] h1{font-size:1.25em}.markdown-message[data-v-f34be4c5] h2{font-size:1.15em}.markdown-message[data-v-f34be4c5] h3{font-size:1.08em}.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{font-size:1em}.markdown-user-prompt[data-v-f34be4c5]{color:#aaa;font-size:12px;font-style:italic}.markdown-user-prompt[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-user-prompt[data-v-f34be4c5] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-6b8d8e3e]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-6b8d8e3e]{animation:1.8s ease-out search-highlight-6b8d8e3e}@keyframes search-highlight-6b8d8e3e{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-6b8d8e3e]{color:#888;background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-6b8d8e3e]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-6b8d8e3e]{color:#ce93d8;background:#ce93d826}.turn-badge-agent[data-v-6b8d8e3e]{color:#7986cb;background:#7986cb26}.turn-badge-system[data-v-6b8d8e3e]{color:#bdbdbd;background:#75757533;font-style:italic}.turn-badge-session[data-v-6b8d8e3e]{color:#9e9e9e;background:#61616133}.turn-badge-script[data-v-6b8d8e3e]{color:#4db6ac;background:#4db6ac26}.turn-time[data-v-6b8d8e3e]{color:#666;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px}.turn-time-arrow[data-v-6b8d8e3e]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-6b8d8e3e]{color:#8891a3}.turn-actions[data-v-6b8d8e3e]{color:#777;font-size:11px}.turn-body[data-v-6b8d8e3e]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-6b8d8e3e]>*{min-width:0;max-width:100%}.turn-body[data-v-6b8d8e3e] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-6b8d8e3e]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-6b8d8e3e]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-6b8d8e3e]:hover{opacity:1}.activity-feed-wrap[data-v-
|
|
1
|
+
.markdown-message[data-v-1b7bd8ca]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-1b7bd8ca] *{max-width:100%}.markdown-message[data-v-1b7bd8ca] p{margin:0 0 .5em}.markdown-message[data-v-1b7bd8ca] p:last-child{margin-bottom:0}.markdown-message[data-v-1b7bd8ca] pre{background:#00000059;border-radius:4px;margin:.5em 0;padding:.5em .75em;overflow-x:auto}.markdown-message[data-v-1b7bd8ca] code{word-break:break-all;background:#0000004d;border-radius:3px;padding:.1em .3em;font-size:.9em}.markdown-message[data-v-1b7bd8ca] pre code{background:0 0;padding:0}.markdown-message[data-v-1b7bd8ca] ul,.markdown-message[data-v-1b7bd8ca] ol{margin:.25em 0 .5em;padding-left:1.5em}.markdown-message[data-v-1b7bd8ca] li{margin:.15em 0}.markdown-message[data-v-1b7bd8ca] a{color:#7986cb;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] .document-link{color:#9fa8da;cursor:pointer;-webkit-text-decoration:underline dotted;text-decoration:underline dotted}.markdown-message[data-v-1b7bd8ca] .document-link:hover{color:#c5cae9;-webkit-text-decoration:underline;text-decoration:underline}.markdown-message[data-v-1b7bd8ca] h1,.markdown-message[data-v-1b7bd8ca] h2,.markdown-message[data-v-1b7bd8ca] h3,.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{margin:.5em 0 .3em;font-weight:600;line-height:1.3}.markdown-message[data-v-1b7bd8ca] h1{font-size:1.25em}.markdown-message[data-v-1b7bd8ca] h2{font-size:1.15em}.markdown-message[data-v-1b7bd8ca] h3{font-size:1.08em}.markdown-message[data-v-1b7bd8ca] h4,.markdown-message[data-v-1b7bd8ca] h5,.markdown-message[data-v-1b7bd8ca] h6{font-size:1em}.markdown-message[data-v-1b7bd8ca] blockquote{color:#ffffffb3;border-left:3px solid #fff3;margin:.5em 0;padding-left:.75em}.markdown-message[data-v-1b7bd8ca] table{border-collapse:collapse;margin:.5em 0}.markdown-message[data-v-1b7bd8ca] th,.markdown-message[data-v-1b7bd8ca] td{border:1px solid #ffffff26;padding:.25em .5em}.tool-row[data-v-fc912366]{border-radius:4px;margin:0;font-size:12px}.tool-header[data-v-fc912366]{color:#bbb;cursor:default;align-items:center;gap:10px;min-width:0;padding:5px 10px;display:flex}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366],.tool-row--toggleable .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:has(.tool-diff) .tool-header[data-v-fc912366]{cursor:pointer}.tool-row:not(.tool-row-generic) .tool-header[data-v-fc912366]:hover,.tool-row--toggleable .tool-header[data-v-fc912366]:hover{background:#ffffff08}.tool-icon[data-v-fc912366]{color:#9fbce0;flex-shrink:0}.tool-name[data-v-fc912366]{color:#d0d0d0;flex-shrink:0;font-weight:600}.tool-arg[data-v-fc912366],.tool-path[data-v-fc912366]{color:#999;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11.5px;overflow:hidden}.tool-open-diff[data-v-fc912366]{flex-shrink:0}.tool-path[data-v-fc912366],.tool-arg[data-v-fc912366]{flex:1}.tool-stat-add[data-v-fc912366]{color:#66bb6a;flex-shrink:0;font-size:11px;font-weight:600}.tool-stat-del[data-v-fc912366]{color:#ef5350;flex-shrink:0;font-size:11px;font-weight:600}.tool-diff[data-v-fc912366]{background:#0003;border-radius:4px;max-height:400px;margin-top:4px;padding:8px 0;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;line-height:1.5;overflow:auto}.diff-line[data-v-fc912366]{white-space:pre;color:#bbb;padding:0 12px}.diff-sign[data-v-fc912366]{color:#555;-webkit-user-select:none;user-select:none;width:14px;display:inline-block}.diff-add[data-v-fc912366]{color:#c8e6c9;background:#66bb6a1a}.diff-add .diff-sign[data-v-fc912366]{color:#66bb6a}.diff-del[data-v-fc912366]{color:#ffcdd2;background:#ef53501a}.diff-del .diff-sign[data-v-fc912366]{color:#ef5350}.tool-output[data-v-fc912366]{color:#aaa;white-space:pre-wrap;background:#00000026;border-radius:4px;max-height:8em;margin-top:4px;padding:6px 10px;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px;overflow:auto}.markdown-message[data-v-f34be4c5]{color:#e0e0e0;word-break:break-word;overflow-wrap:anywhere;min-width:0;max-width:100%;font-size:13px;line-height:1.55}.markdown-message[data-v-f34be4c5] *{max-width:100%}.markdown-message[data-v-f34be4c5] img{-o-object-fit:contain;object-fit:contain;cursor:zoom-in;background:#0003;border-radius:4px;max-width:180px;max-height:100px;margin:.3em 0;display:block}.markdown-message[data-v-f34be4c5] code{word-break:break-all}.markdown-message[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-message[data-v-f34be4c5] p:last-child{margin-bottom:0}.markdown-message[data-v-f34be4c5] code{background:#00000040;border-radius:3px;padding:.1em .3em}.markdown-message[data-v-f34be4c5] h1,.markdown-message[data-v-f34be4c5] h2,.markdown-message[data-v-f34be4c5] h3,.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{margin:.4em 0 .25em;font-weight:600;line-height:1.3}.markdown-message[data-v-f34be4c5] h1{font-size:1.25em}.markdown-message[data-v-f34be4c5] h2{font-size:1.15em}.markdown-message[data-v-f34be4c5] h3{font-size:1.08em}.markdown-message[data-v-f34be4c5] h4,.markdown-message[data-v-f34be4c5] h5,.markdown-message[data-v-f34be4c5] h6{font-size:1em}.markdown-user-prompt[data-v-f34be4c5]{color:#aaa;font-size:12px;font-style:italic}.markdown-user-prompt[data-v-f34be4c5] p{margin:0 0 .4em}.markdown-user-prompt[data-v-f34be4c5] code{background:#ffffff14;border-radius:3px;padding:.1em .3em;font-style:normal}.image-lightbox-img{-o-object-fit:contain;object-fit:contain;cursor:zoom-out;background:#0000004d;border-radius:4px;max-width:92vw;max-height:92vh;display:block}.turn-card[data-v-6b8d8e3e]{border:1px solid #ffffff14;border-left:3px solid var(--turn-accent);background:#ffffff05;border-radius:6px;min-width:0;max-width:100%;margin:14px 0;overflow:hidden}.turn-card--highlighted[data-v-6b8d8e3e]{animation:1.8s ease-out search-highlight-6b8d8e3e}@keyframes search-highlight-6b8d8e3e{0%,45%{box-shadow:0 0 0 2px #818cf8f2,0 0 22px #818cf873}to{box-shadow:none}}.turn-header[data-v-6b8d8e3e]{color:#888;background:#ffffff08;border-bottom:1px solid #ffffff0d;align-items:center;gap:8px;padding:8px 14px;font-size:11px;display:flex}.turn-badge[data-v-6b8d8e3e]{letter-spacing:.3px;border-radius:3px;padding:2px 8px;font-size:11px;font-weight:700}.turn-badge-user[data-v-6b8d8e3e]{color:#ce93d8;background:#ce93d826}.turn-badge-agent[data-v-6b8d8e3e]{color:#7986cb;background:#7986cb26}.turn-badge-system[data-v-6b8d8e3e]{color:#bdbdbd;background:#75757533;font-style:italic}.turn-badge-session[data-v-6b8d8e3e]{color:#9e9e9e;background:#61616133}.turn-badge-script[data-v-6b8d8e3e]{color:#4db6ac;background:#4db6ac26}.turn-time[data-v-6b8d8e3e]{color:#666;font-family:SF Mono,Menlo,Consolas,monospace;font-size:11px}.turn-time-arrow[data-v-6b8d8e3e]{opacity:.7;margin:0 -2px}.turn-time-updated[data-v-6b8d8e3e]{color:#8891a3}.turn-actions[data-v-6b8d8e3e]{color:#777;font-size:11px}.turn-body[data-v-6b8d8e3e]{flex-direction:column;gap:12px;min-width:0;padding:14px 18px;display:flex}.turn-body[data-v-6b8d8e3e]>*{min-width:0;max-width:100%}.turn-body[data-v-6b8d8e3e] .tool-row+.tool-row{margin-top:-8px}.turn-scroll-top[data-v-6b8d8e3e]{justify-content:flex-start;padding:0 8px 6px;display:flex}.turn-scroll-top-btn[data-v-6b8d8e3e]{opacity:.5;transition:opacity .15s}.turn-scroll-top-btn[data-v-6b8d8e3e]:hover{opacity:1}.activity-feed-wrap[data-v-0fba0dc4]{width:100%;height:100%;position:relative}.activity-feed-scroll[data-v-0fba0dc4]{width:100%;height:100%}.activity-feed-nav-cluster[data-v-0fba0dc4]{z-index:2;align-items:center;gap:8px;display:flex;position:absolute;bottom:14px;right:14px}.activity-feed-compacting[data-v-0fba0dc4]{z-index:3;background:var(--kobo-surface);border:1px solid var(--kobo-border);border-radius:6px;align-items:center;gap:8px;padding:6px 14px;display:flex;position:absolute;top:12px;left:50%;transform:translate(-50%);box-shadow:0 4px 16px #00000059}.fade-enter-active[data-v-0fba0dc4],.fade-leave-active[data-v-0fba0dc4]{transition:opacity .16s}.fade-enter-from[data-v-0fba0dc4],.fade-leave-to[data-v-0fba0dc4]{opacity:0}.activity-feed-nav-btn[data-v-0fba0dc4]{opacity:.8;transition:opacity .12s}.activity-feed-nav-btn[data-v-0fba0dc4]:hover{opacity:1}.content-origin-marker[data-v-0fba0dc4]{pointer-events:none;width:0;height:0;margin:0;padding:0}.activity-feed-scroll[data-v-0fba0dc4] .q-scrollarea__content{max-width:100%;overflow-x:hidden}.activity-feed-switching[data-v-0fba0dc4]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}
|