rewritable 0.7.0 → 0.8.0
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 +21 -1
- package/bin/rwa.mjs +47 -3
- package/package.json +1 -1
- package/seeds/rewritable.html +856 -44
- package/src/identity.mjs +1 -0
- package/src/seed.mjs +84 -1
- package/src/workspace.mjs +262 -0
package/seeds/rewritable.html
CHANGED
|
@@ -40,7 +40,11 @@
|
|
|
40
40
|
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0;}
|
|
41
41
|
[hidden]{display:none!important;}
|
|
42
42
|
body{font-family:var(--font-ui);background:var(--bg);color:var(--text);min-height:100dvh;padding-bottom:160px;line-height:1.5;-webkit-font-smoothing:antialiased;}
|
|
43
|
-
#rwa-set{position:fixed;top:12px;right:12px;display:flex;gap:6px;z-index:1000;}
|
|
43
|
+
#rwa-set{position:fixed;top:12px;right:12px;display:flex;gap:6px;align-items:center;z-index:1000;}
|
|
44
|
+
#rwa-mode-tabs{display:flex;gap:2px;align-items:center;background:var(--white);border:1px solid var(--gray-200);border-radius:8px;padding:2px;box-shadow:0 1px 4px rgba(0,0,0,0.04);}
|
|
45
|
+
.rwa-mode-tab{background:transparent;border:0;color:var(--gray-500);font-family:var(--font-mono);font-size:10px;padding:5px 8px;border-radius:6px;cursor:pointer;letter-spacing:.4px;text-transform:uppercase;transition:color .15s,background .15s;}
|
|
46
|
+
.rwa-mode-tab:hover{color:var(--gray-900);background:var(--gray-50);}
|
|
47
|
+
.rwa-mode-tab.on{background:var(--gray-900);color:var(--white);}
|
|
44
48
|
.rwa-st-btn{background:var(--white);border:1px solid var(--gray-200);color:var(--gray-500);font-family:var(--font-mono);font-size:10px;padding:6px 10px;border-radius:6px;cursor:pointer;letter-spacing:.5px;text-transform:uppercase;transition:color .15s,background .15s,border-color .15s;}
|
|
45
49
|
.rwa-st-btn:hover{color:var(--gray-900);border-color:var(--gray-300);background:var(--gray-50);}
|
|
46
50
|
.rwa-st-btn.dirty{color:var(--gray-900);border-color:var(--gray-300);background:var(--gray-100);}
|
|
@@ -49,16 +53,16 @@ body{font-family:var(--font-ui);background:var(--bg);color:var(--text);min-heigh
|
|
|
49
53
|
.rwa-st-btn.run{color:var(--gray-700);border-color:var(--gray-300);background:var(--gray-50);}
|
|
50
54
|
.rwa-st-btn.err{color:var(--red);border-color:var(--red);background:var(--white);}
|
|
51
55
|
.rwa-st-btn.ok{color:var(--green);border-color:var(--green);background:var(--white);}
|
|
52
|
-
#rwa-set-panel{position:fixed;top:50px;right:12px;background:var(--white);border:1px solid var(--gray-200);border-radius:var(--radius-sm);padding:14px;display:none;
|
|
56
|
+
#rwa-set-panel{position:fixed;top:50px;right:12px;background:var(--white);border:1px solid var(--gray-200);border-radius:var(--radius-sm);padding:14px;display:none;width:340px;max-width:calc(100vw - 24px);z-index:999;box-shadow:0 4px 16px rgba(0,0,0,0.08);}
|
|
53
57
|
#rwa-set-panel.open{display:block;}
|
|
54
58
|
.rwa-set-row{display:flex;flex-direction:column;gap:4px;margin-bottom:10px;}
|
|
55
59
|
.rwa-set-row:last-child{margin-bottom:0;}
|
|
56
60
|
.rwa-set-row label{font-family:var(--font-mono);font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:var(--gray-500);}
|
|
57
|
-
.rwa-set-row input,.rwa-set-row select{background:var(--white);border:1px solid var(--gray-200);color:var(--gray-900);font-family:var(--font-ui);font-size:13px;padding:8px 10px;outline:none;border-radius:8px;transition:border-color .15s;}
|
|
61
|
+
.rwa-set-row input,.rwa-set-row select{width:100%;background:var(--white);border:1px solid var(--gray-200);color:var(--gray-900);font-family:var(--font-ui);font-size:13px;padding:8px 10px;outline:none;border-radius:8px;transition:border-color .15s;}
|
|
58
62
|
.rwa-set-row input:focus,.rwa-set-row select:focus{border-color:var(--gray-400);}
|
|
59
63
|
.rwa-set-row select{appearance:none;-webkit-appearance:none;cursor:pointer;}
|
|
60
64
|
.rwa-set-base-url-line{display:flex;gap:6px;}
|
|
61
|
-
.rwa-set-base-url-line input{flex:1;}
|
|
65
|
+
.rwa-set-base-url-line input{flex:1;min-width:0;}
|
|
62
66
|
.rwa-set-base-url-line button{background:var(--gray-100);border:1px solid var(--gray-200);border-radius:8px;padding:6px 12px;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--gray-700);}
|
|
63
67
|
.rwa-set-base-url-line button:hover{background:var(--gray-200);}
|
|
64
68
|
.rwa-set-hint{font-family:var(--font-mono);font-size:10px;line-height:1.5;color:var(--gray-500);}
|
|
@@ -99,6 +103,28 @@ body{font-family:var(--font-ui);background:var(--bg);color:var(--text);min-heigh
|
|
|
99
103
|
.rwa-share-actions button{font:inherit;font-size:12px;padding:5px 10px;border:1px solid var(--gray-300);border-radius:8px;background:var(--white);color:var(--gray-900);cursor:pointer;}
|
|
100
104
|
.rwa-share-actions button:disabled{opacity:.5;cursor:default;}
|
|
101
105
|
#rwa-share-create,#rwa-share-update{background:var(--gray-900);color:var(--white);border-color:var(--gray-900);}
|
|
106
|
+
#rwa-mode-panel{position:fixed;top:50px;right:12px;background:var(--white);border:1px solid var(--gray-200);border-radius:var(--radius-sm);padding:18px 20px;display:none;width:min(420px,92vw);max-height:calc(100dvh - 72px);overflow:auto;z-index:999;box-shadow:0 4px 16px rgba(0,0,0,0.08);font-family:var(--font-ui);font-size:13px;line-height:1.5;color:var(--gray-700);}
|
|
107
|
+
#rwa-mode-panel.open{display:block;}
|
|
108
|
+
#rwa-mode-panel h4{margin:0 0 8px;font-size:15px;color:var(--gray-900);font-weight:600;}
|
|
109
|
+
#rwa-mode-panel p{margin:0 0 10px;}
|
|
110
|
+
.rwa-mode-section{border-top:1px solid var(--gray-100);padding-top:12px;margin-top:12px;}
|
|
111
|
+
.rwa-mode-section:first-child{border-top:0;padding-top:0;margin-top:0;}
|
|
112
|
+
.rwa-mode-kicker{font-family:var(--font-mono);font-size:9px;letter-spacing:1.5px;text-transform:uppercase;color:var(--gray-500);margin-bottom:6px;}
|
|
113
|
+
.rwa-mode-empty{color:var(--gray-400);font-style:italic;padding:6px 0;}
|
|
114
|
+
.rwa-mode-actions{display:flex;gap:6px;flex-wrap:wrap;}
|
|
115
|
+
.rwa-mode-actions button,.rwa-mode-row button{font:inherit;font-size:12px;padding:5px 10px;border:1px solid var(--gray-300);border-radius:8px;background:var(--white);color:var(--gray-900);cursor:pointer;}
|
|
116
|
+
.rwa-mode-actions button.pri{background:var(--gray-900);color:var(--white);border-color:var(--gray-900);}
|
|
117
|
+
.rwa-mode-actions button:disabled,.rwa-mode-row button:disabled{opacity:.5;cursor:default;}
|
|
118
|
+
.rwa-mode-row{display:flex;align-items:flex-start;justify-content:space-between;gap:10px;padding:9px 0;border-bottom:1px solid var(--gray-100);}
|
|
119
|
+
.rwa-mode-row:last-child{border-bottom:0;}
|
|
120
|
+
.rwa-mode-title{font-weight:600;color:var(--gray-900);}
|
|
121
|
+
.rwa-mode-meta{font-family:var(--font-mono);font-size:10px;color:var(--gray-500);word-break:break-all;}
|
|
122
|
+
.rwa-mode-invoke{display:flex;gap:6px;margin-top:8px;}
|
|
123
|
+
.rwa-mode-invoke input{flex:1;min-width:0;border:1px solid var(--gray-200);border-radius:8px;padding:6px 8px;font:inherit;font-size:12px;}
|
|
124
|
+
.rwa-mode-result{margin-top:8px;font-family:var(--font-mono);font-size:10px;background:var(--gray-50);border-radius:6px;padding:6px 8px;white-space:pre-wrap;word-break:break-word;color:var(--gray-600);}
|
|
125
|
+
.rwa-mode-hist{padding:8px 0;border-bottom:1px solid var(--gray-100);}
|
|
126
|
+
.rwa-mode-hist:last-child{border-bottom:0;}
|
|
127
|
+
.rwa-mode-hist .rwa-mode-meta{margin-bottom:3px;}
|
|
102
128
|
.rwa-set-hint code{background:var(--gray-100);padding:1px 4px;border-radius:3px;font-size:10px;}
|
|
103
129
|
.rwa-set-hint.ok{color:#15803d;}
|
|
104
130
|
.rwa-set-hint.err{color:#b91c1c;}
|
|
@@ -164,6 +190,25 @@ body{font-family:var(--font-ui);background:var(--bg);color:var(--text);min-heigh
|
|
|
164
190
|
#rwa-img-chip button{width:22px;height:22px;border:0;border-radius:6px;background:transparent;color:var(--gray-900);font-size:11px;line-height:1;cursor:pointer;padding:0;font-family:var(--font-ui);}
|
|
165
191
|
#rwa-img-chip button:hover{background:var(--gray-100);}
|
|
166
192
|
#rwa-img-chip button.on{background:var(--gray-900);color:var(--white);}
|
|
193
|
+
#rwa-selection-bar{position:absolute;z-index:70;display:flex;align-items:center;gap:4px;padding:5px;border-radius:10px;border:1px solid var(--gray-200);background:var(--white);box-shadow:0 8px 24px rgba(0,0,0,0.12);font-family:var(--font-ui);}
|
|
194
|
+
#rwa-selection-bar[hidden]{display:none!important;}
|
|
195
|
+
#rwa-selection-bar button{height:28px;border:0;border-radius:7px;background:transparent;color:var(--gray-700);font:600 12px var(--font-ui);cursor:pointer;padding:0 9px;}
|
|
196
|
+
#rwa-selection-bar button:hover{background:var(--gray-100);color:var(--gray-900);}
|
|
197
|
+
#rwa-selection-bar button.pri{background:var(--gray-900);color:var(--white);}
|
|
198
|
+
#rwa-selection-bar[data-listening="1"] #rwa-selection-voice{background:var(--red);color:var(--white);}
|
|
199
|
+
#rwa-selection-cmd{width:170px;border:1px solid var(--gray-200);border-radius:7px;padding:6px 8px;outline:none;font:12px var(--font-ui);color:var(--gray-900);}
|
|
200
|
+
#rwa-selection-cmd:focus{border-color:var(--gray-400);}
|
|
201
|
+
body:not([data-rwa-mode="edit"]) #rwa-lens,
|
|
202
|
+
body:not([data-rwa-mode="edit"]) #rwa-pal,
|
|
203
|
+
body:not([data-rwa-mode="edit"]) #rwa-set-panel,
|
|
204
|
+
body:not([data-rwa-mode="edit"]) #rwa-skin-panel,
|
|
205
|
+
body:not([data-rwa-mode="edit"]) #rwa-lens-hist-panel,
|
|
206
|
+
body:not([data-rwa-mode="edit"]) #rwa-st-cog,
|
|
207
|
+
body:not([data-rwa-mode="edit"]) #rwa-st-skin,
|
|
208
|
+
body:not([data-rwa-mode="edit"]) #rwa-img-chip,
|
|
209
|
+
body:not([data-rwa-mode="edit"]) #rwa-selection-bar{display:none!important;}
|
|
210
|
+
body:not([data-rwa-mode="document"]) #rwa-view-toggle,
|
|
211
|
+
body:not([data-rwa-mode="document"]) #rwa-view-chrome{display:none!important;}
|
|
167
212
|
/* images-v1: deterministic width presets (set by the toolbar; class lives in the doc) */
|
|
168
213
|
:where(#rwa-doc-mount) figure.rwa-img-sm{max-width:33%;}
|
|
169
214
|
:where(#rwa-doc-mount) figure.rwa-img-md{max-width:66%;}
|
|
@@ -176,6 +221,10 @@ body{font-family:var(--font-ui);background:var(--bg);color:var(--text);min-heigh
|
|
|
176
221
|
.rwa-frag-pulse{animation:rwa-frag-pulse 1.6s ease-out;}
|
|
177
222
|
@keyframes rwa-frag-pulse{0%{background:rgba(59,130,246,0.22);}100%{background:transparent;}}
|
|
178
223
|
[data-rwa-anchored]{outline:2px solid var(--gray-900);outline-offset:2px;border-radius:4px;}
|
|
224
|
+
.rwa-editable-leaf{cursor:text;border-radius:4px;transition:background .12s ease,box-shadow .12s ease,outline-color .12s ease;}
|
|
225
|
+
.rwa-editable-leaf:hover{background:rgba(59,130,246,0.045);box-shadow:0 0 0 3px rgba(59,130,246,0.08);}
|
|
226
|
+
.rwa-editable-leaf:focus-visible{outline:2px solid rgba(59,130,246,0.5);outline-offset:2px;}
|
|
227
|
+
.rwa-editable-leaf[contenteditable="true"]{background:rgba(59,130,246,0.07);box-shadow:0 0 0 2px rgba(59,130,246,0.22);outline:none;caret-color:var(--blue);}
|
|
179
228
|
/* Inline prompt mode — the block's text starts with "/" (addressing the model,
|
|
180
229
|
not writing content). Tint + inset accent bar only: no border/padding so the
|
|
181
230
|
per-keystroke toggle never shifts layout (caret stays put), and no ::before
|
|
@@ -851,12 +900,13 @@ async function runtimeFsList(prefix) {
|
|
|
851
900
|
const runtimeEvents = {
|
|
852
901
|
commit: new Set(),
|
|
853
902
|
modify: new Set(),
|
|
903
|
+
mode: new Set(),
|
|
854
904
|
status: new Set(),
|
|
855
905
|
};
|
|
856
906
|
|
|
857
907
|
function runtimeOn(event, callback) {
|
|
858
908
|
if (!Object.prototype.hasOwnProperty.call(runtimeEvents, event)) {
|
|
859
|
-
throw new Error("unknown event '" + event + "' (use 'commit', 'modify', or 'status')");
|
|
909
|
+
throw new Error("unknown event '" + event + "' (use 'commit', 'modify', 'mode', or 'status')");
|
|
860
910
|
}
|
|
861
911
|
if (typeof callback !== 'function') throw new TypeError('callback must be a function');
|
|
862
912
|
runtimeEvents[event].add(callback);
|
|
@@ -876,6 +926,147 @@ function emitRuntimeEvent(event, payload) {
|
|
|
876
926
|
}
|
|
877
927
|
}
|
|
878
928
|
|
|
929
|
+
// === Cross-container bus (workspace-presence subset) ========================
|
|
930
|
+
// BroadcastChannel-backed pub/sub across same-origin rewritables. This is a
|
|
931
|
+
// deliberately small public surface: documents can publish/subscribe to normal
|
|
932
|
+
// topics, while runtime-reserved prefixes stay off-limits. The first concrete
|
|
933
|
+
// consumer is workspace autodiscovery on `workspace:presence`.
|
|
934
|
+
const runtimeBusChannels = new Map();
|
|
935
|
+
|
|
936
|
+
function assertRuntimeBusTopic(topic) {
|
|
937
|
+
if (typeof topic !== 'string' || !topic) throw new TypeError('bus topic must be a non-empty string');
|
|
938
|
+
if (topic.length > 128 || !/^[A-Za-z0-9][A-Za-z0-9:_./-]*$/.test(topic)) {
|
|
939
|
+
throw new TypeError('bus topic contains unsupported characters');
|
|
940
|
+
}
|
|
941
|
+
if (/^(?:rwa[:_]|skills:)/.test(topic)) {
|
|
942
|
+
throw new RwaReservedError(topic);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
function runtimeBusChannel(topic) {
|
|
947
|
+
assertRuntimeBusTopic(topic);
|
|
948
|
+
if (typeof BroadcastChannel === 'undefined') throw new Error('BroadcastChannel is not available');
|
|
949
|
+
let ch = runtimeBusChannels.get(topic);
|
|
950
|
+
if (!ch) {
|
|
951
|
+
ch = new BroadcastChannel('rwa_bus:' + topic);
|
|
952
|
+
if (typeof ch.unref === 'function') ch.unref();
|
|
953
|
+
runtimeBusChannels.set(topic, ch);
|
|
954
|
+
}
|
|
955
|
+
return ch;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function runtimeBusPublish(topic, message) {
|
|
959
|
+
const envelope = { topic, from: DOC_UUID, at: Date.now(), message };
|
|
960
|
+
runtimeBusChannel(topic).postMessage(envelope);
|
|
961
|
+
return envelope;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function runtimeBusSubscribe(topic, callback) {
|
|
965
|
+
assertRuntimeBusTopic(topic);
|
|
966
|
+
if (typeof callback !== 'function') throw new TypeError('callback must be a function');
|
|
967
|
+
if (typeof BroadcastChannel === 'undefined') throw new Error('BroadcastChannel is not available');
|
|
968
|
+
const ch = new BroadcastChannel('rwa_bus:' + topic);
|
|
969
|
+
if (typeof ch.unref === 'function') ch.unref();
|
|
970
|
+
const handler = (evt) => {
|
|
971
|
+
const data = evt && evt.data;
|
|
972
|
+
if (!data || data.from === DOC_UUID) return;
|
|
973
|
+
try { callback(data); } catch (_) { /* never let a subscriber break delivery */ }
|
|
974
|
+
};
|
|
975
|
+
ch.addEventListener('message', handler);
|
|
976
|
+
return () => { ch.removeEventListener('message', handler); ch.close(); };
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
// === First-class runtime modes =============================================
|
|
980
|
+
// Modes are page-load local state, deliberately not serialized into INLINE_DOC.
|
|
981
|
+
// Document is the default every time a file opens; Edit is the only mode that
|
|
982
|
+
// attaches WYSIWYG/lens/image editing affordances.
|
|
983
|
+
const RWA_MODES = Object.freeze(['document', 'edit', 'skills', 'actions']);
|
|
984
|
+
let rwaMode = 'document';
|
|
985
|
+
|
|
986
|
+
function escRuntimeHtml(s) {
|
|
987
|
+
return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function isRwaSkillHost() {
|
|
991
|
+
return PRODUCT_KIND === 'skill-host' && !!document.getElementById('rwa-skills');
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function closeRuntimePanels() {
|
|
995
|
+
for (const id of ['rwa-set-panel', 'rwa-info-panel', 'rwa-skin-panel', 'rwa-share-panel', 'rwa-mode-panel']) {
|
|
996
|
+
const el = document.getElementById(id);
|
|
997
|
+
if (el) el.classList.remove('open');
|
|
998
|
+
}
|
|
999
|
+
const hist = document.getElementById('rwa-lens-hist-panel');
|
|
1000
|
+
if (hist) hist.hidden = true;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function hideEditTransients() {
|
|
1004
|
+
if (typeof exitInlineEdit === 'function') exitInlineEdit();
|
|
1005
|
+
if (typeof releaseAnchor === 'function') releaseAnchor();
|
|
1006
|
+
if (typeof closePal === 'function') closePal();
|
|
1007
|
+
if (typeof clearDropMark === 'function') clearDropMark();
|
|
1008
|
+
if (typeof hideSelectionCommandBar === 'function') hideSelectionCommandBar();
|
|
1009
|
+
const chip = document.getElementById('rwa-img-chip');
|
|
1010
|
+
if (chip) chip.hidden = true;
|
|
1011
|
+
const hist = document.getElementById('rwa-lens-hist-panel');
|
|
1012
|
+
if (hist) hist.hidden = true;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
function syncModeChrome() {
|
|
1016
|
+
if (document.body) document.body.dataset.rwaMode = rwaMode;
|
|
1017
|
+
document.querySelectorAll('[data-rwa-mode-target]').forEach(btn => {
|
|
1018
|
+
btn.classList.toggle('on', btn.dataset.rwaModeTarget === rwaMode);
|
|
1019
|
+
});
|
|
1020
|
+
const panel = document.getElementById('rwa-mode-panel');
|
|
1021
|
+
if (!panel) return;
|
|
1022
|
+
if (rwaMode === 'skills' || rwaMode === 'actions') {
|
|
1023
|
+
panel.classList.add('open');
|
|
1024
|
+
renderModePanel().catch(err => {
|
|
1025
|
+
panel.innerHTML = '<h4>' + escRuntimeHtml(rwaMode) + '</h4><p>Could not render this panel: ' + escRuntimeHtml(err?.message || err) + '</p>';
|
|
1026
|
+
});
|
|
1027
|
+
} else {
|
|
1028
|
+
panel.classList.remove('open');
|
|
1029
|
+
}
|
|
1030
|
+
if (typeof syncViewChrome === 'function') syncViewChrome();
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function runtimeSetMode(mode) {
|
|
1034
|
+
if (!RWA_MODES.includes(mode)) throw new Error('unknown mode: ' + mode);
|
|
1035
|
+
if (modifyMutex) {
|
|
1036
|
+
setStatus('err', '✗ modify in progress');
|
|
1037
|
+
throw new Error('cannot switch mode during modify');
|
|
1038
|
+
}
|
|
1039
|
+
if (mode === rwaMode) {
|
|
1040
|
+
syncModeChrome();
|
|
1041
|
+
return rwaMode;
|
|
1042
|
+
}
|
|
1043
|
+
if (rwaMode === 'edit' || mode !== 'edit') hideEditTransients();
|
|
1044
|
+
closeRuntimePanels();
|
|
1045
|
+
if (mode === 'edit' && activeView) {
|
|
1046
|
+
activeView = null;
|
|
1047
|
+
try { sessionStorage.setItem(rwaViewKey(), ''); } catch (_) {}
|
|
1048
|
+
}
|
|
1049
|
+
rwaMode = mode;
|
|
1050
|
+
syncModeChrome();
|
|
1051
|
+
emitRuntimeEvent('mode', { mode: rwaMode });
|
|
1052
|
+
getDoc().then(d => renderDoc(canonLF(d))).catch(() => {});
|
|
1053
|
+
return rwaMode;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
async function renderModePanel() {
|
|
1057
|
+
const panel = document.getElementById('rwa-mode-panel');
|
|
1058
|
+
if (!panel) return;
|
|
1059
|
+
if (rwaMode === 'skills') return renderSkillsModePanel(panel);
|
|
1060
|
+
if (rwaMode === 'actions') return renderActionsModePanel(panel);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function attachModeTabs() {
|
|
1064
|
+
document.querySelectorAll('[data-rwa-mode-target]').forEach(btn => {
|
|
1065
|
+
btn.addEventListener('click', () => runtimeSetMode(btn.dataset.rwaModeTarget));
|
|
1066
|
+
});
|
|
1067
|
+
syncModeChrome();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
879
1070
|
// === Status surface (spec §7) ==============================================
|
|
880
1071
|
// FSA permission state. Tracked here rather than re-derived each call because
|
|
881
1072
|
// the permission events that move it (showSaveFilePicker, queryPermission,
|
|
@@ -1019,6 +1210,7 @@ function renderDoc(html) {
|
|
|
1019
1210
|
// the agent's view of the document is invisible-by-construction to any render mode.
|
|
1020
1211
|
setSourceMap(html);
|
|
1021
1212
|
rebuildLockedRanges(html);
|
|
1213
|
+
const editReady = rwaMode === 'edit' && !activeView;
|
|
1022
1214
|
// rwa-lens/1: click-to-anchor (Task 5.1). renderDoc runs on every commit and
|
|
1023
1215
|
// on bootstrap; remove first so listeners don't multiply across renders. Same
|
|
1024
1216
|
// function reference, so removing the previous instance is safe. With a
|
|
@@ -1026,12 +1218,22 @@ function renderDoc(html) {
|
|
|
1026
1218
|
// listener is removed and not re-added (§5.10). For activeView===null this is
|
|
1027
1219
|
// byte-identical to the previous behavior.
|
|
1028
1220
|
m.removeEventListener('click', handleMountClick);
|
|
1029
|
-
if (
|
|
1030
|
-
// rwa inline-edit:
|
|
1221
|
+
if (editReady) m.addEventListener('click', handleMountClick);
|
|
1222
|
+
// rwa inline-edit: leaf text blocks behave like page text in an editor.
|
|
1223
|
+
// Pointer-down starts editing early enough for the browser to place the caret
|
|
1224
|
+
// where the user clicked; dblclick stays registered for old tests/muscle
|
|
1225
|
+
// memory; keydown gives keyboard users a non-pointer entry path. Same
|
|
1031
1226
|
// remove-then-add discipline as click-to-anchor; not registered under an
|
|
1032
1227
|
// active view (byte-offset resolution is undefined there).
|
|
1228
|
+
if (typeof refreshInlineEditAffordances === 'function') refreshInlineEditAffordances(m);
|
|
1229
|
+
m.removeEventListener('pointerdown', handleMountPointerDown);
|
|
1033
1230
|
m.removeEventListener('dblclick', handleMountDblClick);
|
|
1034
|
-
|
|
1231
|
+
m.removeEventListener('keydown', handleMountEditKeydown);
|
|
1232
|
+
if (editReady) {
|
|
1233
|
+
m.addEventListener('pointerdown', handleMountPointerDown);
|
|
1234
|
+
m.addEventListener('dblclick', handleMountDblClick);
|
|
1235
|
+
m.addEventListener('keydown', handleMountEditKeydown);
|
|
1236
|
+
}
|
|
1035
1237
|
// images-v1: drag-drop insert + hover-✕ chip. Same remove-then-add
|
|
1036
1238
|
// discipline; inert under a render mode (byte-offset resolution undefined).
|
|
1037
1239
|
// The chip/drop-mark reference DOM the innerHTML reset just destroyed.
|
|
@@ -1043,7 +1245,7 @@ function renderDoc(html) {
|
|
|
1043
1245
|
m.removeEventListener('drop', handleMountDrop);
|
|
1044
1246
|
m.removeEventListener('mouseover', handleMountImgOver);
|
|
1045
1247
|
m.removeEventListener('mouseout', handleMountImgOut);
|
|
1046
|
-
if (
|
|
1248
|
+
if (editReady) {
|
|
1047
1249
|
m.addEventListener('dragover', handleMountDragOver);
|
|
1048
1250
|
m.addEventListener('dragleave', handleMountDragLeave);
|
|
1049
1251
|
m.addEventListener('drop', handleMountDrop);
|
|
@@ -1056,6 +1258,136 @@ function renderDoc(html) {
|
|
|
1056
1258
|
if (activeView && typeof activeView.mounted === 'function') {
|
|
1057
1259
|
activeView.mounted(m, viewCtx());
|
|
1058
1260
|
}
|
|
1261
|
+
if (PRODUCT_KIND === 'workspace' && typeof renderWorkspacePresence === 'function') {
|
|
1262
|
+
renderWorkspacePresence();
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// ─── Workspace presence over runtime.bus ─────────────────────────────
|
|
1267
|
+
const RWA_WORKSPACE_PRESENCE_TOPIC = 'workspace:presence';
|
|
1268
|
+
let workspacePresenceTimer = null;
|
|
1269
|
+
let workspacePresenceUnsub = null;
|
|
1270
|
+
let workspaceMonitorUnsub = null;
|
|
1271
|
+
const workspacePeers = new Map();
|
|
1272
|
+
|
|
1273
|
+
function rwaLocationSansHash() {
|
|
1274
|
+
try {
|
|
1275
|
+
const u = new URL(location.href);
|
|
1276
|
+
u.hash = '';
|
|
1277
|
+
return u.href;
|
|
1278
|
+
} catch (_) {
|
|
1279
|
+
return String(location.href || '').split('#')[0];
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function rwaDirectoryHref(href) {
|
|
1284
|
+
try { return new URL('.', href || location.href).href; }
|
|
1285
|
+
catch (_) { return ''; }
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function sameWorkspaceDirectory(href) {
|
|
1289
|
+
const mine = rwaDirectoryHref(rwaLocationSansHash());
|
|
1290
|
+
const theirs = rwaDirectoryHref(href);
|
|
1291
|
+
return !!mine && !!theirs && mine === theirs;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function workspacePresencePayload(action) {
|
|
1295
|
+
const d = runtimeDescribe();
|
|
1296
|
+
return {
|
|
1297
|
+
schema: 'rwa-presence/1',
|
|
1298
|
+
action,
|
|
1299
|
+
uuid: DOC_UUID,
|
|
1300
|
+
kind: PRODUCT_KIND,
|
|
1301
|
+
title: d.title || document.title || RWA.FILE,
|
|
1302
|
+
file: RWA.FILE,
|
|
1303
|
+
url: rwaLocationSansHash(),
|
|
1304
|
+
affordances: Array.isArray(d.affordances) ? d.affordances.map(a => ({ kind: a.kind, name: a.name, label: a.label, provenance: a.provenance })) : [],
|
|
1305
|
+
};
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function workspaceManifestDocs() {
|
|
1309
|
+
const el = document.getElementById('rwa-workspace');
|
|
1310
|
+
if (!el) return [];
|
|
1311
|
+
try {
|
|
1312
|
+
const parsed = JSON.parse(el.textContent || '{}');
|
|
1313
|
+
return Array.isArray(parsed.documents) ? parsed.documents : [];
|
|
1314
|
+
} catch (_) {
|
|
1315
|
+
return [];
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function workspaceKnownKeys() {
|
|
1320
|
+
const keys = new Set();
|
|
1321
|
+
for (const d of workspaceManifestDocs()) {
|
|
1322
|
+
if (d && d.uuid) keys.add('uuid:' + d.uuid);
|
|
1323
|
+
if (d && d.file) keys.add('file:' + d.file);
|
|
1324
|
+
}
|
|
1325
|
+
return keys;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function workspacePeerKnown(peer, known) {
|
|
1329
|
+
return !!(peer && ((peer.uuid && known.has('uuid:' + peer.uuid)) || (peer.file && known.has('file:' + peer.file))));
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function workspacePresenceCard(peer, known) {
|
|
1333
|
+
const isKnown = workspacePeerKnown(peer, known);
|
|
1334
|
+
const href = peer.url || '#';
|
|
1335
|
+
const aff = peer.affordances && peer.affordances.length ? peer.affordances.map(a => a.kind).join(', ') : 'baseline';
|
|
1336
|
+
return '<a class="rwa-ws-card rwa-ws-live-card" href="' + escRuntimeHtml(href) + '">' +
|
|
1337
|
+
'<span class="rwa-ws-kind">' + escRuntimeHtml(peer.kind || 'document') + '</span>' +
|
|
1338
|
+
'<strong>' + escRuntimeHtml(peer.title || peer.file || 'Untitled') + '</strong>' +
|
|
1339
|
+
'<span>' + escRuntimeHtml(peer.file || peer.url || peer.uuid || '') + '</span>' +
|
|
1340
|
+
'<small>' + (isKnown ? 'indexed' : 'new since sync') + ' · open now · ' + escRuntimeHtml(aff) + '</small>' +
|
|
1341
|
+
'</a>';
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function renderWorkspacePresence() {
|
|
1345
|
+
if (PRODUCT_KIND !== 'workspace') return;
|
|
1346
|
+
const root = document.querySelector('[data-rwa-workspace-live]');
|
|
1347
|
+
if (!root) return;
|
|
1348
|
+
const grid = root.querySelector('[data-rwa-workspace-live-grid]');
|
|
1349
|
+
if (!grid) return;
|
|
1350
|
+
const cutoff = Date.now() - 45000;
|
|
1351
|
+
const peers = [...workspacePeers.values()]
|
|
1352
|
+
.filter(p => p && p.lastSeen >= cutoff && sameWorkspaceDirectory(p.url))
|
|
1353
|
+
.sort((a, b) => String(a.title || a.file || '').localeCompare(String(b.title || b.file || '')));
|
|
1354
|
+
root.hidden = peers.length === 0;
|
|
1355
|
+
if (!peers.length) { grid.innerHTML = ''; return; }
|
|
1356
|
+
const known = workspaceKnownKeys();
|
|
1357
|
+
grid.innerHTML = peers.map(p => workspacePresenceCard(p, known)).join('');
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function handleWorkspacePresence(envelope) {
|
|
1361
|
+
const msg = envelope && envelope.message;
|
|
1362
|
+
if (!msg || msg.schema !== 'rwa-presence/1') return;
|
|
1363
|
+
if (msg.action === 'request') {
|
|
1364
|
+
runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('hello'));
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
if (PRODUCT_KIND !== 'workspace' || msg.action !== 'hello' || !sameWorkspaceDirectory(msg.url)) return;
|
|
1368
|
+
workspacePeers.set(msg.uuid || envelope.from, { ...msg, lastSeen: Date.now() });
|
|
1369
|
+
renderWorkspacePresence();
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function startWorkspacePresence() {
|
|
1373
|
+
try {
|
|
1374
|
+
workspacePresenceUnsub = runtimeBusSubscribe(RWA_WORKSPACE_PRESENCE_TOPIC, handleWorkspacePresence);
|
|
1375
|
+
runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('hello'));
|
|
1376
|
+
if (!(typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent || ''))) {
|
|
1377
|
+
workspacePresenceTimer = setInterval(() => {
|
|
1378
|
+
runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('hello'));
|
|
1379
|
+
if (PRODUCT_KIND === 'workspace') renderWorkspacePresence();
|
|
1380
|
+
}, 15000);
|
|
1381
|
+
if (workspacePresenceTimer && typeof workspacePresenceTimer.unref === 'function') workspacePresenceTimer.unref();
|
|
1382
|
+
}
|
|
1383
|
+
if (PRODUCT_KIND === 'workspace') {
|
|
1384
|
+
workspaceMonitorUnsub = () => {};
|
|
1385
|
+
setTimeout(() => runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('request')), 50);
|
|
1386
|
+
}
|
|
1387
|
+
} catch (_) {
|
|
1388
|
+
// BroadcastChannel is unavailable in some constrained contexts. The durable
|
|
1389
|
+
// workspace manifest still works; only live autodiscovery is disabled.
|
|
1390
|
+
}
|
|
1059
1391
|
}
|
|
1060
1392
|
|
|
1061
1393
|
// ─── URL fragment scroll (rwa-bootstrap 0.9) ───────────────────────
|
|
@@ -1116,6 +1448,12 @@ function buildFile(doc) {
|
|
|
1116
1448
|
function buildUI() {
|
|
1117
1449
|
document.getElementById('rwa-runtime').innerHTML = `
|
|
1118
1450
|
<div id="rwa-set">
|
|
1451
|
+
<div id="rwa-mode-tabs" role="tablist" aria-label="runtime mode">
|
|
1452
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="document">Document</button>
|
|
1453
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="edit">Edit</button>
|
|
1454
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="skills">Skills</button>
|
|
1455
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="actions">Actions</button>
|
|
1456
|
+
</div>
|
|
1119
1457
|
<button class="rwa-st-btn" id="rwa-st-status">● ready</button>
|
|
1120
1458
|
<button class="rwa-st-btn" id="rwa-st-info" title="what is this?" aria-label="what is this?">ⓘ</button>
|
|
1121
1459
|
<button class="rwa-st-btn" id="rwa-st-cog">⚙</button>
|
|
@@ -1135,6 +1473,7 @@ function buildUI() {
|
|
|
1135
1473
|
<div id="rwa-info-panel"></div>
|
|
1136
1474
|
<div id="rwa-skin-panel"></div>
|
|
1137
1475
|
<div id="rwa-share-panel"></div>
|
|
1476
|
+
<div id="rwa-mode-panel"></div>
|
|
1138
1477
|
<div id="rwa-pal">
|
|
1139
1478
|
<div id="rwa-pal-box">
|
|
1140
1479
|
<div class="rwa-pal-top">
|
|
@@ -1156,6 +1495,12 @@ function buildUI() {
|
|
|
1156
1495
|
<div id="rwa-lens-paste-hint" hidden></div>
|
|
1157
1496
|
<div id="rwa-lens-hint"></div>
|
|
1158
1497
|
</div>
|
|
1498
|
+
<div id="rwa-selection-bar" hidden data-rwa-no-inline-edit>
|
|
1499
|
+
<button type="button" id="rwa-selection-bold" title="Bold selection" aria-label="bold selection"><strong>B</strong></button>
|
|
1500
|
+
<input id="rwa-selection-cmd" placeholder="make it bold" autocomplete="off" spellcheck="false">
|
|
1501
|
+
<button type="button" class="pri" id="rwa-selection-run">Run</button>
|
|
1502
|
+
<button type="button" id="rwa-selection-voice">Mic</button>
|
|
1503
|
+
</div>
|
|
1159
1504
|
<div id="rwa-lens-hist-panel" hidden></div>`;
|
|
1160
1505
|
|
|
1161
1506
|
const k = document.getElementById('rwa-key'), m = document.getElementById('rwa-model');
|
|
@@ -1163,6 +1508,7 @@ function buildUI() {
|
|
|
1163
1508
|
m.value = sessionStorage.getItem(RWA.K_MODEL) || RWA.MODEL;
|
|
1164
1509
|
k.oninput = e => sessionStorage.setItem(RWA.K_API, e.target.value.trim());
|
|
1165
1510
|
m.oninput = e => sessionStorage.setItem(RWA.K_MODEL, e.target.value.trim() || RWA.MODEL);
|
|
1511
|
+
attachModeTabs();
|
|
1166
1512
|
|
|
1167
1513
|
// Bridge SESSION backend config: the bearer token the bridge requires on every
|
|
1168
1514
|
// /session/* call, and a server-side working dir for the claude session.
|
|
@@ -1301,11 +1647,14 @@ function buildUI() {
|
|
|
1301
1647
|
opt.value = id;
|
|
1302
1648
|
modelOptsEl.appendChild(opt);
|
|
1303
1649
|
}
|
|
1304
|
-
//
|
|
1305
|
-
|
|
1650
|
+
// For local OpenAI-compatible backends, pick a discovered model when the
|
|
1651
|
+
// current field is empty, still the baked default, or belongs to another
|
|
1652
|
+
// backend. The datalist still carries every returned model for manual choice.
|
|
1653
|
+
const currentModel = (m.value || sessionStorage.getItem(RWA.K_MODEL) || '').trim();
|
|
1306
1654
|
const isLocal = backendEl.value === 'ollama' || backendEl.value === 'lmstudio';
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1655
|
+
const shouldSuggestLocal = isLocal && models.length > 0
|
|
1656
|
+
&& (currentModel === '' || currentModel === RWA.MODEL || !models.includes(currentModel));
|
|
1657
|
+
if (shouldSuggestLocal) {
|
|
1309
1658
|
m.value = models[0];
|
|
1310
1659
|
sessionStorage.setItem(RWA.K_MODEL, models[0]);
|
|
1311
1660
|
}
|
|
@@ -1323,6 +1672,7 @@ function buildUI() {
|
|
|
1323
1672
|
document.getElementById('rwa-info-panel').classList.remove('open'); // panels are mutually exclusive
|
|
1324
1673
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1325
1674
|
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1675
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1326
1676
|
document.getElementById('rwa-set-panel').classList.toggle('open');
|
|
1327
1677
|
};
|
|
1328
1678
|
// ⓘ "what is this?" — render the rwa-identity/1 bundle as prose on open so it
|
|
@@ -1332,6 +1682,7 @@ function buildUI() {
|
|
|
1332
1682
|
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1333
1683
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1334
1684
|
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1685
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1335
1686
|
const panel = document.getElementById('rwa-info-panel');
|
|
1336
1687
|
if (!panel.classList.contains('open')) panel.innerHTML = renderInfoPanel();
|
|
1337
1688
|
panel.classList.toggle('open');
|
|
@@ -1351,6 +1702,7 @@ function buildUI() {
|
|
|
1351
1702
|
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1352
1703
|
document.getElementById('rwa-info-panel').classList.remove('open');
|
|
1353
1704
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1705
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1354
1706
|
renderSharePanel().then(() => panel.classList.add('open'));
|
|
1355
1707
|
};
|
|
1356
1708
|
document.getElementById('rwa-st-commit').onclick = commit;
|
|
@@ -1460,6 +1812,24 @@ function buildUI() {
|
|
|
1460
1812
|
});
|
|
1461
1813
|
}
|
|
1462
1814
|
|
|
1815
|
+
const selCmd = document.getElementById('rwa-selection-cmd');
|
|
1816
|
+
document.getElementById('rwa-selection-bold').addEventListener('click', () => runSelectionCommand('make it bold', { actor: 'user:selection-command' }).catch(() => {}));
|
|
1817
|
+
document.getElementById('rwa-selection-run').addEventListener('click', () => {
|
|
1818
|
+
runSelectionCommand(selCmd.value || '', { actor: 'user:selection-command' }).catch(() => {});
|
|
1819
|
+
});
|
|
1820
|
+
document.getElementById('rwa-selection-voice').addEventListener('click', () => startSelectionVoice());
|
|
1821
|
+
selCmd.addEventListener('keydown', (e) => {
|
|
1822
|
+
if (e.key === 'Enter') {
|
|
1823
|
+
e.preventDefault();
|
|
1824
|
+
runSelectionCommand(selCmd.value || '', { actor: 'user:selection-command' }).catch(() => {});
|
|
1825
|
+
} else if (e.key === 'Escape') {
|
|
1826
|
+
hideSelectionCommandBar();
|
|
1827
|
+
}
|
|
1828
|
+
});
|
|
1829
|
+
document.addEventListener('selectionchange', scheduleSelectionCommandRefresh);
|
|
1830
|
+
document.addEventListener('mouseup', scheduleSelectionCommandRefresh);
|
|
1831
|
+
document.addEventListener('keyup', scheduleSelectionCommandRefresh);
|
|
1832
|
+
|
|
1463
1833
|
// rwa-lens/1: Esc releases the anchor when one is held. Listener is on
|
|
1464
1834
|
// `window` so Esc anywhere works (including with focus outside the lens
|
|
1465
1835
|
// input — e.g. on the doc itself after a click).
|
|
@@ -1651,6 +2021,139 @@ async function renderHistoryPanel(panel) {
|
|
|
1651
2021
|
if (closeBtn) closeBtn.addEventListener('click', () => { panel.hidden = true; });
|
|
1652
2022
|
}
|
|
1653
2023
|
|
|
2024
|
+
function renderSkillsModePanel(panel) {
|
|
2025
|
+
if (!isRwaSkillHost()) {
|
|
2026
|
+
panel.innerHTML = [
|
|
2027
|
+
'<div class="rwa-mode-section">',
|
|
2028
|
+
'<div class="rwa-mode-kicker">Skills</div>',
|
|
2029
|
+
'<h4>Skill runtime unavailable</h4>',
|
|
2030
|
+
'<p>This file does not include the skill runtime.</p>',
|
|
2031
|
+
'</div>',
|
|
2032
|
+
].join('');
|
|
2033
|
+
return;
|
|
2034
|
+
}
|
|
2035
|
+
const canInstall = typeof runtimeInstallSkill === 'function' && typeof runtimeUninstallSkill === 'function' && typeof runtimeInvokeSkill === 'function';
|
|
2036
|
+
const skills = runtimeListSkills();
|
|
2037
|
+
const rows = skills.length ? skills.map(s => {
|
|
2038
|
+
const id = escRuntimeHtml(s.skillId);
|
|
2039
|
+
const name = escRuntimeHtml(s.name || s.skillId);
|
|
2040
|
+
const kind = escRuntimeHtml(s.kind || 'skill');
|
|
2041
|
+
const verified = s.verified ? 'verified' : 'unsigned/unverified';
|
|
2042
|
+
return [
|
|
2043
|
+
'<div class="rwa-mode-row" data-skill-id="' + id + '">',
|
|
2044
|
+
'<div style="min-width:0;flex:1">',
|
|
2045
|
+
'<div class="rwa-mode-title">' + name + '</div>',
|
|
2046
|
+
'<div class="rwa-mode-meta">' + kind + ' · ' + escRuntimeHtml(verified) + ' · ' + id + '</div>',
|
|
2047
|
+
canInstall ? '<div class="rwa-mode-invoke"><input data-skill-input="' + id + '" placeholder=\'JSON input, optional\'><button type="button" data-skill-invoke="' + id + '">Invoke</button></div>' : '',
|
|
2048
|
+
'<div class="rwa-mode-result" data-skill-result="' + id + '" hidden></div>',
|
|
2049
|
+
'</div>',
|
|
2050
|
+
canInstall ? '<button type="button" data-skill-uninstall="' + id + '">Uninstall</button>' : '',
|
|
2051
|
+
'</div>',
|
|
2052
|
+
].join('');
|
|
2053
|
+
}).join('') : '<div class="rwa-mode-empty">No skills installed.</div>';
|
|
2054
|
+
panel.innerHTML = [
|
|
2055
|
+
'<div class="rwa-mode-section">',
|
|
2056
|
+
'<div class="rwa-mode-kicker">Skills</div>',
|
|
2057
|
+
'<h4>Installed skills</h4>',
|
|
2058
|
+
canInstall ? '<div class="rwa-mode-actions"><button type="button" class="pri" id="rwa-skills-install">Install skill...</button></div>' : '<p>Skill install and invoke APIs are not available in this container.</p>',
|
|
2059
|
+
'</div>',
|
|
2060
|
+
'<div class="rwa-mode-section">' + rows + '</div>',
|
|
2061
|
+
].join('');
|
|
2062
|
+
const install = panel.querySelector('#rwa-skills-install');
|
|
2063
|
+
if (install) install.addEventListener('click', async () => {
|
|
2064
|
+
const out = await runtimePromptInstall();
|
|
2065
|
+
renderSkillsModePanel(panel);
|
|
2066
|
+
if (out && out.ok === false) setStatus('err', '✗ skill install failed');
|
|
2067
|
+
});
|
|
2068
|
+
panel.querySelectorAll('[data-skill-uninstall]').forEach(btn => {
|
|
2069
|
+
btn.addEventListener('click', async () => {
|
|
2070
|
+
const id = btn.getAttribute('data-skill-uninstall');
|
|
2071
|
+
const out = await runtimeUninstallSkill(id);
|
|
2072
|
+
if (out && out.ok === false) setStatus('err', '✗ skill uninstall failed');
|
|
2073
|
+
renderSkillsModePanel(panel);
|
|
2074
|
+
});
|
|
2075
|
+
});
|
|
2076
|
+
panel.querySelectorAll('[data-skill-invoke]').forEach(btn => {
|
|
2077
|
+
btn.addEventListener('click', async () => {
|
|
2078
|
+
const id = btn.getAttribute('data-skill-invoke');
|
|
2079
|
+
const row = btn.closest('[data-skill-id]');
|
|
2080
|
+
const inputEl = row && row.querySelector('[data-skill-input]');
|
|
2081
|
+
const resultEl = row && row.querySelector('[data-skill-result]');
|
|
2082
|
+
let input = {};
|
|
2083
|
+
try {
|
|
2084
|
+
const raw = (inputEl && inputEl.value || '').trim();
|
|
2085
|
+
input = raw ? JSON.parse(raw) : {};
|
|
2086
|
+
} catch (e) {
|
|
2087
|
+
if (resultEl) { resultEl.hidden = false; resultEl.textContent = 'Invalid JSON input'; }
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
btn.disabled = true;
|
|
2091
|
+
try {
|
|
2092
|
+
const out = await runtimeInvokeSkill(id, input);
|
|
2093
|
+
if (resultEl) { resultEl.hidden = false; resultEl.textContent = JSON.stringify(out, null, 2); }
|
|
2094
|
+
} catch (e) {
|
|
2095
|
+
if (resultEl) { resultEl.hidden = false; resultEl.textContent = e?.message || String(e); }
|
|
2096
|
+
} finally {
|
|
2097
|
+
btn.disabled = false;
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
|
|
2103
|
+
async function renderActionsModePanel(panel) {
|
|
2104
|
+
const d = runtimeDescribe();
|
|
2105
|
+
let entries = [];
|
|
2106
|
+
try { entries = (await idbGet(RWA.HIST)) || []; } catch (_) { entries = []; }
|
|
2107
|
+
if (rwaMode !== 'actions') return;
|
|
2108
|
+
const viewAff = d.affordances.find(a => a.kind === 'view' && a.verified === true);
|
|
2109
|
+
const skillAffs = d.affordances.filter(a => a.provenance === 'installed' && a.skillId);
|
|
2110
|
+
const providerAffs = d.affordances.filter(a => a.provenance !== 'installed');
|
|
2111
|
+
const fmtTs = ts => { try { return new Date(ts).toLocaleString(); } catch (_) { return String(ts); } };
|
|
2112
|
+
const histRows = entries.length ? entries.slice(0, 12).map(e => {
|
|
2113
|
+
const surface = e.surface || e.kind || 'edit';
|
|
2114
|
+
const actor = e.actor ? ' · ' + e.actor : '';
|
|
2115
|
+
const instr = e.instruction || e.reason || '(no instruction recorded)';
|
|
2116
|
+
return '<div class="rwa-mode-hist"><div class="rwa-mode-meta">' + escRuntimeHtml(surface + actor + ' · ' + fmtTs(e.ts)) + '</div><div>' + escRuntimeHtml(instr) + '</div></div>';
|
|
2117
|
+
}).join('') : '<div class="rwa-mode-empty">No runs yet.</div>';
|
|
2118
|
+
const affRows = providerAffs.length ? providerAffs.map(a => {
|
|
2119
|
+
const detail = [a.kind, a.verified ? 'verified' : 'declared'].join(' · ');
|
|
2120
|
+
return '<div class="rwa-mode-row"><div><div class="rwa-mode-title">' + escRuntimeHtml(a.label || a.name) + '</div><div class="rwa-mode-meta">' + escRuntimeHtml(detail + ' · ' + a.name) + '</div></div></div>';
|
|
2121
|
+
}).join('') : '<div class="rwa-mode-empty">No declared live affordances.</div>';
|
|
2122
|
+
const skillRows = skillAffs.length ? skillAffs.map(a =>
|
|
2123
|
+
'<div class="rwa-mode-row"><div><div class="rwa-mode-title">' + escRuntimeHtml(a.name) + '</div><div class="rwa-mode-meta">' + escRuntimeHtml(a.kind || 'skill') + ' · ' + escRuntimeHtml(a.skillId) + '</div></div><button type="button" data-action-skill="' + escRuntimeHtml(a.skillId) + '">Open in Skills</button></div>'
|
|
2124
|
+
).join('') : '<div class="rwa-mode-empty">No installed skills.</div>';
|
|
2125
|
+
panel.innerHTML = [
|
|
2126
|
+
'<div class="rwa-mode-section">',
|
|
2127
|
+
'<div class="rwa-mode-kicker">Actions</div>',
|
|
2128
|
+
'<h4>Action center</h4>',
|
|
2129
|
+
'<div class="rwa-mode-actions">',
|
|
2130
|
+
'<button type="button" id="rwa-actions-undo">Undo</button>',
|
|
2131
|
+
'<button type="button" class="pri" id="rwa-actions-save">Save / Export</button>',
|
|
2132
|
+
'<button type="button" id="rwa-actions-share">Share</button>',
|
|
2133
|
+
viewAff ? '<button type="button" id="rwa-actions-view">' + escRuntimeHtml(d.activeView === viewAff.name ? 'Exit ' + viewAff.label : viewAff.label) + '</button>' : '',
|
|
2134
|
+
'</div>',
|
|
2135
|
+
'</div>',
|
|
2136
|
+
'<div class="rwa-mode-section"><div class="rwa-mode-kicker">Recent runs</div>' + histRows + '</div>',
|
|
2137
|
+
'<div class="rwa-mode-section"><div class="rwa-mode-kicker">Live affordances</div>' + affRows + '</div>',
|
|
2138
|
+
'<div class="rwa-mode-section"><div class="rwa-mode-kicker">Installed skill actions</div>' + skillRows + '</div>',
|
|
2139
|
+
].join('');
|
|
2140
|
+
const undoBtn = panel.querySelector('#rwa-actions-undo');
|
|
2141
|
+
if (undoBtn) undoBtn.addEventListener('click', () => runtimeUndo());
|
|
2142
|
+
const saveBtn = panel.querySelector('#rwa-actions-save');
|
|
2143
|
+
if (saveBtn) saveBtn.addEventListener('click', () => runtimeCommit());
|
|
2144
|
+
const shareBtn = panel.querySelector('#rwa-actions-share');
|
|
2145
|
+
if (shareBtn) shareBtn.addEventListener('click', async () => {
|
|
2146
|
+
document.getElementById('rwa-mode-panel')?.classList.remove('open');
|
|
2147
|
+
await renderSharePanel();
|
|
2148
|
+
document.getElementById('rwa-share-panel')?.classList.add('open');
|
|
2149
|
+
});
|
|
2150
|
+
const viewBtn = panel.querySelector('#rwa-actions-view');
|
|
2151
|
+
if (viewBtn && viewAff) viewBtn.addEventListener('click', () => runtimeSetView(d.activeView === viewAff.name ? null : viewAff.name));
|
|
2152
|
+
panel.querySelectorAll('[data-action-skill]').forEach(btn => {
|
|
2153
|
+
btn.addEventListener('click', () => runtimeSetMode('skills'));
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
|
|
1654
2157
|
// ─── Agent (rwa-edit/1) ─────────────────────────────────────────────
|
|
1655
2158
|
// Spec: rwa-edit-spec.md (v1.4). The agent edits the doc via tool calls,
|
|
1656
2159
|
// not by emitting a full rewritten document. unchanged regions are byte-
|
|
@@ -1834,6 +2337,15 @@ The stored document is ordinary prose HTML inside a single <article>: <h1>/<h2>
|
|
|
1834
2337
|
Slide model the user reasons about: a new slide STARTS at each <h1> or <h2>. So "add a slide" = add a new <h2> title followed by its body paragraphs/bullets at the right position. "Split this slide" = add an <h2> in the middle. "Merge two slides" = remove the second slide's heading. "Reorder slides" = move the heading-plus-its-body block. Keep slide bodies concise — a deck slide is a title plus a few short paragraphs or a short list, not an essay.
|
|
1835
2338
|
|
|
1836
2339
|
Preserve every existing data-rwa-id verbatim (the runtime assigns them). Anchor edits on unique text near the heading you mean.
|
|
2340
|
+
${SYSTEM_PROMPT_RULES}`,
|
|
2341
|
+
|
|
2342
|
+
workspace: `You are editing a rewritable workspace index. Apply the user's request as a small set of surgical edits via tool calls.
|
|
2343
|
+
|
|
2344
|
+
The stored document is a directory control center, normally named rwa-index.html. It summarizes sibling rewritable files from the same folder. The visible <article class="rwa-workspace"> is editable dashboard content; the <script id="rwa-workspace" type="application/rwa-workspace+json"> manifest is generated by the CLI and wrapped in a frozen zone.
|
|
2345
|
+
|
|
2346
|
+
Preserve the workspace-style and workspace-manifest frozen zones exactly. Do not invent files that are not already listed unless the user explicitly asks for placeholder planning content. To refresh the real file list, the user should run \`rwa workspace sync\`; you should not manually edit the manifest.
|
|
2347
|
+
|
|
2348
|
+
When changing the dashboard, keep links to sibling documents relative, and preserve every existing data-rwa-id verbatim.
|
|
1837
2349
|
${SYSTEM_PROMPT_RULES}`,
|
|
1838
2350
|
};
|
|
1839
2351
|
// rwa:extract:end SYSTEM_PROMPTS
|
|
@@ -2589,6 +3101,10 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2589
3101
|
// live mount produces the same order via the same outer-wins traversal — so
|
|
2590
3102
|
// the i-th live anchorable corresponds to map[i].
|
|
2591
3103
|
function handleMountClick(e) {
|
|
3104
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3105
|
+
// Pointer-down may already have opened a WYSIWYG inline edit. In that case
|
|
3106
|
+
// the click belongs to caret placement, not to lens anchoring.
|
|
3107
|
+
if (inlineEdit) return;
|
|
2592
3108
|
// Audit R3 (scoped): respect the per-kind click-to-anchor flag. When
|
|
2593
3109
|
// false (e.g. workflow files), all clicks pass through without anchoring
|
|
2594
3110
|
// — the lens stays in default state and every command runs against the
|
|
@@ -2749,10 +3265,11 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2749
3265
|
}
|
|
2750
3266
|
|
|
2751
3267
|
// ─── Inline manual edit (edit-surface: direct, no-LLM block editing) ──────
|
|
2752
|
-
//
|
|
3268
|
+
// Click a leaf text block and type in place; Enter or blur
|
|
2753
3269
|
// commits through the existing non-agent commit path (runtimeApplyEnvelope,
|
|
2754
|
-
// actor 'user:edit-surface') — no model call, works offline.
|
|
2755
|
-
// still
|
|
3270
|
+
// actor 'user:edit-surface') — no model call, works offline. Container clicks
|
|
3271
|
+
// still anchor the lens (handleMountClick); leaf text clicks become WYSIWYG
|
|
3272
|
+
// edit sessions. Double-click remains a compatibility path. Design:
|
|
2756
3273
|
// docs/plans/2026-06-08-inline-manual-edit-design.md.
|
|
2757
3274
|
//
|
|
2758
3275
|
// Editable set = the leaf-text members of ANCHORABLE_TAGS, so the
|
|
@@ -2760,6 +3277,7 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2760
3277
|
// containers; FIGCAPTION is not independently anchorable (lives in FIGURE).
|
|
2761
3278
|
const INLINE_EDITABLE = new Set(['P','H1','H2','H3','H4','H5','H6','BLOCKQUOTE','LI','TD']);
|
|
2762
3279
|
let inlineEdit = null; // { el, entry } while a block is being hand-edited
|
|
3280
|
+
const INLINE_EDIT_BYPASS = 'button,input,textarea,select,option,summary,[contenteditable="true"],[data-rwa-no-inline-edit]';
|
|
2763
3281
|
|
|
2764
3282
|
// Controlled serializer: turn an edited contenteditable node into a clean
|
|
2765
3283
|
// replace string. Emits ONLY escaped text and <br> (the Shift+Enter soft
|
|
@@ -2779,6 +3297,270 @@ function serializeLeafSafe(el) {
|
|
|
2779
3297
|
}
|
|
2780
3298
|
window.serializeLeafSafe = serializeLeafSafe;
|
|
2781
3299
|
|
|
3300
|
+
// ─── Selection command surface (typed or voice) ─────────────────────
|
|
3301
|
+
// Edit-mode-only layer over a user text selection. Deterministic commands
|
|
3302
|
+
// such as "make it bold" compile to rwa-edit/1 locally; voice recognition is
|
|
3303
|
+
// only an input method that fills/runs the same command path.
|
|
3304
|
+
let selectionCommandState = null; // { el, entry, text, occurrence, range }
|
|
3305
|
+
let selectionVoiceRecognizer = null;
|
|
3306
|
+
|
|
3307
|
+
function hideSelectionCommandBar() {
|
|
3308
|
+
selectionCommandState = null;
|
|
3309
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3310
|
+
if (bar) {
|
|
3311
|
+
bar.hidden = true;
|
|
3312
|
+
delete bar.dataset.listening;
|
|
3313
|
+
}
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function nodeElement(n) {
|
|
3317
|
+
return n && (n.nodeType === 1 ? n : n.parentElement);
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
function closestSelectionLeaf(node, mount) {
|
|
3321
|
+
let el = nodeElement(node);
|
|
3322
|
+
while (el && el !== mount && !INLINE_EDITABLE.has(el.tagName)) el = el.parentElement;
|
|
3323
|
+
return (el && el !== mount) ? el : null;
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
function countTextOccurrences(haystack, needle) {
|
|
3327
|
+
if (!needle) return 0;
|
|
3328
|
+
let n = 0, i = 0;
|
|
3329
|
+
while ((i = haystack.indexOf(needle, i)) !== -1) { n++; i += needle.length; }
|
|
3330
|
+
return n;
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
function nthIndexOf(haystack, needle, nth) {
|
|
3334
|
+
let i = -1, from = 0;
|
|
3335
|
+
for (let n = 0; n <= nth; n++) {
|
|
3336
|
+
i = haystack.indexOf(needle, from);
|
|
3337
|
+
if (i < 0) return -1;
|
|
3338
|
+
from = i + needle.length;
|
|
3339
|
+
}
|
|
3340
|
+
return i;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
function resolveSelectionCommandTarget() {
|
|
3344
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return null;
|
|
3345
|
+
const mount = document.getElementById('rwa-doc-mount');
|
|
3346
|
+
const sel = window.getSelection && window.getSelection();
|
|
3347
|
+
if (!mount || !sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
|
|
3348
|
+
const range = sel.getRangeAt(0);
|
|
3349
|
+
const text = sel.toString();
|
|
3350
|
+
if (!text || !text.trim()) return null;
|
|
3351
|
+
if (!mount.contains(range.commonAncestorContainer)) return null;
|
|
3352
|
+
const startLeaf = closestSelectionLeaf(range.startContainer, mount);
|
|
3353
|
+
const endLeaf = closestSelectionLeaf(range.endContainer, mount);
|
|
3354
|
+
if (!startLeaf || startLeaf !== endLeaf) return null;
|
|
3355
|
+
if (startLeaf.closest('[data-rwa-frozen]') || startLeaf.closest('.rwa-locked')) return null;
|
|
3356
|
+
const ord = anchorableOrdinal(mount, startLeaf);
|
|
3357
|
+
const entry = (ord >= 0 && sourceMap) ? sourceMap[ord] : null;
|
|
3358
|
+
if (!entry || isWithinLockedRange(entry.start, entry.end)) return null;
|
|
3359
|
+
let occurrence = 0;
|
|
3360
|
+
try {
|
|
3361
|
+
const prefix = document.createRange();
|
|
3362
|
+
prefix.selectNodeContents(startLeaf);
|
|
3363
|
+
prefix.setEnd(range.startContainer, range.startOffset);
|
|
3364
|
+
occurrence = countTextOccurrences(prefix.toString(), text);
|
|
3365
|
+
} catch (_) { occurrence = 0; }
|
|
3366
|
+
return { el: startLeaf, entry, text, occurrence, range };
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
function locateSelectionSource(target) {
|
|
3370
|
+
const block = currentDocCache.slice(target.entry.start, target.entry.end);
|
|
3371
|
+
const tag = target.entry.tag.toLowerCase();
|
|
3372
|
+
const openEnd = block.indexOf('>');
|
|
3373
|
+
const closeRe = new RegExp('</' + tag + '\\s*>\\s*$', 'i');
|
|
3374
|
+
const closeMatch = closeRe.exec(block);
|
|
3375
|
+
if (openEnd < 0 || !closeMatch) return null;
|
|
3376
|
+
const innerStart = openEnd + 1;
|
|
3377
|
+
const innerEnd = closeMatch.index;
|
|
3378
|
+
const inner = block.slice(innerStart, innerEnd);
|
|
3379
|
+
const candidates = [];
|
|
3380
|
+
const plain = escapeHtml(target.text);
|
|
3381
|
+
const br = plain.replace(/\r\n|\r|\n/g, '<br>');
|
|
3382
|
+
candidates.push(br);
|
|
3383
|
+
if (plain !== br) candidates.push(plain);
|
|
3384
|
+
for (const selectedSource of candidates) {
|
|
3385
|
+
const idx = nthIndexOf(inner, selectedSource, target.occurrence || 0);
|
|
3386
|
+
if (idx >= 0) {
|
|
3387
|
+
return {
|
|
3388
|
+
block,
|
|
3389
|
+
selectedSource,
|
|
3390
|
+
start: innerStart + idx,
|
|
3391
|
+
end: innerStart + idx + selectedSource.length,
|
|
3392
|
+
};
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
return null;
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
function parseSelectionCommand(raw) {
|
|
3399
|
+
const t = String(raw || '').trim().toLowerCase();
|
|
3400
|
+
if (!t) return null;
|
|
3401
|
+
if (/\b(bold|strong|emphasize strongly)\b/.test(t)) return { kind: 'wrap', tag: 'strong', label: 'bold' };
|
|
3402
|
+
if (/\b(italic|italics|emphasize)\b/.test(t)) return { kind: 'wrap', tag: 'em', label: 'italic' };
|
|
3403
|
+
if (/\b(code|monospace|inline code)\b/.test(t)) return { kind: 'wrap', tag: 'code', label: 'code' };
|
|
3404
|
+
return null;
|
|
3405
|
+
}
|
|
3406
|
+
|
|
3407
|
+
async function applySelectionWrap(target, action, actor, instruction) {
|
|
3408
|
+
if (inlineEdit && serializeLeafSafe(inlineEdit.el) !== inlineEdit.original) {
|
|
3409
|
+
showAffordance('finish the current edit first');
|
|
3410
|
+
return { ok: false, reason: 'inline_edit_dirty' };
|
|
3411
|
+
}
|
|
3412
|
+
if (inlineEdit) exitInlineEdit();
|
|
3413
|
+
const loc = locateSelectionSource(target);
|
|
3414
|
+
if (!loc) {
|
|
3415
|
+
showAffordance('selection could not be mapped to source');
|
|
3416
|
+
return { ok: false, reason: 'selection_unmapped' };
|
|
3417
|
+
}
|
|
3418
|
+
const a = resolveAnchorFind(target.entry);
|
|
3419
|
+
if (!a) {
|
|
3420
|
+
showAffordance('selection block is ambiguous');
|
|
3421
|
+
return { ok: false, reason: 'anchor_unresolved' };
|
|
3422
|
+
}
|
|
3423
|
+
const tag = action.tag;
|
|
3424
|
+
const nextBlock = loc.block.slice(0, loc.start) + '<' + tag + '>' + loc.selectedSource + '</' + tag + '>' + loc.block.slice(loc.end);
|
|
3425
|
+
const replace = a.replacePrefix + nextBlock + a.replaceSuffix;
|
|
3426
|
+
await runtimeApplyEnvelope(
|
|
3427
|
+
{ version: 'rwa-edit/1', edits: [{ find: a.find, replace, reason: 'selection:' + action.label }] },
|
|
3428
|
+
{ surface: 'selection-edit', instruction: instruction || action.label, actor: actor || 'user:selection-command' });
|
|
3429
|
+
try { window.getSelection().removeAllRanges(); } catch (_) {}
|
|
3430
|
+
hideSelectionCommandBar();
|
|
3431
|
+
showAffordance('selection: ' + action.label + ' — ⌘Z to undo');
|
|
3432
|
+
return { ok: true };
|
|
3433
|
+
}
|
|
3434
|
+
|
|
3435
|
+
async function runSelectionCommand(raw, options) {
|
|
3436
|
+
options = options || {};
|
|
3437
|
+
const target = resolveSelectionCommandTarget() || selectionCommandState;
|
|
3438
|
+
if (!target) {
|
|
3439
|
+
showAffordance('select text in Edit mode first');
|
|
3440
|
+
return { ok: false, reason: 'no_selection' };
|
|
3441
|
+
}
|
|
3442
|
+
const action = parseSelectionCommand(raw);
|
|
3443
|
+
if (!action) {
|
|
3444
|
+
showAffordance('selection command not available yet');
|
|
3445
|
+
return { ok: false, reason: 'unknown_command' };
|
|
3446
|
+
}
|
|
3447
|
+
if (action.kind === 'wrap') return applySelectionWrap(target, action, options.actor, raw);
|
|
3448
|
+
return { ok: false, reason: 'unknown_command' };
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
function positionSelectionCommandBar(target) {
|
|
3452
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3453
|
+
if (!bar || !target || !target.range) return;
|
|
3454
|
+
let rect = null;
|
|
3455
|
+
try { rect = target.range.getBoundingClientRect(); } catch (_) {}
|
|
3456
|
+
if (!rect || (rect.width === 0 && rect.height === 0)) {
|
|
3457
|
+
try { rect = target.el.getBoundingClientRect(); } catch (_) {}
|
|
3458
|
+
}
|
|
3459
|
+
const top = Math.max(8, window.scrollY + (rect ? rect.top : 0) - 42);
|
|
3460
|
+
const left = Math.max(8, Math.min(window.scrollX + (rect ? rect.left : 0), window.scrollX + window.innerWidth - 320));
|
|
3461
|
+
bar.style.top = top + 'px';
|
|
3462
|
+
bar.style.left = left + 'px';
|
|
3463
|
+
bar.hidden = false;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
function refreshSelectionCommandBar() {
|
|
3467
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3468
|
+
if (bar && bar.contains(document.activeElement)) return;
|
|
3469
|
+
const target = resolveSelectionCommandTarget();
|
|
3470
|
+
if (!target) { hideSelectionCommandBar(); return; }
|
|
3471
|
+
selectionCommandState = target;
|
|
3472
|
+
positionSelectionCommandBar(target);
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
let selectionRefreshQueued = false;
|
|
3476
|
+
function scheduleSelectionCommandRefresh() {
|
|
3477
|
+
if (selectionRefreshQueued) return;
|
|
3478
|
+
selectionRefreshQueued = true;
|
|
3479
|
+
requestAnimationFrame(() => {
|
|
3480
|
+
selectionRefreshQueued = false;
|
|
3481
|
+
refreshSelectionCommandBar();
|
|
3482
|
+
});
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
function startSelectionVoice() {
|
|
3486
|
+
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
3487
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3488
|
+
const input = document.getElementById('rwa-selection-cmd');
|
|
3489
|
+
if (!selectionCommandState && !resolveSelectionCommandTarget()) {
|
|
3490
|
+
showAffordance('select text first');
|
|
3491
|
+
return;
|
|
3492
|
+
}
|
|
3493
|
+
if (!SR) {
|
|
3494
|
+
showAffordance('voice input is not supported in this browser');
|
|
3495
|
+
if (input) input.focus();
|
|
3496
|
+
return;
|
|
3497
|
+
}
|
|
3498
|
+
try {
|
|
3499
|
+
if (selectionVoiceRecognizer) selectionVoiceRecognizer.abort();
|
|
3500
|
+
} catch (_) {}
|
|
3501
|
+
const rec = new SR();
|
|
3502
|
+
selectionVoiceRecognizer = rec;
|
|
3503
|
+
rec.lang = navigator.language || 'en-US';
|
|
3504
|
+
rec.interimResults = false;
|
|
3505
|
+
rec.maxAlternatives = 1;
|
|
3506
|
+
rec.onstart = () => { if (bar) bar.dataset.listening = '1'; };
|
|
3507
|
+
rec.onresult = (e) => {
|
|
3508
|
+
const text = e?.results?.[0]?.[0]?.transcript || '';
|
|
3509
|
+
if (input) input.value = text;
|
|
3510
|
+
runSelectionCommand(text, { actor: 'user:voice-selection' }).catch(err => showAffordance('voice: ' + (err?.message || err)));
|
|
3511
|
+
};
|
|
3512
|
+
rec.onerror = (e) => showAffordance('voice: ' + (e?.error || 'could not hear command'));
|
|
3513
|
+
rec.onend = () => { if (bar) delete bar.dataset.listening; selectionVoiceRecognizer = null; };
|
|
3514
|
+
rec.start();
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
3518
|
+
window.__refreshSelectionCommandBar = refreshSelectionCommandBar;
|
|
3519
|
+
window.__runSelectionCommand = runSelectionCommand;
|
|
3520
|
+
window.__startSelectionVoice = startSelectionVoice;
|
|
3521
|
+
window.__parseSelectionCommand = parseSelectionCommand;
|
|
3522
|
+
}
|
|
3523
|
+
|
|
3524
|
+
function resolveInlineEditTarget(target) {
|
|
3525
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return null;
|
|
3526
|
+
const mount = document.getElementById('rwa-doc-mount');
|
|
3527
|
+
if (!mount || !target) return null;
|
|
3528
|
+
if (target.closest && target.closest(INLINE_EDIT_BYPASS)) return null;
|
|
3529
|
+
let el = target;
|
|
3530
|
+
while (el && el !== mount && !INLINE_EDITABLE.has(el.tagName)) el = el.parentNode;
|
|
3531
|
+
if (!el || el === mount) return null;
|
|
3532
|
+
if (el.closest('[data-rwa-frozen]') || el.closest('.rwa-locked')) return null;
|
|
3533
|
+
const ord = anchorableOrdinal(mount, el);
|
|
3534
|
+
const entry = (ord >= 0 && sourceMap) ? sourceMap[ord] : null;
|
|
3535
|
+
// A nested anchorable (an inner <li>, or a <p> inside a <td>) is not recorded
|
|
3536
|
+
// by the outer-wins descent, so ord is -1 and we no-op — consistent with the
|
|
3537
|
+
// lens's anchoring model, which can't target sub-blocks either.
|
|
3538
|
+
if (!entry) return null;
|
|
3539
|
+
if (isWithinLockedRange(entry.start, entry.end)) return null; // marker-form frozen backstop
|
|
3540
|
+
return { el, entry };
|
|
3541
|
+
}
|
|
3542
|
+
|
|
3543
|
+
function refreshInlineEditAffordances(mount) {
|
|
3544
|
+
if (!mount) return;
|
|
3545
|
+
mount.querySelectorAll('.rwa-editable-leaf').forEach(el => {
|
|
3546
|
+
el.classList.remove('rwa-editable-leaf');
|
|
3547
|
+
if (el.getAttribute('tabindex') === '0' && el.dataset.rwaEditTab === '1') {
|
|
3548
|
+
el.removeAttribute('tabindex');
|
|
3549
|
+
delete el.dataset.rwaEditTab;
|
|
3550
|
+
}
|
|
3551
|
+
});
|
|
3552
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3553
|
+
mount.querySelectorAll(Array.from(INLINE_EDITABLE).map(t => t.toLowerCase()).join(',')).forEach(el => {
|
|
3554
|
+
const hit = resolveInlineEditTarget(el);
|
|
3555
|
+
if (!hit || hit.el !== el) return;
|
|
3556
|
+
el.classList.add('rwa-editable-leaf');
|
|
3557
|
+
if (!el.hasAttribute('tabindex')) {
|
|
3558
|
+
el.setAttribute('tabindex', '0');
|
|
3559
|
+
el.dataset.rwaEditTab = '1';
|
|
3560
|
+
}
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
|
|
2782
3564
|
function enterInlineEdit(el, entry) {
|
|
2783
3565
|
if (inlineEdit) exitInlineEdit();
|
|
2784
3566
|
inlineEdit = { el, entry, original: '', commandMode: false, demoted: false };
|
|
@@ -2996,34 +3778,39 @@ async function commitInlineEdit() {
|
|
|
2996
3778
|
}
|
|
2997
3779
|
window.commitInlineEdit = commitInlineEdit;
|
|
2998
3780
|
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3781
|
+
function startInlineEditFromEvent(e) {
|
|
3782
|
+
if (rwaMode !== 'edit' || activeView) return false;
|
|
3783
|
+
if (inlineEdit) return false;
|
|
3784
|
+
const hit = resolveInlineEditTarget(e && e.target);
|
|
3785
|
+
if (!hit) return false;
|
|
3786
|
+
enterInlineEdit(hit.el, hit.entry);
|
|
3787
|
+
return true;
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
// Pointer-down opens the edit before the browser performs its default caret
|
|
3791
|
+
// placement for the ensuing click. This is the WYSIWYG path: click text, type.
|
|
3792
|
+
function handleMountPointerDown(e) {
|
|
3793
|
+
if (e.button != null && e.button !== 0) return;
|
|
3794
|
+
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
|
|
3795
|
+
startInlineEditFromEvent(e);
|
|
3796
|
+
}
|
|
3797
|
+
|
|
3798
|
+
// Double-click compatibility for existing behavior and tests.
|
|
3004
3799
|
function handleMountDblClick(e) {
|
|
3005
|
-
|
|
3006
|
-
|
|
3007
|
-
|
|
3008
|
-
|
|
3009
|
-
if (
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
if (el.closest('[data-rwa-frozen]') || el.closest('.rwa-locked')) return;
|
|
3016
|
-
const ord = anchorableOrdinal(mount, el);
|
|
3017
|
-
const entry = (ord >= 0 && sourceMap) ? sourceMap[ord] : null;
|
|
3018
|
-
// A nested anchorable (an inner <li>, or a <p> inside a <td>) is not recorded
|
|
3019
|
-
// by the outer-wins descent, so ord is -1 and we no-op — consistent with the
|
|
3020
|
-
// lens's anchoring model, which can't target sub-blocks either.
|
|
3021
|
-
if (!entry) return;
|
|
3022
|
-
if (isWithinLockedRange(entry.start, entry.end)) return; // marker-form frozen backstop
|
|
3023
|
-
enterInlineEdit(el, entry);
|
|
3800
|
+
startInlineEditFromEvent(e);
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
function handleMountEditKeydown(e) {
|
|
3804
|
+
if (inlineEdit) return;
|
|
3805
|
+
if (e.key !== 'Enter' && e.key !== 'F2') return;
|
|
3806
|
+
const hit = resolveInlineEditTarget(e.target);
|
|
3807
|
+
if (!hit || hit.el !== e.target) return;
|
|
3808
|
+
e.preventDefault();
|
|
3809
|
+
enterInlineEdit(hit.el, hit.entry);
|
|
3024
3810
|
}
|
|
3025
3811
|
if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
3026
3812
|
window.__handleMountDblClick = handleMountDblClick;
|
|
3813
|
+
window.__handleMountPointerDown = handleMountPointerDown;
|
|
3027
3814
|
}
|
|
3028
3815
|
|
|
3029
3816
|
// ─── Image ingestion (images-v1) ──────────────────────────────────────
|
|
@@ -3211,7 +3998,7 @@ function dragHasFiles(e) {
|
|
|
3211
3998
|
return Array.from(t.types || []).includes('Files');
|
|
3212
3999
|
}
|
|
3213
4000
|
function handleMountDragOver(e) {
|
|
3214
|
-
if (modifyMutex || !dragHasFiles(e)) return;
|
|
4001
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex || !dragHasFiles(e)) return;
|
|
3215
4002
|
e.preventDefault();
|
|
3216
4003
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
|
3217
4004
|
clearDropMark();
|
|
@@ -3226,6 +4013,7 @@ function handleMountDragLeave(e) {
|
|
|
3226
4013
|
if (!m || !e.relatedTarget || !m.contains(e.relatedTarget)) clearDropMark();
|
|
3227
4014
|
}
|
|
3228
4015
|
function handleMountDrop(e) {
|
|
4016
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3229
4017
|
if (!dragHasFiles(e)) return;
|
|
3230
4018
|
e.preventDefault();
|
|
3231
4019
|
const target = findImageDropTarget(e);
|
|
@@ -3240,7 +4028,7 @@ function handleMountDrop(e) {
|
|
|
3240
4028
|
|
|
3241
4029
|
// ── paste (a screenshot is the #1 case) ──
|
|
3242
4030
|
function handleDocumentImagePaste(e) {
|
|
3243
|
-
if (activeView) return;
|
|
4031
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3244
4032
|
if (typeof inlineEdit !== 'undefined' && inlineEdit) return; // hand-edit owns its paste
|
|
3245
4033
|
const files = Array.from((e.clipboardData && e.clipboardData.files) || []).filter(f => /^image\//.test(f.type || ''));
|
|
3246
4034
|
if (files.length === 0) return;
|
|
@@ -3256,6 +4044,7 @@ document.addEventListener('paste', handleDocumentImagePaste);
|
|
|
3256
4044
|
|
|
3257
4045
|
// ── /image picker ──
|
|
3258
4046
|
function openImagePicker() {
|
|
4047
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3259
4048
|
const input = document.createElement('input');
|
|
3260
4049
|
input.type = 'file';
|
|
3261
4050
|
input.accept = 'image/*';
|
|
@@ -3346,7 +4135,7 @@ function ensureImgChip() {
|
|
|
3346
4135
|
return chip;
|
|
3347
4136
|
}
|
|
3348
4137
|
function handleMountImgOver(e) {
|
|
3349
|
-
if (activeView || modifyMutex) return;
|
|
4138
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3350
4139
|
const img = e.target instanceof Element ? e.target.closest('img') : null;
|
|
3351
4140
|
const m = document.getElementById('rwa-doc-mount');
|
|
3352
4141
|
if (!img || !m || !m.contains(img)) return;
|
|
@@ -4206,6 +4995,7 @@ HARD RULES: colors are hex strings only (e.g. "#c0392b"); fonts are ONE of the f
|
|
|
4206
4995
|
const setP = document.getElementById('rwa-set-panel'); if (setP) setP.classList.remove('open');
|
|
4207
4996
|
const infoP = document.getElementById('rwa-info-panel'); if (infoP) infoP.classList.remove('open');
|
|
4208
4997
|
const shareP = document.getElementById('rwa-share-panel'); if (shareP) shareP.classList.remove('open');
|
|
4998
|
+
const modeP = document.getElementById('rwa-mode-panel'); if (modeP) modeP.classList.remove('open');
|
|
4209
4999
|
const active = currentSkinName();
|
|
4210
5000
|
panel.innerHTML =
|
|
4211
5001
|
'<div class="rwa-skin-hd"><span>Skins</span><span>' + (active || '—') + '</span></div>'
|
|
@@ -5588,9 +6378,16 @@ function runtimeSetView(name) {
|
|
|
5588
6378
|
if (!spec || spec.name !== name) throw new Error('no registered view named ' + name);
|
|
5589
6379
|
validateViewOutput(spec.render(currentDocCache, viewCtx()), spec); // throws → never activates
|
|
5590
6380
|
releaseAnchor();
|
|
6381
|
+
if (rwaMode !== 'document') {
|
|
6382
|
+
hideEditTransients();
|
|
6383
|
+
closeRuntimePanels();
|
|
6384
|
+
rwaMode = 'document';
|
|
6385
|
+
emitRuntimeEvent('mode', { mode: rwaMode });
|
|
6386
|
+
}
|
|
5591
6387
|
activeView = spec;
|
|
5592
6388
|
sessionStorage.setItem(rwaViewKey(), name);
|
|
5593
6389
|
}
|
|
6390
|
+
if (typeof syncModeChrome === 'function') syncModeChrome();
|
|
5594
6391
|
if (typeof syncViewChrome === 'function') syncViewChrome();
|
|
5595
6392
|
getDoc().then(d => renderDoc(canonLF(d)));
|
|
5596
6393
|
}
|
|
@@ -7241,7 +8038,11 @@ async function commit() {
|
|
|
7241
8038
|
document.addEventListener('keydown', e => {
|
|
7242
8039
|
const mod = navigator.platform.includes('Mac') ? e.metaKey : e.ctrlKey;
|
|
7243
8040
|
if (!mod) return;
|
|
7244
|
-
if (e.key === 'k') {
|
|
8041
|
+
if (e.key === 'k') {
|
|
8042
|
+
e.preventDefault();
|
|
8043
|
+
try { if (rwaMode !== 'edit') runtimeSetMode('edit'); } catch (_) { return; }
|
|
8044
|
+
document.getElementById('rwa-pal').classList.contains('open') ? closePal() : openPal();
|
|
8045
|
+
}
|
|
7245
8046
|
else if (e.key === 'z') { e.preventDefault(); undo(); }
|
|
7246
8047
|
else if (e.key === 's') { e.preventDefault(); commit(); }
|
|
7247
8048
|
});
|
|
@@ -7326,11 +8127,16 @@ document.addEventListener('keydown', e => {
|
|
|
7326
8127
|
del: runtimeFsDel,
|
|
7327
8128
|
list: runtimeFsList,
|
|
7328
8129
|
},
|
|
8130
|
+
bus: {
|
|
8131
|
+
publish: runtimeBusPublish,
|
|
8132
|
+
subscribe: runtimeBusSubscribe,
|
|
8133
|
+
},
|
|
7329
8134
|
modify: runtimeModify,
|
|
7330
8135
|
commit: runtimeCommit,
|
|
7331
8136
|
undo: runtimeUndo,
|
|
7332
8137
|
applyEnvelope: runtimeApplyEnvelope,
|
|
7333
8138
|
on: runtimeOn,
|
|
8139
|
+
setMode: runtimeSetMode,
|
|
7334
8140
|
provide: runtimeProvide, // spec §5.10 — register a view provider
|
|
7335
8141
|
setView: runtimeSetView, // spec §5.10 — activate/deactivate a render mode
|
|
7336
8142
|
describe: runtimeDescribe, // rwa-identity/1 — what this container is + can do
|
|
@@ -7351,6 +8157,12 @@ document.addEventListener('keydown', e => {
|
|
|
7351
8157
|
enumerable: true,
|
|
7352
8158
|
configurable: false,
|
|
7353
8159
|
});
|
|
8160
|
+
Object.defineProperty(window.runtime, 'mode', {
|
|
8161
|
+
get: () => rwaMode,
|
|
8162
|
+
enumerable: true,
|
|
8163
|
+
configurable: false,
|
|
8164
|
+
});
|
|
8165
|
+
startWorkspacePresence();
|
|
7354
8166
|
// §5.10: the presentation render mode ships ONLY for presentation
|
|
7355
8167
|
// containers. For every other kind this block is skipped entirely —
|
|
7356
8168
|
// activeView stays null, no provider is registered, no chrome is built,
|