viberoom 0.3.1 → 0.4.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/NOTICE +12 -0
- package/README.md +123 -96
- package/dist/fsbrowse.js +65 -0
- package/dist/hub.js +18 -0
- package/dist/launcher.js +22 -0
- package/dist/main.js +4 -3
- package/dist/persona.js +1 -1
- package/dist/recipes.js +37 -14
- package/dist/room.js +3 -2
- package/dist/server.js +30 -0
- package/package.json +8 -5
- package/scripts/vendor-acp.mjs +85 -0
- package/ui/app.css +53 -1
- package/ui/app.js +360 -8
- package/ui/index.html +28 -1
- package/vendor/acp/claude-agent-acp/LICENSE +191 -0
- package/vendor/acp/claude-agent-acp/dist/acp-agent.js +7694 -0
- package/vendor/acp/claude-agent-acp/dist/acp-subagents.js +13 -0
- package/vendor/acp/claude-agent-acp/dist/air-extension.js +63 -0
- package/vendor/acp/claude-agent-acp/dist/async-tasks.js +613 -0
- package/vendor/acp/claude-agent-acp/dist/clear-context-coordinator.js +80 -0
- package/vendor/acp/claude-agent-acp/dist/elicitation.js +304 -0
- package/vendor/acp/claude-agent-acp/dist/exit-plan.js +154 -0
- package/vendor/acp/claude-agent-acp/dist/file-change-audit.js +350 -0
- package/vendor/acp/claude-agent-acp/dist/fork-session.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/goal-extension.js +50 -0
- package/vendor/acp/claude-agent-acp/dist/index.js +98 -0
- package/vendor/acp/claude-agent-acp/dist/lib.js +5 -0
- package/vendor/acp/claude-agent-acp/dist/native-subagents.js +422 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/effects.js +166 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/modes.js +41 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/normalization.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/filesystem.js +124 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shared.js +64 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/shell.js +100 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options/tools.js +135 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/options.js +60 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/presentation.js +83 -0
- package/vendor/acp/claude-agent-acp/dist/permissions/response.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/session-config-ids.js +4 -0
- package/vendor/acp/claude-agent-acp/dist/session-failure-extension.js +324 -0
- package/vendor/acp/claude-agent-acp/dist/session-mode.js +234 -0
- package/vendor/acp/claude-agent-acp/dist/session-titles.js +199 -0
- package/vendor/acp/claude-agent-acp/dist/settings.js +185 -0
- package/vendor/acp/claude-agent-acp/dist/tool-result-meta.js +19 -0
- package/vendor/acp/claude-agent-acp/dist/tools.js +1235 -0
- package/vendor/acp/claude-agent-acp/dist/utils.js +81 -0
- package/vendor/acp/claude-agent-acp/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/LICENSE.md +1 -0
- package/vendor/acp/claude-agent-sdk/agentSdkTypes.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/bridge.d.ts +378 -0
- package/vendor/acp/claude-agent-sdk/bridge.mjs +221 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.d.ts +107 -0
- package/vendor/acp/claude-agent-sdk/browser-sdk.js +185 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.d.ts +1 -0
- package/vendor/acp/claude-agent-sdk/extractFromBunfs.js +156 -0
- package/vendor/acp/claude-agent-sdk/manifest.json +65 -0
- package/vendor/acp/claude-agent-sdk/manifest.zst.json +73 -0
- package/vendor/acp/claude-agent-sdk/package.json +7 -0
- package/vendor/acp/claude-agent-sdk/sdk-tools.d.ts +4129 -0
- package/vendor/acp/claude-agent-sdk/sdk.d.ts +8687 -0
- package/vendor/acp/claude-agent-sdk/sdk.mjs +204 -0
- package/vendor/acp/codex-acp/LICENSE +190 -0
- package/vendor/acp/codex-acp/dist/index.js +34238 -0
- package/vendor/acp/codex-acp/package.json +7 -0
package/ui/app.js
CHANGED
|
@@ -60,6 +60,8 @@
|
|
|
60
60
|
chatInfoBtn: $("#chat-info-btn"),
|
|
61
61
|
search: $("#search"),
|
|
62
62
|
messages: $("#messages"),
|
|
63
|
+
jumpLatest: $("#jump-latest"),
|
|
64
|
+
detailsHandle: $("#details-handle"),
|
|
63
65
|
mentionMenu: $("#mention-menu"),
|
|
64
66
|
emojiMenu: $("#emoji-menu"),
|
|
65
67
|
emojiBtn: $("#emoji-btn"),
|
|
@@ -522,6 +524,7 @@
|
|
|
522
524
|
}
|
|
523
525
|
function scrollToBottom() {
|
|
524
526
|
els.messages.scrollTop = els.messages.scrollHeight;
|
|
527
|
+
els.jumpLatest.hidden = true;
|
|
525
528
|
}
|
|
526
529
|
function avatar(p, size, opts) {
|
|
527
530
|
return window.Avatars.avatarHtml(p, size, Object.assign({ recipes: state.recipes }, opts || {}));
|
|
@@ -575,16 +578,26 @@
|
|
|
575
578
|
function geek(id, bodyHtml, hint) {
|
|
576
579
|
return `<details class="geek" id="${id}"><summary>${ic("geek")}for geeks${hint ? `<span class="g-hint">${hint}</span>` : ""}<span class="chev">${ic("down")}</span></summary><div class="geek-body">${bodyHtml}</div></details>`;
|
|
577
580
|
}
|
|
581
|
+
const recentlySaved = new Map();
|
|
578
582
|
function bindSave(container, button, onSave) {
|
|
579
583
|
if (!container || !button) return;
|
|
584
|
+
let dirty = false;
|
|
585
|
+
let saving = false;
|
|
586
|
+
let again = false;
|
|
580
587
|
const arm = () => {
|
|
588
|
+
dirty = true;
|
|
581
589
|
button.disabled = false;
|
|
582
590
|
button.classList.remove("saved");
|
|
583
591
|
button.textContent = "Save";
|
|
584
592
|
};
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
593
|
+
const save = async () => {
|
|
594
|
+
if (!dirty) return;
|
|
595
|
+
if (saving) {
|
|
596
|
+
again = true;
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
saving = true;
|
|
600
|
+
dirty = false;
|
|
588
601
|
button.disabled = true;
|
|
589
602
|
button.classList.add("loading");
|
|
590
603
|
try {
|
|
@@ -592,11 +605,43 @@
|
|
|
592
605
|
button.classList.remove("loading");
|
|
593
606
|
button.classList.add("saved");
|
|
594
607
|
button.innerHTML = `${ic("check")} Saved`;
|
|
608
|
+
if (button.id) recentlySaved.set(button.id, Date.now());
|
|
595
609
|
} catch (e) {
|
|
610
|
+
dirty = true;
|
|
596
611
|
button.classList.remove("loading");
|
|
597
612
|
button.disabled = false;
|
|
598
613
|
showError(e);
|
|
599
614
|
}
|
|
615
|
+
saving = false;
|
|
616
|
+
if (again) {
|
|
617
|
+
again = false;
|
|
618
|
+
save();
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
if (button.id && Date.now() - (recentlySaved.get(button.id) || 0) < 5000) {
|
|
622
|
+
button.disabled = true;
|
|
623
|
+
button.classList.add("saved");
|
|
624
|
+
button.innerHTML = `${ic("check")} Saved`;
|
|
625
|
+
}
|
|
626
|
+
container.addEventListener("input", arm);
|
|
627
|
+
container.addEventListener("change", () => {
|
|
628
|
+
arm();
|
|
629
|
+
save();
|
|
630
|
+
});
|
|
631
|
+
container.addEventListener("keydown", (e) => {
|
|
632
|
+
if (e.key !== "Enter") return;
|
|
633
|
+
const t = e.target;
|
|
634
|
+
if (t.tagName === "INPUT" || (e.ctrlKey && (t.tagName === "TEXTAREA" || t.isContentEditable))) {
|
|
635
|
+
e.preventDefault();
|
|
636
|
+
t.blur();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
container.addEventListener("focusout", (e) => {
|
|
640
|
+
if (e.target.isContentEditable) save();
|
|
641
|
+
});
|
|
642
|
+
button.addEventListener("click", () => {
|
|
643
|
+
dirty = true;
|
|
644
|
+
save();
|
|
600
645
|
});
|
|
601
646
|
}
|
|
602
647
|
function roomHue(room) {
|
|
@@ -837,7 +882,7 @@
|
|
|
837
882
|
<section class="hero">
|
|
838
883
|
<div class="hero-text">
|
|
839
884
|
<div class="hero-eyebrow">viberoom</div>
|
|
840
|
-
<h1
|
|
885
|
+
<h1>${state.freshVibe ? "Welcome" : "Welcome back"}, ${esc(name)} 👋</h1>
|
|
841
886
|
<p>One chat, many coding agents. Summon your vibemates into a room, talk to all of them at once, and let them talk to each other.</p>
|
|
842
887
|
<div class="hero-actions">
|
|
843
888
|
<button class="btn cta hero-cta" id="home-open-room">${ic("plus")}Open a room</button>
|
|
@@ -1258,6 +1303,7 @@
|
|
|
1258
1303
|
for (const perm of room.permissions) renderPermission(room, perm);
|
|
1259
1304
|
refreshSeen(room);
|
|
1260
1305
|
scrollToBottom();
|
|
1306
|
+
renderTimeline();
|
|
1261
1307
|
}
|
|
1262
1308
|
|
|
1263
1309
|
function upsertMessage(roomId, m) {
|
|
@@ -1286,6 +1332,7 @@
|
|
|
1286
1332
|
if (m.from === "human") refreshSeen(room);
|
|
1287
1333
|
}
|
|
1288
1334
|
if (stick) scrollToBottom();
|
|
1335
|
+
if (m.from === "human") renderTimeline();
|
|
1289
1336
|
}
|
|
1290
1337
|
|
|
1291
1338
|
function removeMessage(roomId, id) {
|
|
@@ -1572,7 +1619,7 @@
|
|
|
1572
1619
|
$("#pp-avatar-picker").appendChild(
|
|
1573
1620
|
window.Avatars.pickerElement(p.avatar || "", (emoji) => {
|
|
1574
1621
|
$("#pp-avatar").value = emoji;
|
|
1575
|
-
$("#pp-avatar").dispatchEvent(new Event("
|
|
1622
|
+
$("#pp-avatar").dispatchEvent(new Event("change", { bubbles: true }));
|
|
1576
1623
|
}),
|
|
1577
1624
|
);
|
|
1578
1625
|
renderSkillChecks($("#pp-skills"), p.skills || []);
|
|
@@ -1661,7 +1708,7 @@
|
|
|
1661
1708
|
$("#me-avatar-picker").appendChild(
|
|
1662
1709
|
window.Avatars.pickerElement(s.humanAvatar || "", (emoji) => {
|
|
1663
1710
|
$("#me-avatar").value = emoji;
|
|
1664
|
-
$("#me-avatar").dispatchEvent(new Event("
|
|
1711
|
+
$("#me-avatar").dispatchEvent(new Event("change", { bubbles: true }));
|
|
1665
1712
|
}),
|
|
1666
1713
|
);
|
|
1667
1714
|
bindSave($("#me-vibe"), $("#me-save"), () => post("/api/settings", { humanName: $("#me-name").value, humanAvatar: $("#me-avatar").value, humanDescription: $("#me-desc").value }));
|
|
@@ -1680,8 +1727,8 @@
|
|
|
1680
1727
|
${field("Name", `<input type="text" id="rp-name" maxlength="60" value="${esc(room.name)}">`)}
|
|
1681
1728
|
${field("Emoji", `<div id="rp-emoji-picker"></div><input type="text" id="rp-emoji" maxlength="8" value="${esc(rs.emoji || "")}" placeholder="custom emoji (optional)">`, "A face for the room, next to its name.")}
|
|
1682
1729
|
${field("Topic", `<input type="text" id="rp-topic" maxlength="2000" value="${esc(rs.topic || "")}" placeholder="what this room is about (optional)">`)}
|
|
1683
|
-
${field("Folder", `<input type="text" id="rp-dir" maxlength="1000" value="${esc(room.dir)}" spellcheck="false">`, "Where the vibemates read and write. Changing it restarts them in the new folder; they replay the last messages.")}
|
|
1684
|
-
<label class="field mention-host"><span class="label">
|
|
1730
|
+
${field("Folder", `<span class="dir-row"><input type="text" id="rp-dir" maxlength="1000" value="${esc(room.dir)}" spellcheck="false"><button type="button" class="btn ghost browse-btn" id="rp-dir-browse" title="Choose a folder">${ic("folder")}Browse</button></span>`, "Where the vibemates read and write. Changing it restarts them in the new folder; they replay the last messages.")}
|
|
1731
|
+
<label class="field mention-host"><span class="label">Room rules${geekTip("References follow renames and note when a participant has left. Rules go into every vibemate's brief as instructions, not as routing.")}</span><div id="rp-rules" class="rules-editor" contenteditable="true" spellcheck="true" data-placeholder="e.g. Everyone listens to @Pesho, he is the manager. Keep answers under 3 sentences."></div><span class="hint">One rule per line; type @ to reference a participant.</span><div class="mention-menu inline" id="rp-rules-menu" hidden></div></label>
|
|
1685
1732
|
${field("Language", `<input type="text" id="rp-lang" value="${esc(lang)}" placeholder="follow the human (default), or e.g. English">`)}
|
|
1686
1733
|
</div>
|
|
1687
1734
|
<div class="section">
|
|
@@ -1736,6 +1783,10 @@
|
|
|
1736
1783
|
$("#rp-emoji").dispatchEvent(new Event("input", { bubbles: true }));
|
|
1737
1784
|
}),
|
|
1738
1785
|
);
|
|
1786
|
+
$("#rp-dir-browse").addEventListener("click", () => openFolderPicker($("#rp-dir").value, (dir) => {
|
|
1787
|
+
$("#rp-dir").value = dir;
|
|
1788
|
+
$("#rp-dir").dispatchEvent(new Event("change", { bubbles: true }));
|
|
1789
|
+
}));
|
|
1739
1790
|
bindSave($("#rp-form"), $("#rp-save"), async () => {
|
|
1740
1791
|
const name = $("#rp-name").value;
|
|
1741
1792
|
if (name.trim() !== room.name) await post(roomApi("/rename"), { name });
|
|
@@ -2457,8 +2508,10 @@
|
|
|
2457
2508
|
els.pfForm.addEventListener("submit", async (event) => {
|
|
2458
2509
|
event.preventDefault();
|
|
2459
2510
|
try {
|
|
2511
|
+
if (!(state.settings || {}).profileCompleted) state.freshVibe = true;
|
|
2460
2512
|
await post("/api/settings", { humanName: els.pfName.value.trim(), humanAvatar: els.pfAvatar.value.trim(), humanDescription: els.pfDesc.value.trim(), profileCompleted: true });
|
|
2461
2513
|
closeDialog(els.pfDialog);
|
|
2514
|
+
if (state.view === "home") renderHome();
|
|
2462
2515
|
toast(`Welcome, ${els.pfName.value.trim()}. Open a room and summon a vibemate.`, "info");
|
|
2463
2516
|
} catch (error) {
|
|
2464
2517
|
els.pfError.textContent = error.message;
|
|
@@ -3055,6 +3108,12 @@
|
|
|
3055
3108
|
els.sideToggle.innerHTML = ic(open ? "collapse" : "expand");
|
|
3056
3109
|
}
|
|
3057
3110
|
els.sideToggle.addEventListener("click", () => setSideOpen(els.app.classList.contains("side-collapsed")));
|
|
3111
|
+
els.messages.addEventListener("scroll", () => {
|
|
3112
|
+
els.jumpLatest.hidden = nearBottom();
|
|
3113
|
+
updateTimelineView();
|
|
3114
|
+
});
|
|
3115
|
+
els.jumpLatest.addEventListener("click", () => els.messages.scrollTo({ top: els.messages.scrollHeight, behavior: "smooth" }));
|
|
3116
|
+
els.detailsHandle.addEventListener("click", closeDetails);
|
|
3058
3117
|
setSideOpen(recall("sideOpen") !== "0");
|
|
3059
3118
|
$("#rail-logo").addEventListener("click", () => setView("home"));
|
|
3060
3119
|
$("#rail-new-room").addEventListener("click", openRoomDialog);
|
|
@@ -3168,5 +3227,298 @@
|
|
|
3168
3227
|
});
|
|
3169
3228
|
window.addEventListener("beforeunload", () => remember("view", state.view));
|
|
3170
3229
|
|
|
3230
|
+
const tl = { el: $("#timeline"), ticks: $("#timeline .tl-ticks"), view: $("#timeline .tl-view"), pop: $("#timeline .tl-pop"), items: [] };
|
|
3231
|
+
const TICK_H = 5;
|
|
3232
|
+
function renderTimeline() {
|
|
3233
|
+
const room = currentRoom();
|
|
3234
|
+
const nodes = room && state.view === "room" ? [...els.messages.querySelectorAll(".msg.mine:not(.hidden-by-search)")] : [];
|
|
3235
|
+
tl.items = nodes;
|
|
3236
|
+
tl.el.hidden = nodes.length === 0;
|
|
3237
|
+
tl.pop.hidden = true;
|
|
3238
|
+
if (!nodes.length) return;
|
|
3239
|
+
const total = els.messages.scrollHeight || 1;
|
|
3240
|
+
const h = Math.max(0, tl.ticks.clientHeight - TICK_H);
|
|
3241
|
+
tl.ticks.innerHTML = "";
|
|
3242
|
+
nodes.forEach((el, i) => {
|
|
3243
|
+
const t = document.createElement("div");
|
|
3244
|
+
t.className = "tl-tick";
|
|
3245
|
+
t.dataset.i = i;
|
|
3246
|
+
t.title = "";
|
|
3247
|
+
t.style.top = `${Math.round((el.offsetTop / total) * h)}px`;
|
|
3248
|
+
tl.ticks.appendChild(t);
|
|
3249
|
+
});
|
|
3250
|
+
updateTimelineView();
|
|
3251
|
+
}
|
|
3252
|
+
function updateTimelineView() {
|
|
3253
|
+
if (tl.el.hidden) return;
|
|
3254
|
+
const m = els.messages;
|
|
3255
|
+
const total = m.scrollHeight || 1;
|
|
3256
|
+
const h = tl.ticks.clientHeight;
|
|
3257
|
+
tl.view.style.top = `${(m.scrollTop / total) * h}px`;
|
|
3258
|
+
tl.view.style.height = `${Math.max(8, (m.clientHeight / total) * h)}px`;
|
|
3259
|
+
const top = m.scrollTop;
|
|
3260
|
+
const bottom = m.scrollTop + m.clientHeight;
|
|
3261
|
+
tl.items.forEach((el, i) => {
|
|
3262
|
+
const tick = tl.ticks.children[i];
|
|
3263
|
+
if (tick) tick.classList.toggle("in-view", el.offsetTop + el.offsetHeight > top && el.offsetTop < bottom);
|
|
3264
|
+
});
|
|
3265
|
+
}
|
|
3266
|
+
function timelineText(el) {
|
|
3267
|
+
const t = el.querySelector(".text");
|
|
3268
|
+
return (t ? t.innerText : el.innerText).trim().replace(/\s+/g, " ").slice(0, 240);
|
|
3269
|
+
}
|
|
3270
|
+
function showTimelinePop(i) {
|
|
3271
|
+
const rows = [[i - 2, "faded far"], [i - 1, "faded"], [i, "current"], [i + 1, "faded"], [i + 2, "faded far"]].filter(([k]) => tl.items[k]);
|
|
3272
|
+
tl.pop.innerHTML = rows.map(([k, c]) => `<div class="tl-row ${c}" data-i="${k}">${esc(timelineText(tl.items[k]))}</div>`).join("");
|
|
3273
|
+
tl.pop.hidden = false;
|
|
3274
|
+
tl.ticks.querySelectorAll(".tl-tick.active").forEach((t) => t.classList.remove("active"));
|
|
3275
|
+
const tick = tl.ticks.children[i];
|
|
3276
|
+
if (tick) tick.classList.add("active");
|
|
3277
|
+
const current = tl.pop.querySelector(".tl-row.current");
|
|
3278
|
+
let top = (tick ? tick.offsetTop : 0) - (current ? current.offsetTop + current.offsetHeight / 2 : 20) + TICK_H / 2;
|
|
3279
|
+
top = Math.max(0, Math.min(top, tl.el.clientHeight - tl.pop.offsetHeight));
|
|
3280
|
+
tl.pop.style.top = `${top}px`;
|
|
3281
|
+
}
|
|
3282
|
+
function hideTimelinePop() {
|
|
3283
|
+
tl.pop.hidden = true;
|
|
3284
|
+
tl.ticks.querySelectorAll(".tl-tick.active").forEach((t) => t.classList.remove("active"));
|
|
3285
|
+
}
|
|
3286
|
+
function jumpToMessage(el) {
|
|
3287
|
+
if (!el) return;
|
|
3288
|
+
el.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
3289
|
+
el.classList.remove("flash");
|
|
3290
|
+
void el.offsetWidth;
|
|
3291
|
+
el.classList.add("flash");
|
|
3292
|
+
}
|
|
3293
|
+
tl.ticks.addEventListener("mouseover", (e) => {
|
|
3294
|
+
const tick = e.target.closest(".tl-tick");
|
|
3295
|
+
if (tick) showTimelinePop(Number(tick.dataset.i));
|
|
3296
|
+
});
|
|
3297
|
+
tl.el.addEventListener("mouseleave", hideTimelinePop);
|
|
3298
|
+
tl.ticks.addEventListener("click", (e) => {
|
|
3299
|
+
const tick = e.target.closest(".tl-tick");
|
|
3300
|
+
if (tick) jumpToMessage(tl.items[Number(tick.dataset.i)]);
|
|
3301
|
+
});
|
|
3302
|
+
tl.pop.addEventListener("click", (e) => {
|
|
3303
|
+
const row = e.target.closest(".tl-row");
|
|
3304
|
+
if (row) jumpToMessage(tl.items[Number(row.dataset.i)]);
|
|
3305
|
+
});
|
|
3306
|
+
new ResizeObserver(() => renderTimeline()).observe(els.messages);
|
|
3307
|
+
|
|
3308
|
+
const fp = { onChoose: null, selected: "", roots: [], home: "" };
|
|
3309
|
+
const fpEls = { dialog: $("#folder-dialog"), path: $("#fp-path"), tree: $("#fp-tree"), recent: $("#fp-recent"), error: $("#fp-error"), selected: $("#fp-selected"), choose: $("#fp-choose"), home: $("#fp-home"), newBtn: $("#fp-new") };
|
|
3310
|
+
const sepOf = (p) => (p.includes("\\") || /^[A-Za-z]:/.test(p) ? "\\" : "/");
|
|
3311
|
+
const sameFolder = (a, b) => a.replace(/[\\/]+$/, "").toLowerCase() === b.replace(/[\\/]+$/, "").toLowerCase();
|
|
3312
|
+
const isUnder = (child, parent) => {
|
|
3313
|
+
const c = child.replace(/[\\/]+$/, "").toLowerCase();
|
|
3314
|
+
const p = parent.replace(/[\\/]+$/, "").toLowerCase();
|
|
3315
|
+
return c === p || c.startsWith(p + sepOf(parent)) || (parent.endsWith(sepOf(parent)) && c.startsWith(p + sepOf(parent)));
|
|
3316
|
+
};
|
|
3317
|
+
function fpFail(error) {
|
|
3318
|
+
fpEls.error.textContent = error.message || String(error);
|
|
3319
|
+
fpEls.error.hidden = false;
|
|
3320
|
+
}
|
|
3321
|
+
function fpNode(entry) {
|
|
3322
|
+
const li = document.createElement("li");
|
|
3323
|
+
li.dataset.path = entry.path;
|
|
3324
|
+
li.innerHTML = `<div class="tn${entry.hidden ? " hidden-dir" : ""}"><button type="button" class="tn-tw" title="Expand">${ic("forward")}</button><span class="tn-ico">📁</span><span class="tn-name">${esc(entry.name)}</span></div><ul hidden></ul>`;
|
|
3325
|
+
return li;
|
|
3326
|
+
}
|
|
3327
|
+
function fpSelect(path, li) {
|
|
3328
|
+
fp.selected = path;
|
|
3329
|
+
fpEls.tree.querySelectorAll(".tn.selected").forEach((el) => el.classList.remove("selected"));
|
|
3330
|
+
if (li) li.querySelector(":scope > .tn").classList.add("selected");
|
|
3331
|
+
fpEls.path.value = path;
|
|
3332
|
+
fpEls.selected.textContent = path;
|
|
3333
|
+
fpEls.error.hidden = true;
|
|
3334
|
+
}
|
|
3335
|
+
async function fpLoad(li) {
|
|
3336
|
+
const ul = li.querySelector(":scope > ul");
|
|
3337
|
+
if (li.dataset.loaded) return ul;
|
|
3338
|
+
const data = await get(`/api/fs/dirs?path=${encodeURIComponent(li.dataset.path)}`);
|
|
3339
|
+
ul.innerHTML = "";
|
|
3340
|
+
for (const d of data.dirs) ul.appendChild(fpNode(d));
|
|
3341
|
+
if (!data.dirs.length) ul.innerHTML = `<li class="tn-more">no sub-folders</li>`;
|
|
3342
|
+
li.dataset.loaded = "1";
|
|
3343
|
+
li.querySelector(":scope > .tn").classList.toggle("leaf", !data.dirs.length);
|
|
3344
|
+
return ul;
|
|
3345
|
+
}
|
|
3346
|
+
async function fpExpand(li, open) {
|
|
3347
|
+
const tn = li.querySelector(":scope > .tn");
|
|
3348
|
+
const ul = li.querySelector(":scope > ul");
|
|
3349
|
+
const want = open === undefined ? ul.hidden : open;
|
|
3350
|
+
if (!want) {
|
|
3351
|
+
ul.hidden = true;
|
|
3352
|
+
tn.classList.remove("open");
|
|
3353
|
+
return;
|
|
3354
|
+
}
|
|
3355
|
+
tn.classList.add("open");
|
|
3356
|
+
try {
|
|
3357
|
+
await fpLoad(li);
|
|
3358
|
+
ul.hidden = false;
|
|
3359
|
+
} catch (e) {
|
|
3360
|
+
tn.classList.remove("open");
|
|
3361
|
+
fpFail(e);
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
async function fpReveal(path) {
|
|
3365
|
+
const target = path.replace(/[\\/]+$/, "") || path;
|
|
3366
|
+
let level = fpEls.tree;
|
|
3367
|
+
let found = null;
|
|
3368
|
+
for (let guard = 0; guard < 64; guard++) {
|
|
3369
|
+
const li = [...level.children].find((el) => el.dataset && el.dataset.path && isUnder(target, el.dataset.path));
|
|
3370
|
+
if (!li) break;
|
|
3371
|
+
found = li;
|
|
3372
|
+
if (sameFolder(li.dataset.path, target)) break;
|
|
3373
|
+
await fpExpand(li, true);
|
|
3374
|
+
level = li.querySelector(":scope > ul");
|
|
3375
|
+
}
|
|
3376
|
+
if (found && sameFolder(found.dataset.path, target)) {
|
|
3377
|
+
fpSelect(found.dataset.path, found);
|
|
3378
|
+
found.scrollIntoView({ block: "center" });
|
|
3379
|
+
return true;
|
|
3380
|
+
}
|
|
3381
|
+
return false;
|
|
3382
|
+
}
|
|
3383
|
+
async function fpGoTo(typed) {
|
|
3384
|
+
const p = typed.trim();
|
|
3385
|
+
if (!p) return;
|
|
3386
|
+
try {
|
|
3387
|
+
const data = await get(`/api/fs/dirs?path=${encodeURIComponent(p)}`);
|
|
3388
|
+
if (!(await fpReveal(data.path))) fpSelect(data.path, null);
|
|
3389
|
+
} catch (e) {
|
|
3390
|
+
fpFail(e);
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
function fpRecent() {
|
|
3394
|
+
const dirs = [];
|
|
3395
|
+
for (const room of state.rooms.values()) if (room.dir && !dirs.some((d) => sameFolder(d, room.dir))) dirs.push(room.dir);
|
|
3396
|
+
fpEls.recent.innerHTML = "";
|
|
3397
|
+
for (const d of dirs.slice(0, 6)) {
|
|
3398
|
+
const b = document.createElement("button");
|
|
3399
|
+
b.type = "button";
|
|
3400
|
+
b.className = "chip-btn";
|
|
3401
|
+
b.title = d;
|
|
3402
|
+
b.textContent = d.split(/[\\/]/).filter(Boolean).slice(-1)[0] || d;
|
|
3403
|
+
b.addEventListener("click", () => fpGoTo(d));
|
|
3404
|
+
fpEls.recent.appendChild(b);
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
async function openFolderPicker(initial, onChoose) {
|
|
3408
|
+
fp.onChoose = onChoose;
|
|
3409
|
+
fpEls.error.hidden = true;
|
|
3410
|
+
fpEls.tree.innerHTML = `<li class="tn-more">loading…</li>`;
|
|
3411
|
+
fpSelect("", null);
|
|
3412
|
+
openDialog(fpEls.dialog);
|
|
3413
|
+
try {
|
|
3414
|
+
const data = await get("/api/fs/dirs");
|
|
3415
|
+
fp.roots = data.roots;
|
|
3416
|
+
fp.home = data.home;
|
|
3417
|
+
fpEls.tree.innerHTML = "";
|
|
3418
|
+
for (const r of data.roots) fpEls.tree.appendChild(fpNode(r));
|
|
3419
|
+
fpRecent();
|
|
3420
|
+
const start = (initial || "").trim() || data.home;
|
|
3421
|
+
await fpGoTo(start);
|
|
3422
|
+
fpEls.path.focus();
|
|
3423
|
+
} catch (e) {
|
|
3424
|
+
fpFail(e);
|
|
3425
|
+
}
|
|
3426
|
+
}
|
|
3427
|
+
fpEls.tree.addEventListener("click", (e) => {
|
|
3428
|
+
const li = e.target.closest("li[data-path]");
|
|
3429
|
+
if (!li) return;
|
|
3430
|
+
if (e.target.closest(".tn-tw")) return void fpExpand(li);
|
|
3431
|
+
fpSelect(li.dataset.path, li);
|
|
3432
|
+
});
|
|
3433
|
+
fpEls.tree.addEventListener("dblclick", (e) => {
|
|
3434
|
+
const li = e.target.closest("li[data-path]");
|
|
3435
|
+
if (li && !e.target.closest(".tn-tw")) fpExpand(li);
|
|
3436
|
+
});
|
|
3437
|
+
fpEls.path.addEventListener("keydown", (e) => {
|
|
3438
|
+
if (e.key === "Enter") {
|
|
3439
|
+
e.preventDefault();
|
|
3440
|
+
fpGoTo(fpEls.path.value);
|
|
3441
|
+
}
|
|
3442
|
+
});
|
|
3443
|
+
fpEls.home.addEventListener("click", () => fpGoTo(fp.home));
|
|
3444
|
+
fpEls.newBtn.addEventListener("click", async () => {
|
|
3445
|
+
const li = fpEls.tree.querySelector(".tn.selected")?.closest("li[data-path]");
|
|
3446
|
+
if (!li) return fpFail(new Error("select the folder to create it in first"));
|
|
3447
|
+
await fpExpand(li, true);
|
|
3448
|
+
const ul = li.querySelector(":scope > ul");
|
|
3449
|
+
if (ul.querySelector(".tn-new")) return;
|
|
3450
|
+
const row = document.createElement("li");
|
|
3451
|
+
row.className = "tn-new";
|
|
3452
|
+
row.innerHTML = `<span class="tn-ico">📁</span><input type="text" placeholder="folder name" maxlength="120">`;
|
|
3453
|
+
ul.prepend(row);
|
|
3454
|
+
const input = row.querySelector("input");
|
|
3455
|
+
input.focus();
|
|
3456
|
+
const done = async () => {
|
|
3457
|
+
const name = input.value.trim();
|
|
3458
|
+
row.remove();
|
|
3459
|
+
if (!name) return;
|
|
3460
|
+
try {
|
|
3461
|
+
const r = await post("/api/fs/mkdir", { parent: li.dataset.path, name });
|
|
3462
|
+
delete li.dataset.loaded;
|
|
3463
|
+
await fpExpand(li, true);
|
|
3464
|
+
await fpReveal(r.path);
|
|
3465
|
+
} catch (err) {
|
|
3466
|
+
fpFail(err);
|
|
3467
|
+
}
|
|
3468
|
+
};
|
|
3469
|
+
input.addEventListener("keydown", (e) => {
|
|
3470
|
+
if (e.key === "Enter") {
|
|
3471
|
+
e.preventDefault();
|
|
3472
|
+
done();
|
|
3473
|
+
} else if (e.key === "Escape") {
|
|
3474
|
+
e.preventDefault();
|
|
3475
|
+
row.remove();
|
|
3476
|
+
}
|
|
3477
|
+
});
|
|
3478
|
+
input.addEventListener("blur", () => setTimeout(() => row.isConnected && done(), 120));
|
|
3479
|
+
});
|
|
3480
|
+
fpEls.choose.addEventListener("click", () => {
|
|
3481
|
+
const chosen = fp.selected || fpEls.path.value.trim();
|
|
3482
|
+
if (!chosen) return fpFail(new Error("pick a folder first"));
|
|
3483
|
+
closeDialog(fpEls.dialog);
|
|
3484
|
+
if (fp.onChoose) fp.onChoose(chosen);
|
|
3485
|
+
});
|
|
3486
|
+
$("#room-dir-browse").addEventListener("click", () => openFolderPicker(els.roomDir.value, (dir) => (els.roomDir.value = dir)));
|
|
3487
|
+
|
|
3488
|
+
if (window.matchMedia("(display-mode: standalone)").matches) {
|
|
3489
|
+
const placement = () => ({
|
|
3490
|
+
left: window.screenX,
|
|
3491
|
+
top: window.screenY,
|
|
3492
|
+
width: window.outerWidth,
|
|
3493
|
+
height: window.outerHeight,
|
|
3494
|
+
maximized: window.outerWidth >= screen.availWidth - 2 && window.outerHeight >= screen.availHeight - 2,
|
|
3495
|
+
screen: { left: screen.availLeft, top: screen.availTop, width: screen.availWidth, height: screen.availHeight },
|
|
3496
|
+
});
|
|
3497
|
+
const minimized = (p) => p.left <= -30000 || p.top <= -30000 || p.width < 100 || p.height < 100;
|
|
3498
|
+
const overlaps = (p) => p.left < p.screen.left + p.screen.width - 40 && p.left + p.width > p.screen.left + 40 && p.top < p.screen.top + p.screen.height - 40 && p.top + p.height > p.screen.top;
|
|
3499
|
+
const first = placement();
|
|
3500
|
+
if (!minimized(first) && !overlaps(first)) {
|
|
3501
|
+
window.resizeTo(Math.min(first.width, first.screen.width), Math.min(first.height, first.screen.height));
|
|
3502
|
+
window.moveTo(first.screen.left, first.screen.top);
|
|
3503
|
+
}
|
|
3504
|
+
let lastReport = "";
|
|
3505
|
+
let reportTimer = null;
|
|
3506
|
+
const report = (keepalive) => {
|
|
3507
|
+
const p = placement();
|
|
3508
|
+
if (minimized(p)) return;
|
|
3509
|
+
const json = JSON.stringify(p);
|
|
3510
|
+
if (json === lastReport) return;
|
|
3511
|
+
lastReport = json;
|
|
3512
|
+
fetch("/api/window", { method: "POST", headers: { "Content-Type": "application/json; charset=utf-8" }, body: json, keepalive }).catch(() => {});
|
|
3513
|
+
};
|
|
3514
|
+
const scheduleReport = () => {
|
|
3515
|
+
clearTimeout(reportTimer);
|
|
3516
|
+
reportTimer = setTimeout(() => report(false), 800);
|
|
3517
|
+
};
|
|
3518
|
+
window.addEventListener("resize", scheduleReport);
|
|
3519
|
+
setInterval(scheduleReport, 3000);
|
|
3520
|
+
window.addEventListener("pagehide", () => report(true));
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3171
3523
|
connect();
|
|
3172
3524
|
})();
|
package/ui/index.html
CHANGED
|
@@ -78,6 +78,12 @@
|
|
|
78
78
|
</div>
|
|
79
79
|
</header>
|
|
80
80
|
<div id="messages" class="messages"></div>
|
|
81
|
+
<button type="button" id="jump-latest" class="jump-latest" title="Back to the latest messages" hidden><span data-icon="arrow-down"></span>Latest</button>
|
|
82
|
+
<div id="timeline" class="timeline" hidden>
|
|
83
|
+
<div class="tl-view"></div>
|
|
84
|
+
<div class="tl-ticks"></div>
|
|
85
|
+
<div class="tl-pop" hidden></div>
|
|
86
|
+
</div>
|
|
81
87
|
<div id="mention-menu" class="mention-menu" hidden></div>
|
|
82
88
|
<div id="emoji-menu" class="mention-menu emoji-menu" hidden></div>
|
|
83
89
|
<form id="composer" class="composer">
|
|
@@ -94,6 +100,7 @@
|
|
|
94
100
|
</main>
|
|
95
101
|
<aside class="details" id="details" hidden>
|
|
96
102
|
<div class="resizer" id="details-resizer" title="Drag to resize; double-click to fit"></div>
|
|
103
|
+
<button type="button" class="details-handle" id="details-handle" title="Close the panel"><span data-icon="forward"></span></button>
|
|
97
104
|
<div class="details-inner" id="details-inner"></div>
|
|
98
105
|
</aside>
|
|
99
106
|
</div>
|
|
@@ -125,7 +132,7 @@
|
|
|
125
132
|
<p class="lead">A room is a shared space for you and the vibemates you summon. Give it a name.</p>
|
|
126
133
|
<label>Room name<input id="room-name" type="text" maxlength="60" required placeholder="e.g. Architecture review"></label>
|
|
127
134
|
<label>Working directory <span class="hint">(optional)</span><button type="button" class="geek-tip" title="For geeks"><span data-icon="geek"></span>for geeks</button><span class="geek-text" hidden>The folder the vibemates work in (their cwd). Leave it empty and the hub keeps a folder per room. Instruction files in it (CLAUDE.md, AGENTS.md, GEMINI.md, .cursor/rules) are read by the vibemates by their own conventions; the hub tells you if it finds any.</span>
|
|
128
|
-
<input id="room-dir" type="text" placeholder="C:\projects\my-app"></label>
|
|
135
|
+
<span class="dir-row"><input id="room-dir" type="text" placeholder="C:\projects\my-app"><button type="button" class="btn ghost browse-btn" id="room-dir-browse" title="Choose a folder"><span data-icon="folder"></span>Browse</button></span></label>
|
|
129
136
|
<p class="error" id="room-error" hidden></p>
|
|
130
137
|
<div class="actions"><button type="button" class="btn ghost" data-close>Cancel</button><button type="submit" class="btn primary">Open the room</button></div>
|
|
131
138
|
</form>
|
|
@@ -210,6 +217,26 @@
|
|
|
210
217
|
<div class="actions"><button type="button" class="btn ghost" data-close>Keep my vibe</button><button type="submit" id="erase-submit" class="btn danger solid" disabled>Erase everything</button></div>
|
|
211
218
|
</form>
|
|
212
219
|
</dialog>
|
|
220
|
+
<dialog id="folder-dialog" class="dialog wide folder-dialog">
|
|
221
|
+
<div class="file-head">
|
|
222
|
+
<h3><span class="h-ico" data-icon="folder"></span>Choose a folder</h3>
|
|
223
|
+
<div class="fp-bar">
|
|
224
|
+
<input id="fp-path" type="text" spellcheck="false" placeholder="Type a path and press Enter, or pick one below">
|
|
225
|
+
<button type="button" class="btn ghost" id="fp-home" title="Your home folder"><span data-icon="user"></span>Home</button>
|
|
226
|
+
<button type="button" class="btn ghost" id="fp-new" title="New folder inside the selected one"><span data-icon="plus"></span>New folder</button>
|
|
227
|
+
</div>
|
|
228
|
+
<div class="fp-recent" id="fp-recent"></div>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="fp-body">
|
|
231
|
+
<ul class="tree" id="fp-tree"></ul>
|
|
232
|
+
</div>
|
|
233
|
+
<p class="error" id="fp-error" hidden></p>
|
|
234
|
+
<div class="actions">
|
|
235
|
+
<span class="fp-selected" id="fp-selected"></span>
|
|
236
|
+
<button type="button" class="btn ghost" data-close>Cancel</button>
|
|
237
|
+
<button type="button" class="btn primary" id="fp-choose">Choose this folder</button>
|
|
238
|
+
</div>
|
|
239
|
+
</dialog>
|
|
213
240
|
<dialog id="file-dialog" class="dialog wide file-dialog">
|
|
214
241
|
<div class="file-head">
|
|
215
242
|
<h3><span class="h-ico" data-icon="link"></span><span id="fv-title">File</span></h3>
|