shadok-ai 0.15.49 → 0.15.51
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/README.md +2 -2
- package/package.json +1 -1
- package/public/echo-author.js +24 -0
- package/public/index.html +78 -2
package/README.md
CHANGED
|
@@ -204,8 +204,8 @@ Enter to skip; you can add it later from the web UI).
|
|
|
204
204
|
browser, since shell mode sits outside the agent's guardrails. The command and
|
|
205
205
|
its output stay in the chat, reload included — as does one you typed yourself
|
|
206
206
|
in the terminal view.
|
|
207
|
-
- **Files you hand an agent** — paste or drop a file into the composer
|
|
208
|
-
image, a PDF, a CSV…) and it becomes a **chip** above the text box: a
|
|
207
|
+
- **Files you hand an agent** — paste or drop a file into the composer, or pick
|
|
208
|
+
one with the 📎 button beside it (an image, a PDF, a CSV…), and it becomes a **chip** above the text box: a
|
|
209
209
|
thumbnail for an image, a named tag for anything else, each with a ✕ to
|
|
210
210
|
remove it. On send, an image shows **inline in your own message bubble**;
|
|
211
211
|
under the hood the agent still receives it as an `[Attached …: path]` line,
|
package/package.json
CHANGED
package/public/echo-author.js
CHANGED
|
@@ -24,3 +24,27 @@ export function echoAuthor(msg) {
|
|
|
24
24
|
// from another of your tabs; a single cockpit has one human.
|
|
25
25
|
return place || "you";
|
|
26
26
|
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Is this prompt the viewer's OWN? Decides the side of the bubble: yours on the
|
|
30
|
+
* right, anyone else's — a teammate on the web, a Telegram sender, a cron, the
|
|
31
|
+
* auto-retry — on the left, still tinted as a human message (only the agent is
|
|
32
|
+
* white).
|
|
33
|
+
*
|
|
34
|
+
* Only a web prompt can be yours: another platform is another person, or nobody.
|
|
35
|
+
* Without a name it is a prompt from this browser or a cockpit with no accounts
|
|
36
|
+
* — yours. While `me` is not known yet (`/me` still in flight), a named web
|
|
37
|
+
* prompt is taken as yours too: guessing "someone else" would flash your own
|
|
38
|
+
* history to the left on every reload.
|
|
39
|
+
*
|
|
40
|
+
* @param {{from?: string, origin?: string, auto?: boolean}} msg
|
|
41
|
+
* @param {string | null | undefined} me the signed-in account's name, if known
|
|
42
|
+
*/
|
|
43
|
+
export function isOwnPrompt(msg, me) {
|
|
44
|
+
if (msg?.auto) return false;
|
|
45
|
+
const origin = (msg?.origin ?? "").trim();
|
|
46
|
+
if (origin && origin !== "web") return false;
|
|
47
|
+
const from = (msg?.from ?? "").trim();
|
|
48
|
+
if (!from || !me) return true;
|
|
49
|
+
return from === me;
|
|
50
|
+
}
|
package/public/index.html
CHANGED
|
@@ -1128,6 +1128,11 @@
|
|
|
1128
1128
|
between this bubble and the one before, so it keeps its own label — a
|
|
1129
1129
|
wordless boundary the reader would otherwise never see. */
|
|
1130
1130
|
.turn.user { align-self: flex-end; align-items: flex-end; }
|
|
1131
|
+
/* Someone else's prompt (a teammate, a Telegram sender, a cron): on the
|
|
1132
|
+
agent's side, still a human bubble — same tint as yours, only the agent is
|
|
1133
|
+
white. The side and the name say who; a colour per person read as noise. */
|
|
1134
|
+
.turn.user.other { align-self: flex-start; align-items: flex-start; }
|
|
1135
|
+
.turn.user + .turn.user.new-speaker > .label { display: block; }
|
|
1131
1136
|
.turn.claude { align-self: flex-start; }
|
|
1132
1137
|
.bubble {
|
|
1133
1138
|
padding: 9px 14px;
|
|
@@ -1535,6 +1540,25 @@
|
|
|
1535
1540
|
line-height: 1.45;
|
|
1536
1541
|
}
|
|
1537
1542
|
#composer textarea:focus-visible { outline: none; border-color: var(--amber); box-shadow: 0 0 0 3px var(--amber-soft); }
|
|
1543
|
+
/* The paperclip: a square as tall as the one-line textarea, aligned to its
|
|
1544
|
+
bottom so it stays beside the last line while the field grows. */
|
|
1545
|
+
.cmp-attach {
|
|
1546
|
+
flex: none;
|
|
1547
|
+
align-self: flex-end;
|
|
1548
|
+
width: 42px;
|
|
1549
|
+
height: 42px;
|
|
1550
|
+
padding: 0;
|
|
1551
|
+
display: inline-flex;
|
|
1552
|
+
align-items: center;
|
|
1553
|
+
justify-content: center;
|
|
1554
|
+
background: transparent;
|
|
1555
|
+
color: var(--text-dim);
|
|
1556
|
+
border: 1px solid var(--line);
|
|
1557
|
+
border-radius: var(--radius);
|
|
1558
|
+
cursor: pointer;
|
|
1559
|
+
}
|
|
1560
|
+
.cmp-attach:hover:not(:disabled) { color: var(--amber); border-color: var(--amber); }
|
|
1561
|
+
.cmp-attach:disabled { opacity: 0.4; cursor: default; }
|
|
1538
1562
|
|
|
1539
1563
|
/* ── Engine room (live terminal) ─────────────────────── */
|
|
1540
1564
|
#machine {
|
|
@@ -2946,6 +2970,11 @@
|
|
|
2946
2970
|
<form id="composer" hidden>
|
|
2947
2971
|
<!-- Thumbnails of the files dropped / pasted into this agent's composer. -->
|
|
2948
2972
|
<div id="composerAtts" class="cmp-atts" hidden></div>
|
|
2973
|
+
<!-- Pick a file to send: the same path as a paste or a drop (attachFile). -->
|
|
2974
|
+
<button type="button" id="attachBtn" class="cmp-attach" title="Attach a file" aria-label="Attach a file">
|
|
2975
|
+
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M21 11.5l-8.6 8.6a5.5 5.5 0 0 1-7.8-7.8l8.6-8.6a3.7 3.7 0 0 1 5.2 5.2l-8.6 8.6a1.8 1.8 0 0 1-2.6-2.6l7.9-7.9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
2976
|
+
</button>
|
|
2977
|
+
<input type="file" id="attachInput" multiple hidden>
|
|
2949
2978
|
<textarea id="promptInput" rows="1"
|
|
2950
2979
|
placeholder="Write your prompt — Enter to send, Shift+Enter for a new line"></textarea>
|
|
2951
2980
|
<button type="submit" class="primary" id="sendBtn">Send</button>
|
|
@@ -3023,7 +3052,7 @@
|
|
|
3023
3052
|
hasReadonlyPreset,
|
|
3024
3053
|
applyReadonlyPreset, isManagedProfile, profileSaveBody, secretUsers } from "/profile-card.js?v=__ASSET_V__";
|
|
3025
3054
|
import { notifyState, BLINK_MS } from "/notify.js?v=__ASSET_V__";
|
|
3026
|
-
import { echoAuthor } from "/echo-author.js?v=__ASSET_V__";
|
|
3055
|
+
import { echoAuthor, isOwnPrompt } from "/echo-author.js?v=__ASSET_V__";
|
|
3027
3056
|
import { dialPos, dialColor, arcSegments, dialTitle, SWEEP_DEG } from "/gauge-dial.js?v=__ASSET_V__";
|
|
3028
3057
|
import { TOUR_STEPS, bubblePlacement, unionRect, visibleSteps } from "/tour-steps.js?v=__ASSET_V__";
|
|
3029
3058
|
import { pickChannelSource, dirKey, serverSwapped } from "/channel-store.js?v=__ASSET_V__";
|
|
@@ -3081,6 +3110,7 @@
|
|
|
3081
3110
|
window.hasReadonlyPreset = hasReadonlyPreset;
|
|
3082
3111
|
window.applyReadonlyPreset = applyReadonlyPreset;
|
|
3083
3112
|
window.echoAuthor = echoAuthor;
|
|
3113
|
+
window.isOwnPrompt = isOwnPrompt;
|
|
3084
3114
|
window.notifyState = notifyState;
|
|
3085
3115
|
window.BLINK_MS = BLINK_MS;
|
|
3086
3116
|
window.dialPos = dialPos;
|
|
@@ -3130,6 +3160,7 @@
|
|
|
3130
3160
|
extractLiveText: () => "",
|
|
3131
3161
|
livePreviewDecision: () => null,
|
|
3132
3162
|
echoAuthor: () => "",
|
|
3163
|
+
isOwnPrompt: () => true,
|
|
3133
3164
|
notifyState: () => ({ color: null, badge: "", blink: false }),
|
|
3134
3165
|
dialPos: () => 0,
|
|
3135
3166
|
dialColor: () => "",
|
|
@@ -5896,6 +5927,32 @@
|
|
|
5896
5927
|
return window.echoAuthor ? window.echoAuthor(msg) : "you";
|
|
5897
5928
|
}
|
|
5898
5929
|
|
|
5930
|
+
/** Which side a human prompt sits on: yours on the right, anyone else's on
|
|
5931
|
+
* the left — still a tinted human bubble, only the agent is white. The author
|
|
5932
|
+
* is kept on the turn so the side can be re-decided once `/me` answers
|
|
5933
|
+
* (`resideAllPrompts`). */
|
|
5934
|
+
function markPromptSide(bubble, msg) {
|
|
5935
|
+
const turn = bubble.parentElement;
|
|
5936
|
+
turn.dataset.from = msg?.from ?? "";
|
|
5937
|
+
turn.dataset.origin = msg?.origin ?? "";
|
|
5938
|
+
if (msg?.auto) turn.dataset.auto = "1";
|
|
5939
|
+
// A run of prompts shows one label — but only while it is ONE speaker.
|
|
5940
|
+
const prev = turn.previousElementSibling;
|
|
5941
|
+
const who = turn.querySelector(":scope > .label")?.firstChild?.textContent ?? "";
|
|
5942
|
+
const prevWho = prev?.classList.contains("user") ? prev.querySelector(":scope > .label")?.firstChild?.textContent ?? "" : who;
|
|
5943
|
+
turn.classList.toggle("new-speaker", who !== prevWho);
|
|
5944
|
+
applyPromptSide(turn);
|
|
5945
|
+
}
|
|
5946
|
+
function applyPromptSide(turn) {
|
|
5947
|
+
const msg = { from: turn.dataset.from, origin: turn.dataset.origin, auto: turn.dataset.auto === "1" };
|
|
5948
|
+
const own = window.isOwnPrompt ? window.isOwnPrompt(msg, me?.name) : true;
|
|
5949
|
+
turn.classList.toggle("other", !own);
|
|
5950
|
+
}
|
|
5951
|
+
function resideAllPrompts() {
|
|
5952
|
+
for (const turn of document.querySelectorAll(".turn.user[data-origin]")) applyPromptSide(turn);
|
|
5953
|
+
}
|
|
5954
|
+
window.addEventListener("shadok-bridge-ready", resideAllPrompts);
|
|
5955
|
+
|
|
5899
5956
|
function addTurn(tab, role, who, text, extraClass, at) {
|
|
5900
5957
|
// Time of the last message in the channel list, relative ("3m ago"). Every
|
|
5901
5958
|
// message (replayed history or live) carries `at`; the last one wins.
|
|
@@ -7399,6 +7456,7 @@
|
|
|
7399
7456
|
// SAME echoAuthor the live echo uses: "Ada · telegram"
|
|
7400
7457
|
// for a Telegram message, "you" for your own web prompt.
|
|
7401
7458
|
const b = addTurn(t, "user", echoAuthorLabel({ from: h.from, origin: h.origin }), h.text, "hist", h.at);
|
|
7459
|
+
markPromptSide(b, { from: h.from, origin: h.origin });
|
|
7402
7460
|
stampMsgIds(t, b.parentElement, h.ids);
|
|
7403
7461
|
} else {
|
|
7404
7462
|
const b = addTurn(t, "claude", "claude", h.text, "hist", h.at);
|
|
@@ -7513,6 +7571,7 @@
|
|
|
7513
7571
|
closeActivity(t);
|
|
7514
7572
|
{
|
|
7515
7573
|
const b = addTurn(t, "user", echoAuthorLabel(msg), msg.text, null, Date.now());
|
|
7574
|
+
markPromptSide(b, msg);
|
|
7516
7575
|
awaitPromptId(b, msg.text);
|
|
7517
7576
|
markUnreadBoundary(t, b.parentElement);
|
|
7518
7577
|
}
|
|
@@ -7969,7 +8028,11 @@
|
|
|
7969
8028
|
// Sending = going back to the bottom, whatever happens (even after scrolling
|
|
7970
8029
|
// up): you want to see your message and the answer that follows.
|
|
7971
8030
|
t.transcriptEl._stickBottom = true;
|
|
7972
|
-
|
|
8031
|
+
{
|
|
8032
|
+
const b = addTurn(t, "user", "you", text, null, Date.now());
|
|
8033
|
+
markPromptSide(b, {});
|
|
8034
|
+
awaitPromptId(b, text);
|
|
8035
|
+
}
|
|
7973
8036
|
startTurnTimer(t);
|
|
7974
8037
|
setTabMood(t, "working");
|
|
7975
8038
|
// Remember this channel's last sent prompt, so Escape (an interruption) can
|
|
@@ -8216,6 +8279,17 @@
|
|
|
8216
8279
|
for (const item of items) await attachFile(item.getAsFile());
|
|
8217
8280
|
});
|
|
8218
8281
|
|
|
8282
|
+
// The paperclip opens the OS file picker; what is picked goes through
|
|
8283
|
+
// attachFile exactly like a paste or a drop.
|
|
8284
|
+
const attachInput = $("attachInput");
|
|
8285
|
+
$("attachBtn").addEventListener("click", () => { if (active) attachInput.click(); });
|
|
8286
|
+
attachInput.addEventListener("change", async () => {
|
|
8287
|
+
const files = [...(attachInput.files || [])];
|
|
8288
|
+
attachInput.value = ""; // picking the same file again must fire `change` again
|
|
8289
|
+
for (const f of files) await attachFile(f);
|
|
8290
|
+
promptInput.focus();
|
|
8291
|
+
});
|
|
8292
|
+
|
|
8219
8293
|
// Drag-and-drop files anywhere on the chat pane → attach them, same path as
|
|
8220
8294
|
// paste. Only reacts to an OS file drag (`types` includes "Files"), so the
|
|
8221
8295
|
// internal tab/group reordering DnD is untouched. A depth counter keeps the
|
|
@@ -9497,6 +9571,8 @@
|
|
|
9497
9571
|
$("signOut").hidden = !named;
|
|
9498
9572
|
$("whoSep").hidden = !named;
|
|
9499
9573
|
if (named) $("whoAmI").textContent = "Signed in as " + me.name + " · " + me.role;
|
|
9574
|
+
// Prompts drawn before we knew who you are get their side now.
|
|
9575
|
+
resideAllPrompts();
|
|
9500
9576
|
// Security: for a signed-in person (or the lone admin of a no-password
|
|
9501
9577
|
// instance, who is everybody). The server answers 403 otherwise.
|
|
9502
9578
|
$("securityBtn").hidden = !(me?.name || me?.role === "admin");
|