quoxcli 0.2.0 → 0.3.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/LICENSE +1 -1
- package/README.md +2 -0
- package/bin/quox.js +99 -126
- package/lib/api/endpoints/admin.js +26 -0
- package/lib/api/endpoints/agent-schedules.js +26 -0
- package/lib/api/endpoints/agents.js +7 -0
- package/lib/api/endpoints/assistants.js +12 -0
- package/lib/api/endpoints/away.js +15 -0
- package/lib/api/endpoints/chat.js +7 -0
- package/lib/api/endpoints/contactsq.js +2 -0
- package/lib/api/endpoints/conversations.js +14 -0
- package/lib/api/endpoints/executions.js +13 -0
- package/lib/api/endpoints/fetchAllowlist.js +27 -0
- package/lib/api/endpoints/files.js +14 -0
- package/lib/api/endpoints/hat.js +30 -0
- package/lib/api/endpoints/inbox.js +20 -0
- package/lib/api/endpoints/index.js +14 -0
- package/lib/api/endpoints/memory.js +19 -0
- package/lib/api/endpoints/mind.js +13 -0
- package/lib/api/endpoints/mon.js +6 -0
- package/lib/api/endpoints/objectives.js +6 -0
- package/lib/api/endpoints/plugins.js +18 -0
- package/lib/api/endpoints/presence.js +33 -0
- package/lib/api/endpoints/quoxplan.js +7 -0
- package/lib/api/endpoints/quoxseo.js +6 -0
- package/lib/api/endpoints/router.js +17 -0
- package/lib/api/endpoints/safety.js +7 -0
- package/lib/api/endpoints/skills.js +20 -0
- package/lib/api/endpoints/stepup.js +20 -0
- package/lib/api/endpoints/teams.js +2 -2
- package/lib/api/endpoints/triggers.js +26 -0
- package/lib/api/endpoints/v1.js +86 -0
- package/lib/api/endpoints/ward.js +7 -0
- package/lib/api/endpoints/workflows.js +7 -0
- package/lib/api/endpoints/x.js +8 -0
- package/lib/commands/actions.js +61 -0
- package/lib/commands/admin.js +16 -0
- package/lib/commands/agent.js +163 -27
- package/lib/commands/agentAttach.js +94 -6
- package/lib/commands/approvals.js +73 -1
- package/lib/commands/aside.js +24 -0
- package/lib/commands/ask.js +144 -0
- package/lib/commands/assistant.js +59 -4
- package/lib/commands/auth.js +8 -2
- package/lib/commands/backup.js +8 -4
- package/lib/commands/brain.js +105 -14
- package/lib/commands/build.js +19 -5
- package/lib/commands/chat.js +57 -2
- package/lib/commands/claude.js +34 -8
- package/lib/commands/contacts.js +49 -0
- package/lib/commands/conversation.js +35 -4
- package/lib/commands/dashboard.js +43 -0
- package/lib/commands/department.js +28 -7
- package/lib/commands/devResident.js +15 -3
- package/lib/commands/devSupervisor.js +13 -1
- package/lib/commands/discord.js +75 -39
- package/lib/commands/done.js +98 -28
- package/lib/commands/engagement.js +17 -6
- package/lib/commands/fetchAllowlist.js +71 -0
- package/lib/commands/file.js +38 -1
- package/lib/commands/fleet.js +311 -4
- package/lib/commands/governance.js +12 -3
- package/lib/commands/gripe.js +44 -13
- package/lib/commands/hat.js +178 -9
- package/lib/commands/helpers.js +18 -1
- package/lib/commands/hyperion.js +58 -0
- package/lib/commands/inbox.js +74 -23
- package/lib/commands/licence.js +14 -155
- package/lib/commands/lucid.js +10 -4
- package/lib/commands/memory.js +85 -8
- package/lib/commands/mind.js +71 -0
- package/lib/commands/mon.js +69 -2
- package/lib/commands/monitoring.js +1 -1
- package/lib/commands/motion.js +2 -2
- package/lib/commands/nucleus.js +48 -17
- package/lib/commands/objective.js +11 -0
- package/lib/commands/offload.js +89 -0
- package/lib/commands/onboard.js +2 -1
- package/lib/commands/org.js +11 -1
- package/lib/commands/peek.js +28 -0
- package/lib/commands/plan.js +38 -0
- package/lib/commands/plugin.js +110 -0
- package/lib/commands/presence.js +89 -0
- package/lib/commands/provider.js +159 -0
- package/lib/commands/qlarity.js +19 -3
- package/lib/commands/quoxchat.js +13 -4
- package/lib/commands/quoxseo.js +38 -0
- package/lib/commands/reach.js +2 -1
- package/lib/commands/router.js +77 -0
- package/lib/commands/rows.js +358 -0
- package/lib/commands/run.js +64 -9
- package/lib/commands/server.js +292 -0
- package/lib/commands/setup.js +33 -7
- package/lib/commands/skills.js +48 -5
- package/lib/commands/slot.js +260 -7
- package/lib/commands/status.js +1 -1
- package/lib/commands/team.js +3 -1
- package/lib/commands/tips.js +23 -0
- package/lib/commands/tool.js +133 -45
- package/lib/commands/trace.js +12 -7
- package/lib/commands/trigger.js +95 -0
- package/lib/commands/update.js +46 -0
- package/lib/commands/vault.js +31 -8
- package/lib/commands/vm.js +237 -0
- package/lib/commands/voice.js +34 -3
- package/lib/commands/volt.js +45 -3
- package/lib/commands/walkthrough.js +32 -0
- package/lib/commands/watch.js +73 -21
- package/lib/commands/welcome.js +14 -0
- package/lib/commands/workflow.js +189 -32
- package/lib/commands/x.js +13 -4
- package/lib/core/actingAs.js +2 -2
- package/lib/core/activeSummary.js +186 -0
- package/lib/core/aside.js +54 -0
- package/lib/core/askDecision.js +60 -0
- package/lib/core/auth.js +164 -13
- package/lib/core/callerSource.js +1 -1
- package/lib/core/cliBuild.js +62 -0
- package/lib/core/client.js +144 -35
- package/lib/core/commandRegistrars.js +121 -0
- package/lib/core/config.js +142 -24
- package/lib/core/customHats.js +168 -0
- package/lib/core/doneEngineFetch.js +8 -0
- package/lib/core/errors.js +329 -20
- package/lib/core/global-flags.js +105 -5
- package/lib/core/hyperion.js +102 -0
- package/lib/core/inboxResolution.js +65 -0
- package/lib/core/lazyCommands.js +4 -1
- package/lib/core/offload.js +504 -0
- package/lib/core/org-override.js +1 -1
- package/lib/core/orgResolve.js +70 -0
- package/lib/core/output.js +4 -4
- package/lib/core/peek.js +126 -0
- package/lib/core/profileUsers.js +51 -0
- package/lib/core/promptCancel.js +18 -0
- package/lib/core/repoPath.js +11 -0
- package/lib/core/residentSeed.js +3 -1
- package/lib/core/rootOptions.js +39 -0
- package/lib/core/router.js +168 -0
- package/lib/core/selfUpdate.js +188 -0
- package/lib/core/sessionsOverview.js +64 -0
- package/lib/core/slotKeeper.js +248 -3
- package/lib/core/slotWake.js +120 -0
- package/lib/core/supervisorEngine.js +71 -2
- package/lib/core/version.js +1 -1
- package/lib/core/vm.js +361 -0
- package/lib/core/vmTui.js +86 -0
- package/lib/core/walkthrough.js +35 -0
- package/lib/hyperion/deck.js +29 -0
- package/lib/lucid/LucidCard.jsx +68 -0
- package/lib/lucid/LucidIntro.jsx +45 -58
- package/lib/lucid/cards.js +86 -0
- package/lib/lucid/markdown.js +81 -0
- package/lib/lucid/motion.js +56 -0
- package/lib/quoxon/activity.js +14 -2
- package/lib/quoxon/aocl.js +117 -0
- package/lib/quoxon/api.js +504 -25
- package/lib/quoxon/app.js +3000 -230
- package/lib/quoxon/approvalFacts.js +94 -0
- package/lib/quoxon/approvalsView.js +325 -0
- package/lib/quoxon/asideView.js +40 -0
- package/lib/quoxon/ask.js +121 -0
- package/lib/quoxon/away.js +52 -0
- package/lib/quoxon/bang.js +224 -0
- package/lib/quoxon/bulkRisk.js +17 -0
- package/lib/quoxon/capabilities.js +13 -6
- package/lib/quoxon/cells.js +53 -0
- package/lib/quoxon/commandShort.js +112 -0
- package/lib/quoxon/commands.js +363 -38
- package/lib/quoxon/connect.js +210 -23
- package/lib/quoxon/connectCard.js +19 -2
- package/lib/quoxon/consent.js +86 -0
- package/lib/quoxon/copy.js +4 -3
- package/lib/quoxon/countdownLine.js +11 -0
- package/lib/quoxon/demo.autoplay.js +29 -0
- package/lib/quoxon/demo.fixtures.js +1 -1
- package/lib/quoxon/demo.js +44 -3
- package/lib/quoxon/devObserver.js +20 -0
- package/lib/quoxon/dividerAocl.js +191 -0
- package/lib/quoxon/dividerRule.js +57 -0
- package/lib/quoxon/dragonModes.js +150 -0
- package/lib/quoxon/dragonView.js +41 -0
- package/lib/quoxon/earlyInput.js +40 -0
- package/lib/quoxon/echo.js +15 -0
- package/lib/quoxon/egg.js +2 -2
- package/lib/quoxon/events.js +244 -25
- package/lib/quoxon/feedback.js +50 -0
- package/lib/quoxon/gallery.js +21 -7
- package/lib/quoxon/handoffOffer.js +78 -0
- package/lib/quoxon/hatSwitch.js +37 -0
- package/lib/quoxon/headless.js +161 -0
- package/lib/quoxon/hostUrls.js +41 -0
- package/lib/quoxon/indicator.js +6 -2
- package/lib/quoxon/intent.js +12 -1
- package/lib/quoxon/keys.js +49 -6
- package/lib/quoxon/launcher.js +103 -0
- package/lib/quoxon/main.js +92 -160
- package/lib/quoxon/markdown.js +45 -21
- package/lib/quoxon/markdownSettle.js +19 -0
- package/lib/quoxon/matrixDomains.json +46 -0
- package/lib/quoxon/mentions.js +90 -0
- package/lib/quoxon/mind.js +103 -0
- package/lib/quoxon/mode.js +46 -0
- package/lib/quoxon/model.js +132 -26
- package/lib/quoxon/modelPrefs.js +345 -0
- package/lib/quoxon/motion/clock.js +3 -2
- package/lib/quoxon/motion/level.js +91 -0
- package/lib/quoxon/mouse.js +211 -0
- package/lib/quoxon/noai.js +57 -0
- package/lib/quoxon/objects/away.js +72 -0
- package/lib/quoxon/objects/fetch.js +17 -5
- package/lib/quoxon/objects/fleet.js +149 -0
- package/lib/quoxon/objects/health.js +186 -0
- package/lib/quoxon/objects/index.js +24 -3
- package/lib/quoxon/objects/mirror.js +137 -0
- package/lib/quoxon/objects/peek.js +17 -0
- package/lib/quoxon/objects/prove.js +130 -0
- package/lib/quoxon/objects/replay.js +125 -0
- package/lib/quoxon/objects/sitrep.js +58 -0
- package/lib/quoxon/objects/view.js +11 -0
- package/lib/quoxon/onboarding/copy.js +32 -4
- package/lib/quoxon/onboarding/credential.js +17 -4
- package/lib/quoxon/onboarding/dockerOffer.js +45 -0
- package/lib/quoxon/onboarding/failureCause.js +33 -0
- package/lib/quoxon/onboarding/finish.js +15 -0
- package/lib/quoxon/onboarding/index.js +34 -7
- package/lib/quoxon/onboarding/installRunner.js +64 -5
- package/lib/quoxon/onboarding/preflight.js +17 -3
- package/lib/quoxon/onboarding/probe.js +65 -0
- package/lib/quoxon/onboarding/view.js +76 -19
- package/lib/quoxon/orgLabel.js +10 -0
- package/lib/quoxon/orgSwitch.js +33 -0
- package/lib/quoxon/ownBroadcast.js +22 -0
- package/lib/quoxon/palette.js +43 -0
- package/lib/quoxon/print.js +9 -5
- package/lib/quoxon/profileSwitch.js +53 -0
- package/lib/quoxon/promptHistory.js +41 -0
- package/lib/quoxon/providers.js +48 -0
- package/lib/quoxon/qart.js +20 -51
- package/lib/quoxon/qdragon.js +410 -0
- package/lib/quoxon/qdragonFly.js +61 -0
- package/lib/quoxon/qdragonScenes.js +282 -0
- package/lib/quoxon/qeyes.js +213 -0
- package/lib/quoxon/qmotion.js +58 -37
- package/lib/quoxon/render.js +638 -119
- package/lib/quoxon/resend.js +72 -0
- package/lib/quoxon/roster.js +95 -9
- package/lib/quoxon/route.js +6 -5
- package/lib/quoxon/rows.js +711 -0
- package/lib/quoxon/rowsStore.js +80 -0
- package/lib/quoxon/screenContext.js +150 -0
- package/lib/quoxon/screenFrames.js +254 -0
- package/lib/quoxon/session.js +172 -2
- package/lib/quoxon/sessionDead.js +17 -0
- package/lib/quoxon/signin.js +125 -0
- package/lib/quoxon/slash/agents.js +112 -0
- package/lib/quoxon/slash/argSources.js +65 -0
- package/lib/quoxon/slash/args.js +65 -0
- package/lib/quoxon/slash/automate.js +89 -0
- package/lib/quoxon/slash/broadcast.js +140 -0
- package/lib/quoxon/slash/broadcastRun.js +108 -0
- package/lib/quoxon/slash/card.js +30 -0
- package/lib/quoxon/slash/challenge.js +114 -0
- package/lib/quoxon/slash/cliCommand.js +53 -0
- package/lib/quoxon/slash/cliLeaves.json +948 -0
- package/lib/quoxon/slash/compact.js +1 -1
- package/lib/quoxon/slash/confirm.js +30 -0
- package/lib/quoxon/slash/crystallize.js +69 -0
- package/lib/quoxon/slash/dashboard.js +136 -0
- package/lib/quoxon/slash/draft.js +54 -0
- package/lib/quoxon/slash/hatWalk.js +206 -0
- package/lib/quoxon/slash/inverses.js +77 -0
- package/lib/quoxon/slash/killer.js +24 -0
- package/lib/quoxon/slash/killerApi.js +16 -0
- package/lib/quoxon/slash/ledger.js +13 -8
- package/lib/quoxon/slash/listView.js +3 -1
- package/lib/quoxon/slash/modelCmd.js +261 -0
- package/lib/quoxon/slash/palette.js +21 -7
- package/lib/quoxon/slash/paletteController.js +117 -7
- package/lib/quoxon/slash/panels.js +21 -6
- package/lib/quoxon/slash/picker.js +20 -2
- package/lib/quoxon/slash/restart.js +71 -13
- package/lib/quoxon/slash/rowCmd.js +157 -0
- package/lib/quoxon/slash/rowWalk.js +175 -0
- package/lib/quoxon/slash/settings.js +16 -6
- package/lib/quoxon/slash/spawn.js +177 -0
- package/lib/quoxon/slash/stepUp.js +274 -0
- package/lib/quoxon/slash/stepUpCard.js +60 -0
- package/lib/quoxon/slash/stepUpCmds.js +78 -0
- package/lib/quoxon/slash/undoApi.js +38 -0
- package/lib/quoxon/slash/undoCmd.js +49 -0
- package/lib/quoxon/slash/undoWish.js +15 -0
- package/lib/quoxon/slash/watch.js +164 -0
- package/lib/quoxon/slash/wish.js +23 -0
- package/lib/quoxon/slash/wishCmd.js +32 -0
- package/lib/quoxon/slotChat.js +300 -0
- package/lib/quoxon/slotFormat.js +195 -0
- package/lib/quoxon/slotRun.js +131 -0
- package/lib/quoxon/sound/mapping.js +2 -1
- package/lib/quoxon/stray.js +22 -0
- package/lib/quoxon/suggestCommand.js +59 -0
- package/lib/quoxon/term.js +46 -11
- package/lib/quoxon/textBuffer.js +176 -2
- package/lib/quoxon/theme.js +58 -25
- package/lib/quoxon/tips.js +433 -0
- package/lib/quoxon/toolError.js +41 -0
- package/lib/quoxon/transcript.js +42 -4
- package/lib/quoxon/web/qface.js +18 -10
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +136 -0
- package/lib/quoxon/whatsnew.jsonl +89 -0
- package/lib/quoxon/windowOrg.js +113 -0
- package/lib/server/diag.js +129 -0
- package/lib/server/doctor.js +71 -0
- package/lib/server/install.js +149 -0
- package/lib/server/pluginEnable.js +161 -0
- package/lib/server/skew.js +56 -0
- package/lib/server/snapshot.js +131 -0
- package/lib/server/status.js +128 -0
- package/lib/server/sys.js +65 -0
- package/lib/server/uninstall.js +63 -0
- package/lib/server/update.js +234 -0
- package/lib/toolOutputShape.js +315 -0
- package/lib/tui/views/HyperionDeck.jsx +79 -0
- package/lib/tui/views/LucidDeck.jsx +209 -217
- package/package.json +4 -1
- package/lib/core/websiteSession.js +0 -148
package/lib/quoxon/textBuffer.js
CHANGED
|
@@ -18,7 +18,135 @@ const UNDO_LIMIT = 100;
|
|
|
18
18
|
const PASTE_RE = /\[Pasted text #(\d+) \+\d+ lines\]/g;
|
|
19
19
|
|
|
20
20
|
export function initialBuffer(text = '') {
|
|
21
|
-
return { text, cursor: text.length, undo: [], redo: [], pastes: {}, pasteSeq: 0 };
|
|
21
|
+
return { text, cursor: text.length, anchor: null, undo: [], redo: [], pastes: {}, pasteSeq: 0 };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* QUOXON-MOUSE: the composer selection, { from, to } (from < to), or null. A selection is the range between
|
|
26
|
+
* `anchor` (where a drag started) and the cursor; only the mouse sets an anchor.
|
|
27
|
+
*/
|
|
28
|
+
export function selection(s) {
|
|
29
|
+
if (s?.anchor == null || s.anchor === s.cursor) return null;
|
|
30
|
+
return { from: Math.min(s.anchor, s.cursor), to: Math.max(s.anchor, s.cursor) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The word under `idx` for a double-click: a run of non-space characters, or the one space character itself. */
|
|
34
|
+
export function wordAt(text, idx) {
|
|
35
|
+
const t = String(text);
|
|
36
|
+
const i = Math.max(0, Math.min(idx, t.length - 1));
|
|
37
|
+
if (!t.length) return { from: 0, to: 0 };
|
|
38
|
+
if (/\s/.test(t[i])) return { from: i, to: nextBoundary(t, i) };
|
|
39
|
+
let a = i; let b = i;
|
|
40
|
+
while (a > 0 && !/\s/.test(t[a - 1])) a--;
|
|
41
|
+
while (b < t.length && !/\s/.test(t[b])) b++;
|
|
42
|
+
return { from: a, to: b };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Snap an index onto a grapheme boundary (at or before it). */
|
|
46
|
+
function snap(text, idx) {
|
|
47
|
+
const i = Math.max(0, Math.min(Number(idx) || 0, text.length));
|
|
48
|
+
return i === text.length ? i : prevBoundary(text, i + 1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The selected range widened so it never leaves half a paste placeholder behind. */
|
|
52
|
+
function selRange(s, sel) {
|
|
53
|
+
let { from, to } = sel;
|
|
54
|
+
PASTE_RE.lastIndex = 0;
|
|
55
|
+
let m;
|
|
56
|
+
while ((m = PASTE_RE.exec(s.text))) {
|
|
57
|
+
const a = m.index, b = a + m[0].length;
|
|
58
|
+
if (to > a && from < b) { from = Math.min(from, a); to = Math.max(to, b); }
|
|
59
|
+
}
|
|
60
|
+
return { from, to };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const EDITS = new Set(['insert', 'newline', 'paste', 'backspace', 'delete', 'deleteWordBack', 'killLineBack', 'killLineForward', 'deleteWordForward']);
|
|
64
|
+
|
|
65
|
+
/** COMPOSER-EDIT: the selected text, paste placeholders expanded (what Shift selection copies). '' when none. */
|
|
66
|
+
export function selectedText(s) {
|
|
67
|
+
const sel = selection(s);
|
|
68
|
+
if (!sel) return '';
|
|
69
|
+
const { from, to } = selRange(s, sel);
|
|
70
|
+
return expandPastes({ text: s.text.slice(from, to), pastes: s.pastes || {} });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ---- COMPOSER-EDIT: words and soft-wrapped rows ------------------------------------------------
|
|
74
|
+
const WORD = /[\p{L}\p{N}_]/u;
|
|
75
|
+
/** Readline backward-word: skip non-word characters, then the word. */
|
|
76
|
+
function wordLeftIndex(t, i) {
|
|
77
|
+
while (i > 0 && !WORD.test(t[i - 1])) i--;
|
|
78
|
+
while (i > 0 && WORD.test(t[i - 1])) i--;
|
|
79
|
+
return i;
|
|
80
|
+
}
|
|
81
|
+
/** Readline forward-word: skip non-word characters, then the word (lands at its end). */
|
|
82
|
+
function wordRightIndex(t, i) {
|
|
83
|
+
while (i < t.length && !WORD.test(t[i])) i++;
|
|
84
|
+
while (i < t.length && WORD.test(t[i])) i++;
|
|
85
|
+
return i;
|
|
86
|
+
}
|
|
87
|
+
/** Remove [from, to), widened so no paste placeholder is left half cut. */
|
|
88
|
+
function cutRange(s, from, to) {
|
|
89
|
+
if (from >= to) return s;
|
|
90
|
+
const r = selRange(s, { from, to });
|
|
91
|
+
return commit(s, { text: s.text.slice(0, r.from) + s.text.slice(r.to), cursor: r.from });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const cellW = (g) => (/[ᄀ-ᅟ⺀-〾ぁ-㏿㐀-䶿一-鿿ꀀ-가-힣豈-︰-﹏-⦆¢-₩]/u.test(g) || (/\p{Extended_Pictographic}/u.test(g) && (g.codePointAt(0) >= 0x1f000 || g.includes('\uFE0F'))) ? 2 : 1);
|
|
95
|
+
/**
|
|
96
|
+
* The draft as the rows the terminal shows at `width` cells: [{ start, end }] (UTF-16 indices, end exclusive,
|
|
97
|
+
* newline not included). Word wrap like Ink's: a word that does not fit starts a new row, spaces hang at the row
|
|
98
|
+
* end, and a word longer than a row is split hard.
|
|
99
|
+
*/
|
|
100
|
+
export function visualRows(text, width) {
|
|
101
|
+
const out = [];
|
|
102
|
+
let o = 0;
|
|
103
|
+
for (const line of String(text).split('\n')) {
|
|
104
|
+
if (!(width > 0) || !line.length) { out.push({ start: o, end: o + line.length }); o += line.length + 1; continue; }
|
|
105
|
+
const tokens = line.match(/\s+|\S+/g);
|
|
106
|
+
let rowStart = o; let w = 0; let p = o;
|
|
107
|
+
for (const tok of tokens) {
|
|
108
|
+
const gs = graphemes(tok);
|
|
109
|
+
const tw = gs.reduce((n, g) => n + cellW(g.text), 0);
|
|
110
|
+
if (/^\s/.test(tok) || w + tw <= width) { w += tw; p += tok.length; continue; }
|
|
111
|
+
if (tw <= width) { out.push({ start: rowStart, end: p }); rowStart = p; w = tw; p += tok.length; continue; }
|
|
112
|
+
for (const g of gs) { // longer than a row: split it hard
|
|
113
|
+
const cw = cellW(g.text);
|
|
114
|
+
if (w + cw > width && w > 0) { out.push({ start: rowStart, end: p }); rowStart = p; w = 0; }
|
|
115
|
+
w += cw; p += g.text.length;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
out.push({ start: rowStart, end: o + line.length });
|
|
119
|
+
o += line.length + 1;
|
|
120
|
+
}
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
/** Index of the visual row holding `cursor`. A cursor at a soft break sits at the start of the next row. */
|
|
124
|
+
function rowOf(rows, cursor) {
|
|
125
|
+
for (let i = rows.length - 1; i >= 0; i--) if (cursor >= rows[i].start) return i;
|
|
126
|
+
return 0;
|
|
127
|
+
}
|
|
128
|
+
/** COMPOSER-EDIT: is the cursor on the first (`up`) or last (`down`) row the terminal shows? */
|
|
129
|
+
export function atVisualEdge(s, dir, width) {
|
|
130
|
+
if (!(width > 0)) {
|
|
131
|
+
return dir === 'up' ? !s.text.slice(0, s.cursor).includes('\n') : !s.text.slice(s.cursor).includes('\n');
|
|
132
|
+
}
|
|
133
|
+
const rows = visualRows(s.text, width);
|
|
134
|
+
const r = rowOf(rows, s.cursor);
|
|
135
|
+
return dir === 'up' ? r === 0 : r === rows.length - 1;
|
|
136
|
+
}
|
|
137
|
+
function moveVisual(s, dir, width) {
|
|
138
|
+
const rows = visualRows(s.text, width);
|
|
139
|
+
const r = rowOf(rows, s.cursor);
|
|
140
|
+
const t = dir === 'up' ? r - 1 : r + 1;
|
|
141
|
+
if (t < 0 || t >= rows.length) return s;
|
|
142
|
+
const col = graphemes(s.text.slice(rows[r].start, s.cursor)).reduce((n, g) => n + cellW(g.text), 0);
|
|
143
|
+
const row = rows[t];
|
|
144
|
+
const soft = t + 1 < rows.length && rows[t + 1].start === row.end; // the row breaks softly: its end is the next row's start
|
|
145
|
+
const gs = graphemes(s.text.slice(row.start, row.end));
|
|
146
|
+
let at = 0; let idx = row.end;
|
|
147
|
+
for (const g of gs) { const w = cellW(g.text); if (col < at + w) { idx = row.start + g.index; break; } at += w; }
|
|
148
|
+
if (soft && idx >= row.end) idx = prevBoundary(s.text, row.end);
|
|
149
|
+
return { ...s, cursor: idx };
|
|
22
150
|
}
|
|
23
151
|
|
|
24
152
|
export function graphemes(text) {
|
|
@@ -86,6 +214,14 @@ function indexAtLineCol(text, line, col) {
|
|
|
86
214
|
return start + gs[col].index;
|
|
87
215
|
}
|
|
88
216
|
|
|
217
|
+
/** First logical line the composer shows (it shows at most `maxLines`, keeping the cursor line in view). */
|
|
218
|
+
export function composerFrom(buf, maxLines = 8) {
|
|
219
|
+
const n = buf.text.split('\n').length;
|
|
220
|
+
if (n <= maxLines) return 0;
|
|
221
|
+
const { line } = lineCol(buf.text, buf.cursor);
|
|
222
|
+
return Math.min(Math.max(0, line - Math.floor(maxLines / 2)), n - maxLines);
|
|
223
|
+
}
|
|
224
|
+
|
|
89
225
|
export function normalizeNewlines(t) { return String(t).replace(/\r\n?/g, '\n'); }
|
|
90
226
|
|
|
91
227
|
/** Build the collapsed form of a paste, or null when it is small enough to inline. */
|
|
@@ -94,7 +230,36 @@ export function shouldCollapse(text) {
|
|
|
94
230
|
return text.length > PASTE_CHAR_LIMIT || lines > PASTE_LINE_LIMIT;
|
|
95
231
|
}
|
|
96
232
|
|
|
97
|
-
export function bufferReducer(
|
|
233
|
+
export function bufferReducer(s0, a) {
|
|
234
|
+
const sel = selection(s0);
|
|
235
|
+
const s = s0.anchor == null ? s0 : { ...s0, anchor: null };
|
|
236
|
+
if (sel && EDITS.has(a.type)) {
|
|
237
|
+
// QUOXON-MOUSE: Backspace/Delete remove the selection; typing or pasting replaces it. One undo step.
|
|
238
|
+
const { from, to } = selRange(s, sel);
|
|
239
|
+
const cut = { ...s, text: s.text.slice(0, from) + s.text.slice(to), cursor: from };
|
|
240
|
+
if (a.type !== 'insert' && a.type !== 'newline' && a.type !== 'paste') return commit(s, { text: cut.text, cursor: from });
|
|
241
|
+
const next = baseReducer({ ...cut, undo: [] }, a);
|
|
242
|
+
return commit(s, { text: next.text, cursor: next.cursor, pastes: next.pastes, pasteSeq: next.pasteSeq });
|
|
243
|
+
}
|
|
244
|
+
if (sel && a.type === 'left') return { ...s, cursor: sel.from };
|
|
245
|
+
if (sel && a.type === 'right') return { ...s, cursor: sel.to };
|
|
246
|
+
switch (a.type) {
|
|
247
|
+
case 'extend': { // COMPOSER-EDIT: Shift+movement grows the selection from where it started
|
|
248
|
+
const anchor = s0.anchor ?? s0.cursor;
|
|
249
|
+
const moved = baseReducer({ ...s, anchor: null }, { type: a.move, width: a.width });
|
|
250
|
+
return { ...s, cursor: moved.cursor, anchor: moved.cursor === anchor ? null : anchor };
|
|
251
|
+
}
|
|
252
|
+
case 'selectAll': return s.text ? { ...s, anchor: 0, cursor: s.text.length } : s;
|
|
253
|
+
case 'place': return { ...s, cursor: snap(s.text, a.index) };
|
|
254
|
+
case 'select': {
|
|
255
|
+
const anchor = snap(s.text, a.anchor); const cursor = snap(s.text, a.cursor);
|
|
256
|
+
return { ...s, anchor: anchor === cursor ? null : anchor, cursor };
|
|
257
|
+
}
|
|
258
|
+
default: return baseReducer(s, a);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function baseReducer(s, a) {
|
|
98
263
|
switch (a.type) {
|
|
99
264
|
case 'insert': {
|
|
100
265
|
const t = normalizeNewlines(a.text);
|
|
@@ -144,6 +309,14 @@ export function bufferReducer(s, a) {
|
|
|
144
309
|
if (from === s.cursor) return s;
|
|
145
310
|
return commit(s, { text: s.text.slice(0, from) + s.text.slice(s.cursor), cursor: from });
|
|
146
311
|
}
|
|
312
|
+
case 'wordLeft': return { ...s, cursor: wordLeftIndex(s.text, s.cursor) };
|
|
313
|
+
case 'wordRight': return { ...s, cursor: wordRightIndex(s.text, s.cursor) };
|
|
314
|
+
case 'killLineForward': { // Ctrl+K: to the end of the line; at the end, join the next line
|
|
315
|
+
const nl = s.text.indexOf('\n', s.cursor);
|
|
316
|
+
const end = nl === -1 ? s.text.length : (nl === s.cursor ? nl + 1 : nl);
|
|
317
|
+
return cutRange(s, s.cursor, end);
|
|
318
|
+
}
|
|
319
|
+
case 'deleteWordForward': return cutRange(s, s.cursor, wordRightIndex(s.text, s.cursor)); // Alt+D
|
|
147
320
|
case 'left': return { ...s, cursor: prevBoundary(s.text, s.cursor) };
|
|
148
321
|
case 'right': return { ...s, cursor: nextBoundary(s.text, s.cursor) };
|
|
149
322
|
case 'home': return { ...s, cursor: lineCol(s.text, s.cursor).start };
|
|
@@ -152,6 +325,7 @@ export function bufferReducer(s, a) {
|
|
|
152
325
|
return { ...s, cursor: nl === -1 ? s.text.length : nl };
|
|
153
326
|
}
|
|
154
327
|
case 'up': case 'down': {
|
|
328
|
+
if (a.width > 0) return moveVisual(s, a.type, a.width); // COMPOSER-EDIT: through soft-wrapped rows
|
|
155
329
|
const { line, col } = lineCol(s.text, s.cursor);
|
|
156
330
|
const target = a.type === 'up' ? line - 1 : line + 1;
|
|
157
331
|
const count = s.text.split('\n').length;
|
package/lib/quoxon/theme.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Purple is for keys and accents only, never whole sentences.
|
|
9
9
|
*/
|
|
10
10
|
import { graphemes } from "./textBuffer.js";
|
|
11
|
+
import { SEMANTIC_DARK, SEMANTIC_LIGHT, stripMarks, MARK } from './palette.js';
|
|
11
12
|
|
|
12
13
|
export const MIN_COLS = 40;
|
|
13
14
|
|
|
@@ -20,14 +21,18 @@ export const DARK = {
|
|
|
20
21
|
value: ['#f1f5f9', 255],
|
|
21
22
|
number: ['#38bdf8', 75],
|
|
22
23
|
link: ['#38bdf8', 75],
|
|
23
|
-
code: ['#
|
|
24
|
-
key: ['#
|
|
25
|
-
muted: ['#
|
|
24
|
+
code: ['#2dd4bf', 80], codeBg: ['#1f2937', 235], codeRule: ['#155e75', 24],
|
|
25
|
+
key: ['#2dd4bf', 80], // a thing you type: the command colour (purple is brand chrome only)
|
|
26
|
+
muted: ['#bcc8e8', 189],
|
|
26
27
|
ok: ['#4ade80', 78], warn: ['#f59e0b', 214], error: ['#f87171', 210],
|
|
28
|
+
ask: ['#f472b6', 211], // QUOXON-ASK: "needs you" (every y/N and approval): glyph and keys, bold line
|
|
29
|
+
gold: ['#d4af37', 178], // the authorisation moment: step-up PIN card frame, filled dots, human-present countdown
|
|
27
30
|
// legacy names, kept so existing call sites read the same
|
|
28
31
|
sky: ['#38bdf8', 75], violet: ['#a855f7', 135], indigo: ['#818cf8', 105], teal: ['#06b6d4', 38],
|
|
29
|
-
fail: ['#f87171', 210], dim: ['#
|
|
30
|
-
text: ['#
|
|
32
|
+
fail: ['#f87171', 210], dim: ['#bcc8e8', 189], faint: ['#7c8fe0', 105],
|
|
33
|
+
text: ['#f1f5f9', 255], input: ['#ffffff', 231], placeholder: ['#bcc8e8', 189],
|
|
34
|
+
// semantic roles (palette.js)
|
|
35
|
+
prose: SEMANTIC_DARK.prose, command: SEMANTIC_DARK.command, id: SEMANTIC_DARK.value, brand: SEMANTIC_DARK.brand, quiet: SEMANTIC_DARK.quiet, rule: SEMANTIC_DARK.rule
|
|
31
36
|
};
|
|
32
37
|
export const LIGHT = {
|
|
33
38
|
heading: ['#0369a1', 25],
|
|
@@ -37,16 +42,19 @@ export const LIGHT = {
|
|
|
37
42
|
value: ['#0f172a', 234],
|
|
38
43
|
number: ['#0369a1', 25],
|
|
39
44
|
link: ['#0369a1', 25],
|
|
40
|
-
code: ['#
|
|
41
|
-
key: ['#
|
|
42
|
-
muted: ['#
|
|
45
|
+
code: ['#0f766e', 30], codeBg: ['#ecfeff', 195], codeRule: ['#67e8f9', 117],
|
|
46
|
+
key: ['#0f766e', 30],
|
|
47
|
+
muted: ['#3b4f87', 60],
|
|
43
48
|
ok: ['#15803d', 28], warn: ['#b45309', 130], error: ['#b91c1c', 124],
|
|
49
|
+
ask: ['#be185d', 161],
|
|
50
|
+
gold: ['#8a6500', 94],
|
|
44
51
|
sky: ['#0369a1', 25], violet: ['#7e22ce', 91], indigo: ['#4338ca', 62], teal: ['#0e7490', 30],
|
|
45
|
-
fail: ['#b91c1c', 124], dim: ['#
|
|
46
|
-
text: ['#
|
|
52
|
+
fail: ['#b91c1c', 124], dim: ['#3b4f87', 60], faint: ['#7280c8', 104],
|
|
53
|
+
text: ['#0f172a', 234], input: ['#0f172a', 234], placeholder: ['#3b4f87', 60],
|
|
54
|
+
prose: SEMANTIC_LIGHT.prose, command: SEMANTIC_LIGHT.command, id: SEMANTIC_LIGHT.value, brand: SEMANTIC_LIGHT.brand, quiet: SEMANTIC_LIGHT.quiet, rule: SEMANTIC_LIGHT.rule
|
|
47
55
|
};
|
|
48
56
|
/** Roles a reader must be able to read: each is held to >= 4.5:1 on its background (tests). */
|
|
49
|
-
export const ROLES = ['heading', 'icon', 'bullet1', 'bullet2', 'bullet3', 'label', 'value', 'number', 'link', 'code', 'key', 'muted', 'ok', 'warn', 'error', 'text', 'placeholder'];
|
|
57
|
+
export const ROLES = ['heading', 'icon', 'bullet1', 'bullet2', 'bullet3', 'label', 'value', 'number', 'link', 'code', 'key', 'muted', 'ok', 'warn', 'error', 'ask', 'gold', 'text', 'placeholder', 'prose', 'command', 'id', 'brand', 'quiet'];
|
|
50
58
|
|
|
51
59
|
/** Heading gradient stops across the word: sky -> indigo -> violet (light: the -700 shades). */
|
|
52
60
|
export const HEADING_STOPS = { dark: ['#38bdf8', '#6366f1', '#a855f7'], light: ['#0369a1', '#4338ca', '#7e22ce'] };
|
|
@@ -59,12 +67,12 @@ export const AGENT_SHADES = {
|
|
|
59
67
|
/** Model providers in their brand colours (dark, light). Unknown = muted. */
|
|
60
68
|
export const PROVIDER_COLORS = {
|
|
61
69
|
openai: ['#f5f5f5', '#111111'], anthropic: ['#D97757', '#b4532e'], google: ['#4796E3', '#1a62c4'],
|
|
62
|
-
ollama: ['#e5e7eb', '#
|
|
70
|
+
ollama: ['#e5e7eb', '#3b4f87'], mistral: ['#FA520F', '#c2410c'], deepseek: ['#4D6BFE', '#2f4fe0'],
|
|
63
71
|
perplexity: ['#20808D', '#1a6b76'], xai: ['#f5f5f5', '#111111'],
|
|
64
72
|
// B5: OpenAI-compatible presets (dark bg shade, light bg shade; all >= 4.5:1 on their background)
|
|
65
73
|
openrouter: ['#6566F1', '#4f46e5'], huggingface: ['#FFD21E', '#8a6500'], groq: ['#F55036', '#c2290f'],
|
|
66
74
|
together: ['#0F6FFF', '#0b57d0'], lmstudio: ['#a78bfa', '#6d28d9'], vllm: ['#30A2FF', '#0b6bb8'],
|
|
67
|
-
openai_compatible: ['#
|
|
75
|
+
openai_compatible: ['#c7d2fe', '#3b4f87']
|
|
68
76
|
};
|
|
69
77
|
/** Protocol trace marks (AEE, AOCL, VOLT, WARD), each its own Q shade (dark, light). */
|
|
70
78
|
export const PROTO_COLORS = {
|
|
@@ -187,21 +195,21 @@ export const bulletRole = (depth) => ['bullet1', 'bullet2', 'bullet3'][((depth %
|
|
|
187
195
|
|
|
188
196
|
// ---- paint: split a plain line into coloured runs; the characters never change ---------------------
|
|
189
197
|
const GLYPH_ROLE = {
|
|
190
|
-
'●': 'ok', '✔': 'ok', '◐': 'warn', '○': '
|
|
191
|
-
'◉': 'icon', 'Ⓠ': 'icon', '▸': '
|
|
198
|
+
'●': 'ok', '✔': 'ok', '◐': 'warn', '○': 'quiet', '?': 'quiet', '✖': 'error', '⚠': 'warn', '⊘': 'warn',
|
|
199
|
+
'◉': 'icon', 'Ⓠ': 'icon', '▸': 'brand', '⛔': 'warn', '↳': 'icon', '»': 'quiet', '·': 'quiet', '⬢': 'icon', '⬡': 'quiet', '▶': 'ask'
|
|
192
200
|
};
|
|
193
201
|
const KEYS = '(?:(?:Ctrl|Shift|Alt|Cmd|Option)\\+\\S+|Enter|Tab|Esc|Up|Down|Left|Right|Space|Backspace|PgUp|PgDn)';
|
|
194
202
|
const TOKEN = new RegExp([
|
|
195
203
|
'(https?:\\/\\/[^\\s)]+)', // 1 link
|
|
196
204
|
'(\\bquox(?: (?![·])[^\\s·,;)]+)+)', // 2 a quox command
|
|
197
|
-
'((?<![\\w/])\\/[a-z][\\w-]*)', // 3 slash command
|
|
205
|
+
'((?<![\\w/])\\/[a-z][\\w-]*(?: (?:--?[a-z][\\w-]*|<[^>\\s]+>|\\[[^\\]\\s]+\\]|\\d+|[\\w-]+(?:\\|[\\w-]+)+))*)', // 3 slash command + its flags/args
|
|
198
206
|
`((?<![\\w+])${KEYS}(?![\\w+]))`, // 4 keyboard key
|
|
199
207
|
'((?<![\\w<])<[^>\\s]+>|\\[[^\\]\\s]+\\])', // 5 placeholder <name> or [args]
|
|
200
208
|
'( · )', // 6 separator
|
|
201
209
|
'((?<![\\w/.-])(?:(?:gpt|claude|gemini|o[134]|mistral|codestral|deepseek|sonar|grok)-[\\w.:-]*\\w|ollama:[\\w.:-]*\\w|OpenAI|openai|Anthropic|anthropic|Claude(?: API)?|Gemini|Ollama|ollama|Mistral|DeepSeek|Perplexity|Grok|xAI)(?![\\w-]))', // 7 provider/model
|
|
202
210
|
'((?<![\\w.#-])\\d+(?:[.,:-]\\d+)*(?:%|ms|s|k|m|h|d|x|KB|MB|GB)?(?![\\w]))' // 8 number
|
|
203
211
|
].join('|'), 'g');
|
|
204
|
-
const ROLE_OF = [null, '
|
|
212
|
+
const ROLE_OF = [null, 'id', 'command', 'command', 'command', 'command', 'quiet', 'provider', 'id'];
|
|
205
213
|
const LETTER_KEY = /^([a-z])(?= (?:approve|deny|open|stop|peek|confirm|edit|retry|cancel|copy|quit)\b)/;
|
|
206
214
|
|
|
207
215
|
/** Runs of `text` with roles; `base` colours the prose between tokens. Pure. */
|
|
@@ -211,6 +219,16 @@ export function paintRuns(text, base) {
|
|
|
211
219
|
const s = String(text ?? '');
|
|
212
220
|
let i = 0;
|
|
213
221
|
const startsSeg = (at) => at === 0 || s.slice(0, at).endsWith(' · ') || /^\s*$/.test(s.slice(0, at));
|
|
222
|
+
if (/[⟦⟪]/.test(s)) { // explicit markup: `command` and «value» are atomic spans, the rest is auto-tokenised
|
|
223
|
+
let j = 0;
|
|
224
|
+
for (const m of s.matchAll(MARK)) {
|
|
225
|
+
for (const r of paintRuns(s.slice(j, m.index), base)) push(r.text, r.role);
|
|
226
|
+
push(m[1] ?? m[2], m[1] !== undefined ? 'command' : 'id');
|
|
227
|
+
j = m.index + m[0].length;
|
|
228
|
+
}
|
|
229
|
+
for (const r of paintRuns(s.slice(j), base)) push(r.text, r.role);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
214
232
|
for (const m of s.matchAll(TOKEN)) {
|
|
215
233
|
proseRuns(s.slice(i, m.index), base, push, startsSeg(i));
|
|
216
234
|
const g = m.findIndex((x, k) => k > 0 && x !== undefined);
|
|
@@ -222,32 +240,47 @@ export function paintRuns(text, base) {
|
|
|
222
240
|
}
|
|
223
241
|
function proseRuns(seg, base, push, segStart) {
|
|
224
242
|
const k = segStart && seg.match(LETTER_KEY);
|
|
225
|
-
if (k) { push(k[1], '
|
|
243
|
+
if (k) { push(k[1], 'command'); seg = seg.slice(1); }
|
|
226
244
|
push(seg, base);
|
|
227
245
|
}
|
|
228
246
|
|
|
229
247
|
/**
|
|
230
248
|
* A note/list line as runs: a leading state glyph, then `Label value` (two spaces) or `label: value`,
|
|
231
|
-
* then tokens. tone: undefined/'dim' (neutral), 'ok', 'warn', 'accent', 'error'.
|
|
249
|
+
* then tokens. tone: undefined/'dim' (neutral), 'ok', 'warn', 'accent', 'error', 'ask' (a question for the human).
|
|
232
250
|
*/
|
|
233
251
|
export function paintLine(text, tone) {
|
|
234
252
|
const s = String(text ?? '');
|
|
235
253
|
const out = [];
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
254
|
+
// The tone colours the GLYPH only. The sentence stays prose, so commands and values inside keep their roles.
|
|
255
|
+
const TONE_GLYPH = { warn: 'warn', error: 'error', fail: 'error', ok: 'ok', accent: 'brand', ask: 'ask' };
|
|
256
|
+
// [\s\S]* not .*: a notice can carry a newline (a server error body), and a failed match crashed the TUI
|
|
257
|
+
// ("m is not iterable", 2026-10-15 03:44 while the collector was down).
|
|
258
|
+
const m = s.match(/^(\s*)(?:([●✔◐○?✖⚠⊘◉Ⓠ▸⛔↳»·⬢⬡▶])( ))?([\s\S]*)$/u) || [s, '', undefined, undefined, s];
|
|
239
259
|
const [, lead, glyph, gsp, rest0] = m;
|
|
240
260
|
if (lead) out.push({ text: lead, role: undefined });
|
|
241
|
-
if (glyph) out.push({ text: glyph, role:
|
|
261
|
+
if (glyph) out.push({ text: glyph, role: ['ok', 'warn', 'error'].includes(GLYPH_ROLE[glyph]) ? GLYPH_ROLE[glyph] : (TONE_GLYPH[tone] || GLYPH_ROLE[glyph]) }, { text: gsp, role: undefined });
|
|
242
262
|
let rest = rest0;
|
|
263
|
+
if (tone === 'ask') { // QUOXON-ASK: "▶ reason. question? keys": the keys after the last "? " are the ask colour
|
|
264
|
+
const q = rest.lastIndexOf('? ');
|
|
265
|
+
if (q >= 0) { out.push(...paintRuns(rest.slice(0, q + 2), 'prose'), ...paintRuns(rest.slice(q + 2), 'ask')); return out; }
|
|
266
|
+
out.push(...paintRuns(rest, 'prose')); return out;
|
|
267
|
+
}
|
|
243
268
|
const lv = rest.match(/^(\S[^·]{0,30}?\S|\S)( {2,})(\S.*)$/) || rest.match(/^([A-Za-z][\w .-]{0,22}:)( )(\S.*)$/);
|
|
244
|
-
|
|
269
|
+
const valueBase = 'prose';
|
|
245
270
|
if (lv && !/^https?:/.test(lv[1])) {
|
|
246
271
|
const pv = providerOf(lv[1].replace(/\s*\(.*\)$/, ''), null);
|
|
247
272
|
out.push({ text: lv[1], role: pv ? `provider:${pv}` : 'label' }, { text: lv[2], role: undefined });
|
|
248
273
|
rest = lv[3];
|
|
249
|
-
if (!prose) valueBase = 'value';
|
|
250
274
|
}
|
|
251
275
|
out.push(...paintRuns(rest, valueBase));
|
|
252
276
|
return out;
|
|
253
277
|
}
|
|
278
|
+
|
|
279
|
+
/** The glyph a bare notice line gets so its tone shows without tinting the sentence. '' for neutral tones. */
|
|
280
|
+
export const TONE_PREFIX = { warn: '⚠ ', error: '✖ ', fail: '✖ ', ok: '✔ ' };
|
|
281
|
+
export const toneText = (text, tone) => {
|
|
282
|
+
const p = TONE_PREFIX[tone];
|
|
283
|
+
const t = String(text ?? '');
|
|
284
|
+
return p && !/^\s*[●✔◐○?✖⚠⊘◉Ⓠ▸⛔↳»·⬢⬡▶] /u.test(t) ? p + t : t;
|
|
285
|
+
};
|
|
286
|
+
export { stripMarks };
|