rewritable 0.7.0 → 0.8.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/README.md +21 -1
- package/bin/rwa.mjs +47 -3
- package/package.json +1 -1
- package/seeds/rewritable.html +868 -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,148 @@ 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 safeWorkspaceHref(url) {
|
|
1333
|
+
// peer.url arrives over the public presence bus from another page in the origin,
|
|
1334
|
+
// so it is untrusted. escRuntimeHtml blocks attribute breakout but NOT a
|
|
1335
|
+
// `javascript:`/`data:` scheme, which would execute on click. Resolve and accept
|
|
1336
|
+
// only navigable web/file schemes; fall back to '#' for anything else or unparsable.
|
|
1337
|
+
try {
|
|
1338
|
+
const u = new URL(url, location.href);
|
|
1339
|
+
if (u.protocol === 'http:' || u.protocol === 'https:' || u.protocol === 'file:') return u.href;
|
|
1340
|
+
} catch (_) {}
|
|
1341
|
+
return '#';
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function workspacePresenceCard(peer, known) {
|
|
1345
|
+
const isKnown = workspacePeerKnown(peer, known);
|
|
1346
|
+
const href = safeWorkspaceHref(peer.url);
|
|
1347
|
+
const aff = peer.affordances && peer.affordances.length ? peer.affordances.map(a => a.kind).join(', ') : 'baseline';
|
|
1348
|
+
return '<a class="rwa-ws-card rwa-ws-live-card" href="' + escRuntimeHtml(href) + '">' +
|
|
1349
|
+
'<span class="rwa-ws-kind">' + escRuntimeHtml(peer.kind || 'document') + '</span>' +
|
|
1350
|
+
'<strong>' + escRuntimeHtml(peer.title || peer.file || 'Untitled') + '</strong>' +
|
|
1351
|
+
'<span>' + escRuntimeHtml(peer.file || peer.url || peer.uuid || '') + '</span>' +
|
|
1352
|
+
'<small>' + (isKnown ? 'indexed' : 'new since sync') + ' · open now · ' + escRuntimeHtml(aff) + '</small>' +
|
|
1353
|
+
'</a>';
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function renderWorkspacePresence() {
|
|
1357
|
+
if (PRODUCT_KIND !== 'workspace') return;
|
|
1358
|
+
const root = document.querySelector('[data-rwa-workspace-live]');
|
|
1359
|
+
if (!root) return;
|
|
1360
|
+
const grid = root.querySelector('[data-rwa-workspace-live-grid]');
|
|
1361
|
+
if (!grid) return;
|
|
1362
|
+
const cutoff = Date.now() - 45000;
|
|
1363
|
+
const peers = [...workspacePeers.values()]
|
|
1364
|
+
.filter(p => p && p.lastSeen >= cutoff && sameWorkspaceDirectory(p.url))
|
|
1365
|
+
.sort((a, b) => String(a.title || a.file || '').localeCompare(String(b.title || b.file || '')));
|
|
1366
|
+
root.hidden = peers.length === 0;
|
|
1367
|
+
if (!peers.length) { grid.innerHTML = ''; return; }
|
|
1368
|
+
const known = workspaceKnownKeys();
|
|
1369
|
+
grid.innerHTML = peers.map(p => workspacePresenceCard(p, known)).join('');
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
function handleWorkspacePresence(envelope) {
|
|
1373
|
+
const msg = envelope && envelope.message;
|
|
1374
|
+
if (!msg || msg.schema !== 'rwa-presence/1') return;
|
|
1375
|
+
if (msg.action === 'request') {
|
|
1376
|
+
runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('hello'));
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (PRODUCT_KIND !== 'workspace' || msg.action !== 'hello' || !sameWorkspaceDirectory(msg.url)) return;
|
|
1380
|
+
workspacePeers.set(msg.uuid || envelope.from, { ...msg, lastSeen: Date.now() });
|
|
1381
|
+
renderWorkspacePresence();
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function startWorkspacePresence() {
|
|
1385
|
+
try {
|
|
1386
|
+
workspacePresenceUnsub = runtimeBusSubscribe(RWA_WORKSPACE_PRESENCE_TOPIC, handleWorkspacePresence);
|
|
1387
|
+
runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('hello'));
|
|
1388
|
+
if (!(typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent || ''))) {
|
|
1389
|
+
workspacePresenceTimer = setInterval(() => {
|
|
1390
|
+
runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('hello'));
|
|
1391
|
+
if (PRODUCT_KIND === 'workspace') renderWorkspacePresence();
|
|
1392
|
+
}, 15000);
|
|
1393
|
+
if (workspacePresenceTimer && typeof workspacePresenceTimer.unref === 'function') workspacePresenceTimer.unref();
|
|
1394
|
+
}
|
|
1395
|
+
if (PRODUCT_KIND === 'workspace') {
|
|
1396
|
+
workspaceMonitorUnsub = () => {};
|
|
1397
|
+
setTimeout(() => runtimeBusPublish(RWA_WORKSPACE_PRESENCE_TOPIC, workspacePresencePayload('request')), 50);
|
|
1398
|
+
}
|
|
1399
|
+
} catch (_) {
|
|
1400
|
+
// BroadcastChannel is unavailable in some constrained contexts. The durable
|
|
1401
|
+
// workspace manifest still works; only live autodiscovery is disabled.
|
|
1402
|
+
}
|
|
1059
1403
|
}
|
|
1060
1404
|
|
|
1061
1405
|
// ─── URL fragment scroll (rwa-bootstrap 0.9) ───────────────────────
|
|
@@ -1116,6 +1460,12 @@ function buildFile(doc) {
|
|
|
1116
1460
|
function buildUI() {
|
|
1117
1461
|
document.getElementById('rwa-runtime').innerHTML = `
|
|
1118
1462
|
<div id="rwa-set">
|
|
1463
|
+
<div id="rwa-mode-tabs" role="tablist" aria-label="runtime mode">
|
|
1464
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="document">Document</button>
|
|
1465
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="edit">Edit</button>
|
|
1466
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="skills">Skills</button>
|
|
1467
|
+
<button class="rwa-mode-tab" type="button" data-rwa-mode-target="actions">Actions</button>
|
|
1468
|
+
</div>
|
|
1119
1469
|
<button class="rwa-st-btn" id="rwa-st-status">● ready</button>
|
|
1120
1470
|
<button class="rwa-st-btn" id="rwa-st-info" title="what is this?" aria-label="what is this?">ⓘ</button>
|
|
1121
1471
|
<button class="rwa-st-btn" id="rwa-st-cog">⚙</button>
|
|
@@ -1135,6 +1485,7 @@ function buildUI() {
|
|
|
1135
1485
|
<div id="rwa-info-panel"></div>
|
|
1136
1486
|
<div id="rwa-skin-panel"></div>
|
|
1137
1487
|
<div id="rwa-share-panel"></div>
|
|
1488
|
+
<div id="rwa-mode-panel"></div>
|
|
1138
1489
|
<div id="rwa-pal">
|
|
1139
1490
|
<div id="rwa-pal-box">
|
|
1140
1491
|
<div class="rwa-pal-top">
|
|
@@ -1156,6 +1507,12 @@ function buildUI() {
|
|
|
1156
1507
|
<div id="rwa-lens-paste-hint" hidden></div>
|
|
1157
1508
|
<div id="rwa-lens-hint"></div>
|
|
1158
1509
|
</div>
|
|
1510
|
+
<div id="rwa-selection-bar" hidden data-rwa-no-inline-edit>
|
|
1511
|
+
<button type="button" id="rwa-selection-bold" title="Bold selection" aria-label="bold selection"><strong>B</strong></button>
|
|
1512
|
+
<input id="rwa-selection-cmd" placeholder="make it bold" autocomplete="off" spellcheck="false">
|
|
1513
|
+
<button type="button" class="pri" id="rwa-selection-run">Run</button>
|
|
1514
|
+
<button type="button" id="rwa-selection-voice">Mic</button>
|
|
1515
|
+
</div>
|
|
1159
1516
|
<div id="rwa-lens-hist-panel" hidden></div>`;
|
|
1160
1517
|
|
|
1161
1518
|
const k = document.getElementById('rwa-key'), m = document.getElementById('rwa-model');
|
|
@@ -1163,6 +1520,7 @@ function buildUI() {
|
|
|
1163
1520
|
m.value = sessionStorage.getItem(RWA.K_MODEL) || RWA.MODEL;
|
|
1164
1521
|
k.oninput = e => sessionStorage.setItem(RWA.K_API, e.target.value.trim());
|
|
1165
1522
|
m.oninput = e => sessionStorage.setItem(RWA.K_MODEL, e.target.value.trim() || RWA.MODEL);
|
|
1523
|
+
attachModeTabs();
|
|
1166
1524
|
|
|
1167
1525
|
// Bridge SESSION backend config: the bearer token the bridge requires on every
|
|
1168
1526
|
// /session/* call, and a server-side working dir for the claude session.
|
|
@@ -1301,11 +1659,14 @@ function buildUI() {
|
|
|
1301
1659
|
opt.value = id;
|
|
1302
1660
|
modelOptsEl.appendChild(opt);
|
|
1303
1661
|
}
|
|
1304
|
-
//
|
|
1305
|
-
|
|
1662
|
+
// For local OpenAI-compatible backends, pick a discovered model when the
|
|
1663
|
+
// current field is empty, still the baked default, or belongs to another
|
|
1664
|
+
// backend. The datalist still carries every returned model for manual choice.
|
|
1665
|
+
const currentModel = (m.value || sessionStorage.getItem(RWA.K_MODEL) || '').trim();
|
|
1306
1666
|
const isLocal = backendEl.value === 'ollama' || backendEl.value === 'lmstudio';
|
|
1307
|
-
const
|
|
1308
|
-
|
|
1667
|
+
const shouldSuggestLocal = isLocal && models.length > 0
|
|
1668
|
+
&& (currentModel === '' || currentModel === RWA.MODEL || !models.includes(currentModel));
|
|
1669
|
+
if (shouldSuggestLocal) {
|
|
1309
1670
|
m.value = models[0];
|
|
1310
1671
|
sessionStorage.setItem(RWA.K_MODEL, models[0]);
|
|
1311
1672
|
}
|
|
@@ -1323,6 +1684,7 @@ function buildUI() {
|
|
|
1323
1684
|
document.getElementById('rwa-info-panel').classList.remove('open'); // panels are mutually exclusive
|
|
1324
1685
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1325
1686
|
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1687
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1326
1688
|
document.getElementById('rwa-set-panel').classList.toggle('open');
|
|
1327
1689
|
};
|
|
1328
1690
|
// ⓘ "what is this?" — render the rwa-identity/1 bundle as prose on open so it
|
|
@@ -1332,6 +1694,7 @@ function buildUI() {
|
|
|
1332
1694
|
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1333
1695
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1334
1696
|
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1697
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1335
1698
|
const panel = document.getElementById('rwa-info-panel');
|
|
1336
1699
|
if (!panel.classList.contains('open')) panel.innerHTML = renderInfoPanel();
|
|
1337
1700
|
panel.classList.toggle('open');
|
|
@@ -1351,6 +1714,7 @@ function buildUI() {
|
|
|
1351
1714
|
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1352
1715
|
document.getElementById('rwa-info-panel').classList.remove('open');
|
|
1353
1716
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1717
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1354
1718
|
renderSharePanel().then(() => panel.classList.add('open'));
|
|
1355
1719
|
};
|
|
1356
1720
|
document.getElementById('rwa-st-commit').onclick = commit;
|
|
@@ -1460,6 +1824,24 @@ function buildUI() {
|
|
|
1460
1824
|
});
|
|
1461
1825
|
}
|
|
1462
1826
|
|
|
1827
|
+
const selCmd = document.getElementById('rwa-selection-cmd');
|
|
1828
|
+
document.getElementById('rwa-selection-bold').addEventListener('click', () => runSelectionCommand('make it bold', { actor: 'user:selection-command' }).catch(() => {}));
|
|
1829
|
+
document.getElementById('rwa-selection-run').addEventListener('click', () => {
|
|
1830
|
+
runSelectionCommand(selCmd.value || '', { actor: 'user:selection-command' }).catch(() => {});
|
|
1831
|
+
});
|
|
1832
|
+
document.getElementById('rwa-selection-voice').addEventListener('click', () => startSelectionVoice());
|
|
1833
|
+
selCmd.addEventListener('keydown', (e) => {
|
|
1834
|
+
if (e.key === 'Enter') {
|
|
1835
|
+
e.preventDefault();
|
|
1836
|
+
runSelectionCommand(selCmd.value || '', { actor: 'user:selection-command' }).catch(() => {});
|
|
1837
|
+
} else if (e.key === 'Escape') {
|
|
1838
|
+
hideSelectionCommandBar();
|
|
1839
|
+
}
|
|
1840
|
+
});
|
|
1841
|
+
document.addEventListener('selectionchange', scheduleSelectionCommandRefresh);
|
|
1842
|
+
document.addEventListener('mouseup', scheduleSelectionCommandRefresh);
|
|
1843
|
+
document.addEventListener('keyup', scheduleSelectionCommandRefresh);
|
|
1844
|
+
|
|
1463
1845
|
// rwa-lens/1: Esc releases the anchor when one is held. Listener is on
|
|
1464
1846
|
// `window` so Esc anywhere works (including with focus outside the lens
|
|
1465
1847
|
// input — e.g. on the doc itself after a click).
|
|
@@ -1651,6 +2033,139 @@ async function renderHistoryPanel(panel) {
|
|
|
1651
2033
|
if (closeBtn) closeBtn.addEventListener('click', () => { panel.hidden = true; });
|
|
1652
2034
|
}
|
|
1653
2035
|
|
|
2036
|
+
function renderSkillsModePanel(panel) {
|
|
2037
|
+
if (!isRwaSkillHost()) {
|
|
2038
|
+
panel.innerHTML = [
|
|
2039
|
+
'<div class="rwa-mode-section">',
|
|
2040
|
+
'<div class="rwa-mode-kicker">Skills</div>',
|
|
2041
|
+
'<h4>Skill runtime unavailable</h4>',
|
|
2042
|
+
'<p>This file does not include the skill runtime.</p>',
|
|
2043
|
+
'</div>',
|
|
2044
|
+
].join('');
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
2047
|
+
const canInstall = typeof runtimeInstallSkill === 'function' && typeof runtimeUninstallSkill === 'function' && typeof runtimeInvokeSkill === 'function';
|
|
2048
|
+
const skills = runtimeListSkills();
|
|
2049
|
+
const rows = skills.length ? skills.map(s => {
|
|
2050
|
+
const id = escRuntimeHtml(s.skillId);
|
|
2051
|
+
const name = escRuntimeHtml(s.name || s.skillId);
|
|
2052
|
+
const kind = escRuntimeHtml(s.kind || 'skill');
|
|
2053
|
+
const verified = s.verified ? 'verified' : 'unsigned/unverified';
|
|
2054
|
+
return [
|
|
2055
|
+
'<div class="rwa-mode-row" data-skill-id="' + id + '">',
|
|
2056
|
+
'<div style="min-width:0;flex:1">',
|
|
2057
|
+
'<div class="rwa-mode-title">' + name + '</div>',
|
|
2058
|
+
'<div class="rwa-mode-meta">' + kind + ' · ' + escRuntimeHtml(verified) + ' · ' + id + '</div>',
|
|
2059
|
+
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>' : '',
|
|
2060
|
+
'<div class="rwa-mode-result" data-skill-result="' + id + '" hidden></div>',
|
|
2061
|
+
'</div>',
|
|
2062
|
+
canInstall ? '<button type="button" data-skill-uninstall="' + id + '">Uninstall</button>' : '',
|
|
2063
|
+
'</div>',
|
|
2064
|
+
].join('');
|
|
2065
|
+
}).join('') : '<div class="rwa-mode-empty">No skills installed.</div>';
|
|
2066
|
+
panel.innerHTML = [
|
|
2067
|
+
'<div class="rwa-mode-section">',
|
|
2068
|
+
'<div class="rwa-mode-kicker">Skills</div>',
|
|
2069
|
+
'<h4>Installed skills</h4>',
|
|
2070
|
+
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>',
|
|
2071
|
+
'</div>',
|
|
2072
|
+
'<div class="rwa-mode-section">' + rows + '</div>',
|
|
2073
|
+
].join('');
|
|
2074
|
+
const install = panel.querySelector('#rwa-skills-install');
|
|
2075
|
+
if (install) install.addEventListener('click', async () => {
|
|
2076
|
+
const out = await runtimePromptInstall();
|
|
2077
|
+
renderSkillsModePanel(panel);
|
|
2078
|
+
if (out && out.ok === false) setStatus('err', '✗ skill install failed');
|
|
2079
|
+
});
|
|
2080
|
+
panel.querySelectorAll('[data-skill-uninstall]').forEach(btn => {
|
|
2081
|
+
btn.addEventListener('click', async () => {
|
|
2082
|
+
const id = btn.getAttribute('data-skill-uninstall');
|
|
2083
|
+
const out = await runtimeUninstallSkill(id);
|
|
2084
|
+
if (out && out.ok === false) setStatus('err', '✗ skill uninstall failed');
|
|
2085
|
+
renderSkillsModePanel(panel);
|
|
2086
|
+
});
|
|
2087
|
+
});
|
|
2088
|
+
panel.querySelectorAll('[data-skill-invoke]').forEach(btn => {
|
|
2089
|
+
btn.addEventListener('click', async () => {
|
|
2090
|
+
const id = btn.getAttribute('data-skill-invoke');
|
|
2091
|
+
const row = btn.closest('[data-skill-id]');
|
|
2092
|
+
const inputEl = row && row.querySelector('[data-skill-input]');
|
|
2093
|
+
const resultEl = row && row.querySelector('[data-skill-result]');
|
|
2094
|
+
let input = {};
|
|
2095
|
+
try {
|
|
2096
|
+
const raw = (inputEl && inputEl.value || '').trim();
|
|
2097
|
+
input = raw ? JSON.parse(raw) : {};
|
|
2098
|
+
} catch (e) {
|
|
2099
|
+
if (resultEl) { resultEl.hidden = false; resultEl.textContent = 'Invalid JSON input'; }
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
btn.disabled = true;
|
|
2103
|
+
try {
|
|
2104
|
+
const out = await runtimeInvokeSkill(id, input);
|
|
2105
|
+
if (resultEl) { resultEl.hidden = false; resultEl.textContent = JSON.stringify(out, null, 2); }
|
|
2106
|
+
} catch (e) {
|
|
2107
|
+
if (resultEl) { resultEl.hidden = false; resultEl.textContent = e?.message || String(e); }
|
|
2108
|
+
} finally {
|
|
2109
|
+
btn.disabled = false;
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
});
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
async function renderActionsModePanel(panel) {
|
|
2116
|
+
const d = runtimeDescribe();
|
|
2117
|
+
let entries = [];
|
|
2118
|
+
try { entries = (await idbGet(RWA.HIST)) || []; } catch (_) { entries = []; }
|
|
2119
|
+
if (rwaMode !== 'actions') return;
|
|
2120
|
+
const viewAff = d.affordances.find(a => a.kind === 'view' && a.verified === true);
|
|
2121
|
+
const skillAffs = d.affordances.filter(a => a.provenance === 'installed' && a.skillId);
|
|
2122
|
+
const providerAffs = d.affordances.filter(a => a.provenance !== 'installed');
|
|
2123
|
+
const fmtTs = ts => { try { return new Date(ts).toLocaleString(); } catch (_) { return String(ts); } };
|
|
2124
|
+
const histRows = entries.length ? entries.slice(0, 12).map(e => {
|
|
2125
|
+
const surface = e.surface || e.kind || 'edit';
|
|
2126
|
+
const actor = e.actor ? ' · ' + e.actor : '';
|
|
2127
|
+
const instr = e.instruction || e.reason || '(no instruction recorded)';
|
|
2128
|
+
return '<div class="rwa-mode-hist"><div class="rwa-mode-meta">' + escRuntimeHtml(surface + actor + ' · ' + fmtTs(e.ts)) + '</div><div>' + escRuntimeHtml(instr) + '</div></div>';
|
|
2129
|
+
}).join('') : '<div class="rwa-mode-empty">No runs yet.</div>';
|
|
2130
|
+
const affRows = providerAffs.length ? providerAffs.map(a => {
|
|
2131
|
+
const detail = [a.kind, a.verified ? 'verified' : 'declared'].join(' · ');
|
|
2132
|
+
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>';
|
|
2133
|
+
}).join('') : '<div class="rwa-mode-empty">No declared live affordances.</div>';
|
|
2134
|
+
const skillRows = skillAffs.length ? skillAffs.map(a =>
|
|
2135
|
+
'<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>'
|
|
2136
|
+
).join('') : '<div class="rwa-mode-empty">No installed skills.</div>';
|
|
2137
|
+
panel.innerHTML = [
|
|
2138
|
+
'<div class="rwa-mode-section">',
|
|
2139
|
+
'<div class="rwa-mode-kicker">Actions</div>',
|
|
2140
|
+
'<h4>Action center</h4>',
|
|
2141
|
+
'<div class="rwa-mode-actions">',
|
|
2142
|
+
'<button type="button" id="rwa-actions-undo">Undo</button>',
|
|
2143
|
+
'<button type="button" class="pri" id="rwa-actions-save">Save / Export</button>',
|
|
2144
|
+
'<button type="button" id="rwa-actions-share">Share</button>',
|
|
2145
|
+
viewAff ? '<button type="button" id="rwa-actions-view">' + escRuntimeHtml(d.activeView === viewAff.name ? 'Exit ' + viewAff.label : viewAff.label) + '</button>' : '',
|
|
2146
|
+
'</div>',
|
|
2147
|
+
'</div>',
|
|
2148
|
+
'<div class="rwa-mode-section"><div class="rwa-mode-kicker">Recent runs</div>' + histRows + '</div>',
|
|
2149
|
+
'<div class="rwa-mode-section"><div class="rwa-mode-kicker">Live affordances</div>' + affRows + '</div>',
|
|
2150
|
+
'<div class="rwa-mode-section"><div class="rwa-mode-kicker">Installed skill actions</div>' + skillRows + '</div>',
|
|
2151
|
+
].join('');
|
|
2152
|
+
const undoBtn = panel.querySelector('#rwa-actions-undo');
|
|
2153
|
+
if (undoBtn) undoBtn.addEventListener('click', () => runtimeUndo());
|
|
2154
|
+
const saveBtn = panel.querySelector('#rwa-actions-save');
|
|
2155
|
+
if (saveBtn) saveBtn.addEventListener('click', () => runtimeCommit());
|
|
2156
|
+
const shareBtn = panel.querySelector('#rwa-actions-share');
|
|
2157
|
+
if (shareBtn) shareBtn.addEventListener('click', async () => {
|
|
2158
|
+
document.getElementById('rwa-mode-panel')?.classList.remove('open');
|
|
2159
|
+
await renderSharePanel();
|
|
2160
|
+
document.getElementById('rwa-share-panel')?.classList.add('open');
|
|
2161
|
+
});
|
|
2162
|
+
const viewBtn = panel.querySelector('#rwa-actions-view');
|
|
2163
|
+
if (viewBtn && viewAff) viewBtn.addEventListener('click', () => runtimeSetView(d.activeView === viewAff.name ? null : viewAff.name));
|
|
2164
|
+
panel.querySelectorAll('[data-action-skill]').forEach(btn => {
|
|
2165
|
+
btn.addEventListener('click', () => runtimeSetMode('skills'));
|
|
2166
|
+
});
|
|
2167
|
+
}
|
|
2168
|
+
|
|
1654
2169
|
// ─── Agent (rwa-edit/1) ─────────────────────────────────────────────
|
|
1655
2170
|
// Spec: rwa-edit-spec.md (v1.4). The agent edits the doc via tool calls,
|
|
1656
2171
|
// not by emitting a full rewritten document. unchanged regions are byte-
|
|
@@ -1834,6 +2349,15 @@ The stored document is ordinary prose HTML inside a single <article>: <h1>/<h2>
|
|
|
1834
2349
|
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
2350
|
|
|
1836
2351
|
Preserve every existing data-rwa-id verbatim (the runtime assigns them). Anchor edits on unique text near the heading you mean.
|
|
2352
|
+
${SYSTEM_PROMPT_RULES}`,
|
|
2353
|
+
|
|
2354
|
+
workspace: `You are editing a rewritable workspace index. Apply the user's request as a small set of surgical edits via tool calls.
|
|
2355
|
+
|
|
2356
|
+
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.
|
|
2357
|
+
|
|
2358
|
+
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.
|
|
2359
|
+
|
|
2360
|
+
When changing the dashboard, keep links to sibling documents relative, and preserve every existing data-rwa-id verbatim.
|
|
1837
2361
|
${SYSTEM_PROMPT_RULES}`,
|
|
1838
2362
|
};
|
|
1839
2363
|
// rwa:extract:end SYSTEM_PROMPTS
|
|
@@ -2589,6 +3113,10 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2589
3113
|
// live mount produces the same order via the same outer-wins traversal — so
|
|
2590
3114
|
// the i-th live anchorable corresponds to map[i].
|
|
2591
3115
|
function handleMountClick(e) {
|
|
3116
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3117
|
+
// Pointer-down may already have opened a WYSIWYG inline edit. In that case
|
|
3118
|
+
// the click belongs to caret placement, not to lens anchoring.
|
|
3119
|
+
if (inlineEdit) return;
|
|
2592
3120
|
// Audit R3 (scoped): respect the per-kind click-to-anchor flag. When
|
|
2593
3121
|
// false (e.g. workflow files), all clicks pass through without anchoring
|
|
2594
3122
|
// — the lens stays in default state and every command runs against the
|
|
@@ -2749,10 +3277,11 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2749
3277
|
}
|
|
2750
3278
|
|
|
2751
3279
|
// ─── Inline manual edit (edit-surface: direct, no-LLM block editing) ──────
|
|
2752
|
-
//
|
|
3280
|
+
// Click a leaf text block and type in place; Enter or blur
|
|
2753
3281
|
// commits through the existing non-agent commit path (runtimeApplyEnvelope,
|
|
2754
|
-
// actor 'user:edit-surface') — no model call, works offline.
|
|
2755
|
-
// still
|
|
3282
|
+
// actor 'user:edit-surface') — no model call, works offline. Container clicks
|
|
3283
|
+
// still anchor the lens (handleMountClick); leaf text clicks become WYSIWYG
|
|
3284
|
+
// edit sessions. Double-click remains a compatibility path. Design:
|
|
2756
3285
|
// docs/plans/2026-06-08-inline-manual-edit-design.md.
|
|
2757
3286
|
//
|
|
2758
3287
|
// Editable set = the leaf-text members of ANCHORABLE_TAGS, so the
|
|
@@ -2760,6 +3289,7 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2760
3289
|
// containers; FIGCAPTION is not independently anchorable (lives in FIGURE).
|
|
2761
3290
|
const INLINE_EDITABLE = new Set(['P','H1','H2','H3','H4','H5','H6','BLOCKQUOTE','LI','TD']);
|
|
2762
3291
|
let inlineEdit = null; // { el, entry } while a block is being hand-edited
|
|
3292
|
+
const INLINE_EDIT_BYPASS = 'button,input,textarea,select,option,summary,[contenteditable="true"],[data-rwa-no-inline-edit]';
|
|
2763
3293
|
|
|
2764
3294
|
// Controlled serializer: turn an edited contenteditable node into a clean
|
|
2765
3295
|
// replace string. Emits ONLY escaped text and <br> (the Shift+Enter soft
|
|
@@ -2779,6 +3309,270 @@ function serializeLeafSafe(el) {
|
|
|
2779
3309
|
}
|
|
2780
3310
|
window.serializeLeafSafe = serializeLeafSafe;
|
|
2781
3311
|
|
|
3312
|
+
// ─── Selection command surface (typed or voice) ─────────────────────
|
|
3313
|
+
// Edit-mode-only layer over a user text selection. Deterministic commands
|
|
3314
|
+
// such as "make it bold" compile to rwa-edit/1 locally; voice recognition is
|
|
3315
|
+
// only an input method that fills/runs the same command path.
|
|
3316
|
+
let selectionCommandState = null; // { el, entry, text, occurrence, range }
|
|
3317
|
+
let selectionVoiceRecognizer = null;
|
|
3318
|
+
|
|
3319
|
+
function hideSelectionCommandBar() {
|
|
3320
|
+
selectionCommandState = null;
|
|
3321
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3322
|
+
if (bar) {
|
|
3323
|
+
bar.hidden = true;
|
|
3324
|
+
delete bar.dataset.listening;
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
function nodeElement(n) {
|
|
3329
|
+
return n && (n.nodeType === 1 ? n : n.parentElement);
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
function closestSelectionLeaf(node, mount) {
|
|
3333
|
+
let el = nodeElement(node);
|
|
3334
|
+
while (el && el !== mount && !INLINE_EDITABLE.has(el.tagName)) el = el.parentElement;
|
|
3335
|
+
return (el && el !== mount) ? el : null;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
function countTextOccurrences(haystack, needle) {
|
|
3339
|
+
if (!needle) return 0;
|
|
3340
|
+
let n = 0, i = 0;
|
|
3341
|
+
while ((i = haystack.indexOf(needle, i)) !== -1) { n++; i += needle.length; }
|
|
3342
|
+
return n;
|
|
3343
|
+
}
|
|
3344
|
+
|
|
3345
|
+
function nthIndexOf(haystack, needle, nth) {
|
|
3346
|
+
let i = -1, from = 0;
|
|
3347
|
+
for (let n = 0; n <= nth; n++) {
|
|
3348
|
+
i = haystack.indexOf(needle, from);
|
|
3349
|
+
if (i < 0) return -1;
|
|
3350
|
+
from = i + needle.length;
|
|
3351
|
+
}
|
|
3352
|
+
return i;
|
|
3353
|
+
}
|
|
3354
|
+
|
|
3355
|
+
function resolveSelectionCommandTarget() {
|
|
3356
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return null;
|
|
3357
|
+
const mount = document.getElementById('rwa-doc-mount');
|
|
3358
|
+
const sel = window.getSelection && window.getSelection();
|
|
3359
|
+
if (!mount || !sel || sel.rangeCount === 0 || sel.isCollapsed) return null;
|
|
3360
|
+
const range = sel.getRangeAt(0);
|
|
3361
|
+
const text = sel.toString();
|
|
3362
|
+
if (!text || !text.trim()) return null;
|
|
3363
|
+
if (!mount.contains(range.commonAncestorContainer)) return null;
|
|
3364
|
+
const startLeaf = closestSelectionLeaf(range.startContainer, mount);
|
|
3365
|
+
const endLeaf = closestSelectionLeaf(range.endContainer, mount);
|
|
3366
|
+
if (!startLeaf || startLeaf !== endLeaf) return null;
|
|
3367
|
+
if (startLeaf.closest('[data-rwa-frozen]') || startLeaf.closest('.rwa-locked')) return null;
|
|
3368
|
+
const ord = anchorableOrdinal(mount, startLeaf);
|
|
3369
|
+
const entry = (ord >= 0 && sourceMap) ? sourceMap[ord] : null;
|
|
3370
|
+
if (!entry || isWithinLockedRange(entry.start, entry.end)) return null;
|
|
3371
|
+
let occurrence = 0;
|
|
3372
|
+
try {
|
|
3373
|
+
const prefix = document.createRange();
|
|
3374
|
+
prefix.selectNodeContents(startLeaf);
|
|
3375
|
+
prefix.setEnd(range.startContainer, range.startOffset);
|
|
3376
|
+
occurrence = countTextOccurrences(prefix.toString(), text);
|
|
3377
|
+
} catch (_) { occurrence = 0; }
|
|
3378
|
+
return { el: startLeaf, entry, text, occurrence, range };
|
|
3379
|
+
}
|
|
3380
|
+
|
|
3381
|
+
function locateSelectionSource(target) {
|
|
3382
|
+
const block = currentDocCache.slice(target.entry.start, target.entry.end);
|
|
3383
|
+
const tag = target.entry.tag.toLowerCase();
|
|
3384
|
+
const openEnd = block.indexOf('>');
|
|
3385
|
+
const closeRe = new RegExp('</' + tag + '\\s*>\\s*$', 'i');
|
|
3386
|
+
const closeMatch = closeRe.exec(block);
|
|
3387
|
+
if (openEnd < 0 || !closeMatch) return null;
|
|
3388
|
+
const innerStart = openEnd + 1;
|
|
3389
|
+
const innerEnd = closeMatch.index;
|
|
3390
|
+
const inner = block.slice(innerStart, innerEnd);
|
|
3391
|
+
const candidates = [];
|
|
3392
|
+
const plain = escapeHtml(target.text);
|
|
3393
|
+
const br = plain.replace(/\r\n|\r|\n/g, '<br>');
|
|
3394
|
+
candidates.push(br);
|
|
3395
|
+
if (plain !== br) candidates.push(plain);
|
|
3396
|
+
for (const selectedSource of candidates) {
|
|
3397
|
+
const idx = nthIndexOf(inner, selectedSource, target.occurrence || 0);
|
|
3398
|
+
if (idx >= 0) {
|
|
3399
|
+
return {
|
|
3400
|
+
block,
|
|
3401
|
+
selectedSource,
|
|
3402
|
+
start: innerStart + idx,
|
|
3403
|
+
end: innerStart + idx + selectedSource.length,
|
|
3404
|
+
};
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
return null;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
function parseSelectionCommand(raw) {
|
|
3411
|
+
const t = String(raw || '').trim().toLowerCase();
|
|
3412
|
+
if (!t) return null;
|
|
3413
|
+
if (/\b(bold|strong|emphasize strongly)\b/.test(t)) return { kind: 'wrap', tag: 'strong', label: 'bold' };
|
|
3414
|
+
if (/\b(italic|italics|emphasize)\b/.test(t)) return { kind: 'wrap', tag: 'em', label: 'italic' };
|
|
3415
|
+
if (/\b(code|monospace|inline code)\b/.test(t)) return { kind: 'wrap', tag: 'code', label: 'code' };
|
|
3416
|
+
return null;
|
|
3417
|
+
}
|
|
3418
|
+
|
|
3419
|
+
async function applySelectionWrap(target, action, actor, instruction) {
|
|
3420
|
+
if (inlineEdit && serializeLeafSafe(inlineEdit.el) !== inlineEdit.original) {
|
|
3421
|
+
showAffordance('finish the current edit first');
|
|
3422
|
+
return { ok: false, reason: 'inline_edit_dirty' };
|
|
3423
|
+
}
|
|
3424
|
+
if (inlineEdit) exitInlineEdit();
|
|
3425
|
+
const loc = locateSelectionSource(target);
|
|
3426
|
+
if (!loc) {
|
|
3427
|
+
showAffordance('selection could not be mapped to source');
|
|
3428
|
+
return { ok: false, reason: 'selection_unmapped' };
|
|
3429
|
+
}
|
|
3430
|
+
const a = resolveAnchorFind(target.entry);
|
|
3431
|
+
if (!a) {
|
|
3432
|
+
showAffordance('selection block is ambiguous');
|
|
3433
|
+
return { ok: false, reason: 'anchor_unresolved' };
|
|
3434
|
+
}
|
|
3435
|
+
const tag = action.tag;
|
|
3436
|
+
const nextBlock = loc.block.slice(0, loc.start) + '<' + tag + '>' + loc.selectedSource + '</' + tag + '>' + loc.block.slice(loc.end);
|
|
3437
|
+
const replace = a.replacePrefix + nextBlock + a.replaceSuffix;
|
|
3438
|
+
await runtimeApplyEnvelope(
|
|
3439
|
+
{ version: 'rwa-edit/1', edits: [{ find: a.find, replace, reason: 'selection:' + action.label }] },
|
|
3440
|
+
{ surface: 'selection-edit', instruction: instruction || action.label, actor: actor || 'user:selection-command' });
|
|
3441
|
+
try { window.getSelection().removeAllRanges(); } catch (_) {}
|
|
3442
|
+
hideSelectionCommandBar();
|
|
3443
|
+
showAffordance('selection: ' + action.label + ' — ⌘Z to undo');
|
|
3444
|
+
return { ok: true };
|
|
3445
|
+
}
|
|
3446
|
+
|
|
3447
|
+
async function runSelectionCommand(raw, options) {
|
|
3448
|
+
options = options || {};
|
|
3449
|
+
const target = resolveSelectionCommandTarget() || selectionCommandState;
|
|
3450
|
+
if (!target) {
|
|
3451
|
+
showAffordance('select text in Edit mode first');
|
|
3452
|
+
return { ok: false, reason: 'no_selection' };
|
|
3453
|
+
}
|
|
3454
|
+
const action = parseSelectionCommand(raw);
|
|
3455
|
+
if (!action) {
|
|
3456
|
+
showAffordance('selection command not available yet');
|
|
3457
|
+
return { ok: false, reason: 'unknown_command' };
|
|
3458
|
+
}
|
|
3459
|
+
if (action.kind === 'wrap') return applySelectionWrap(target, action, options.actor, raw);
|
|
3460
|
+
return { ok: false, reason: 'unknown_command' };
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function positionSelectionCommandBar(target) {
|
|
3464
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3465
|
+
if (!bar || !target || !target.range) return;
|
|
3466
|
+
let rect = null;
|
|
3467
|
+
try { rect = target.range.getBoundingClientRect(); } catch (_) {}
|
|
3468
|
+
if (!rect || (rect.width === 0 && rect.height === 0)) {
|
|
3469
|
+
try { rect = target.el.getBoundingClientRect(); } catch (_) {}
|
|
3470
|
+
}
|
|
3471
|
+
const top = Math.max(8, window.scrollY + (rect ? rect.top : 0) - 42);
|
|
3472
|
+
const left = Math.max(8, Math.min(window.scrollX + (rect ? rect.left : 0), window.scrollX + window.innerWidth - 320));
|
|
3473
|
+
bar.style.top = top + 'px';
|
|
3474
|
+
bar.style.left = left + 'px';
|
|
3475
|
+
bar.hidden = false;
|
|
3476
|
+
}
|
|
3477
|
+
|
|
3478
|
+
function refreshSelectionCommandBar() {
|
|
3479
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3480
|
+
if (bar && bar.contains(document.activeElement)) return;
|
|
3481
|
+
const target = resolveSelectionCommandTarget();
|
|
3482
|
+
if (!target) { hideSelectionCommandBar(); return; }
|
|
3483
|
+
selectionCommandState = target;
|
|
3484
|
+
positionSelectionCommandBar(target);
|
|
3485
|
+
}
|
|
3486
|
+
|
|
3487
|
+
let selectionRefreshQueued = false;
|
|
3488
|
+
function scheduleSelectionCommandRefresh() {
|
|
3489
|
+
if (selectionRefreshQueued) return;
|
|
3490
|
+
selectionRefreshQueued = true;
|
|
3491
|
+
requestAnimationFrame(() => {
|
|
3492
|
+
selectionRefreshQueued = false;
|
|
3493
|
+
refreshSelectionCommandBar();
|
|
3494
|
+
});
|
|
3495
|
+
}
|
|
3496
|
+
|
|
3497
|
+
function startSelectionVoice() {
|
|
3498
|
+
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
|
|
3499
|
+
const bar = document.getElementById('rwa-selection-bar');
|
|
3500
|
+
const input = document.getElementById('rwa-selection-cmd');
|
|
3501
|
+
if (!selectionCommandState && !resolveSelectionCommandTarget()) {
|
|
3502
|
+
showAffordance('select text first');
|
|
3503
|
+
return;
|
|
3504
|
+
}
|
|
3505
|
+
if (!SR) {
|
|
3506
|
+
showAffordance('voice input is not supported in this browser');
|
|
3507
|
+
if (input) input.focus();
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
try {
|
|
3511
|
+
if (selectionVoiceRecognizer) selectionVoiceRecognizer.abort();
|
|
3512
|
+
} catch (_) {}
|
|
3513
|
+
const rec = new SR();
|
|
3514
|
+
selectionVoiceRecognizer = rec;
|
|
3515
|
+
rec.lang = navigator.language || 'en-US';
|
|
3516
|
+
rec.interimResults = false;
|
|
3517
|
+
rec.maxAlternatives = 1;
|
|
3518
|
+
rec.onstart = () => { if (bar) bar.dataset.listening = '1'; };
|
|
3519
|
+
rec.onresult = (e) => {
|
|
3520
|
+
const text = e?.results?.[0]?.[0]?.transcript || '';
|
|
3521
|
+
if (input) input.value = text;
|
|
3522
|
+
runSelectionCommand(text, { actor: 'user:voice-selection' }).catch(err => showAffordance('voice: ' + (err?.message || err)));
|
|
3523
|
+
};
|
|
3524
|
+
rec.onerror = (e) => showAffordance('voice: ' + (e?.error || 'could not hear command'));
|
|
3525
|
+
rec.onend = () => { if (bar) delete bar.dataset.listening; selectionVoiceRecognizer = null; };
|
|
3526
|
+
rec.start();
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
3530
|
+
window.__refreshSelectionCommandBar = refreshSelectionCommandBar;
|
|
3531
|
+
window.__runSelectionCommand = runSelectionCommand;
|
|
3532
|
+
window.__startSelectionVoice = startSelectionVoice;
|
|
3533
|
+
window.__parseSelectionCommand = parseSelectionCommand;
|
|
3534
|
+
}
|
|
3535
|
+
|
|
3536
|
+
function resolveInlineEditTarget(target) {
|
|
3537
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return null;
|
|
3538
|
+
const mount = document.getElementById('rwa-doc-mount');
|
|
3539
|
+
if (!mount || !target) return null;
|
|
3540
|
+
if (target.closest && target.closest(INLINE_EDIT_BYPASS)) return null;
|
|
3541
|
+
let el = target;
|
|
3542
|
+
while (el && el !== mount && !INLINE_EDITABLE.has(el.tagName)) el = el.parentNode;
|
|
3543
|
+
if (!el || el === mount) return null;
|
|
3544
|
+
if (el.closest('[data-rwa-frozen]') || el.closest('.rwa-locked')) return null;
|
|
3545
|
+
const ord = anchorableOrdinal(mount, el);
|
|
3546
|
+
const entry = (ord >= 0 && sourceMap) ? sourceMap[ord] : null;
|
|
3547
|
+
// A nested anchorable (an inner <li>, or a <p> inside a <td>) is not recorded
|
|
3548
|
+
// by the outer-wins descent, so ord is -1 and we no-op — consistent with the
|
|
3549
|
+
// lens's anchoring model, which can't target sub-blocks either.
|
|
3550
|
+
if (!entry) return null;
|
|
3551
|
+
if (isWithinLockedRange(entry.start, entry.end)) return null; // marker-form frozen backstop
|
|
3552
|
+
return { el, entry };
|
|
3553
|
+
}
|
|
3554
|
+
|
|
3555
|
+
function refreshInlineEditAffordances(mount) {
|
|
3556
|
+
if (!mount) return;
|
|
3557
|
+
mount.querySelectorAll('.rwa-editable-leaf').forEach(el => {
|
|
3558
|
+
el.classList.remove('rwa-editable-leaf');
|
|
3559
|
+
if (el.getAttribute('tabindex') === '0' && el.dataset.rwaEditTab === '1') {
|
|
3560
|
+
el.removeAttribute('tabindex');
|
|
3561
|
+
delete el.dataset.rwaEditTab;
|
|
3562
|
+
}
|
|
3563
|
+
});
|
|
3564
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3565
|
+
mount.querySelectorAll(Array.from(INLINE_EDITABLE).map(t => t.toLowerCase()).join(',')).forEach(el => {
|
|
3566
|
+
const hit = resolveInlineEditTarget(el);
|
|
3567
|
+
if (!hit || hit.el !== el) return;
|
|
3568
|
+
el.classList.add('rwa-editable-leaf');
|
|
3569
|
+
if (!el.hasAttribute('tabindex')) {
|
|
3570
|
+
el.setAttribute('tabindex', '0');
|
|
3571
|
+
el.dataset.rwaEditTab = '1';
|
|
3572
|
+
}
|
|
3573
|
+
});
|
|
3574
|
+
}
|
|
3575
|
+
|
|
2782
3576
|
function enterInlineEdit(el, entry) {
|
|
2783
3577
|
if (inlineEdit) exitInlineEdit();
|
|
2784
3578
|
inlineEdit = { el, entry, original: '', commandMode: false, demoted: false };
|
|
@@ -2996,34 +3790,39 @@ async function commitInlineEdit() {
|
|
|
2996
3790
|
}
|
|
2997
3791
|
window.commitInlineEdit = commitInlineEdit;
|
|
2998
3792
|
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3793
|
+
function startInlineEditFromEvent(e) {
|
|
3794
|
+
if (rwaMode !== 'edit' || activeView) return false;
|
|
3795
|
+
if (inlineEdit) return false;
|
|
3796
|
+
const hit = resolveInlineEditTarget(e && e.target);
|
|
3797
|
+
if (!hit) return false;
|
|
3798
|
+
enterInlineEdit(hit.el, hit.entry);
|
|
3799
|
+
return true;
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
// Pointer-down opens the edit before the browser performs its default caret
|
|
3803
|
+
// placement for the ensuing click. This is the WYSIWYG path: click text, type.
|
|
3804
|
+
function handleMountPointerDown(e) {
|
|
3805
|
+
if (e.button != null && e.button !== 0) return;
|
|
3806
|
+
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
|
|
3807
|
+
startInlineEditFromEvent(e);
|
|
3808
|
+
}
|
|
3809
|
+
|
|
3810
|
+
// Double-click compatibility for existing behavior and tests.
|
|
3004
3811
|
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);
|
|
3812
|
+
startInlineEditFromEvent(e);
|
|
3813
|
+
}
|
|
3814
|
+
|
|
3815
|
+
function handleMountEditKeydown(e) {
|
|
3816
|
+
if (inlineEdit) return;
|
|
3817
|
+
if (e.key !== 'Enter' && e.key !== 'F2') return;
|
|
3818
|
+
const hit = resolveInlineEditTarget(e.target);
|
|
3819
|
+
if (!hit || hit.el !== e.target) return;
|
|
3820
|
+
e.preventDefault();
|
|
3821
|
+
enterInlineEdit(hit.el, hit.entry);
|
|
3024
3822
|
}
|
|
3025
3823
|
if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
3026
3824
|
window.__handleMountDblClick = handleMountDblClick;
|
|
3825
|
+
window.__handleMountPointerDown = handleMountPointerDown;
|
|
3027
3826
|
}
|
|
3028
3827
|
|
|
3029
3828
|
// ─── Image ingestion (images-v1) ──────────────────────────────────────
|
|
@@ -3211,7 +4010,7 @@ function dragHasFiles(e) {
|
|
|
3211
4010
|
return Array.from(t.types || []).includes('Files');
|
|
3212
4011
|
}
|
|
3213
4012
|
function handleMountDragOver(e) {
|
|
3214
|
-
if (modifyMutex || !dragHasFiles(e)) return;
|
|
4013
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex || !dragHasFiles(e)) return;
|
|
3215
4014
|
e.preventDefault();
|
|
3216
4015
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
|
3217
4016
|
clearDropMark();
|
|
@@ -3226,6 +4025,7 @@ function handleMountDragLeave(e) {
|
|
|
3226
4025
|
if (!m || !e.relatedTarget || !m.contains(e.relatedTarget)) clearDropMark();
|
|
3227
4026
|
}
|
|
3228
4027
|
function handleMountDrop(e) {
|
|
4028
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3229
4029
|
if (!dragHasFiles(e)) return;
|
|
3230
4030
|
e.preventDefault();
|
|
3231
4031
|
const target = findImageDropTarget(e);
|
|
@@ -3240,7 +4040,7 @@ function handleMountDrop(e) {
|
|
|
3240
4040
|
|
|
3241
4041
|
// ── paste (a screenshot is the #1 case) ──
|
|
3242
4042
|
function handleDocumentImagePaste(e) {
|
|
3243
|
-
if (activeView) return;
|
|
4043
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3244
4044
|
if (typeof inlineEdit !== 'undefined' && inlineEdit) return; // hand-edit owns its paste
|
|
3245
4045
|
const files = Array.from((e.clipboardData && e.clipboardData.files) || []).filter(f => /^image\//.test(f.type || ''));
|
|
3246
4046
|
if (files.length === 0) return;
|
|
@@ -3256,6 +4056,7 @@ document.addEventListener('paste', handleDocumentImagePaste);
|
|
|
3256
4056
|
|
|
3257
4057
|
// ── /image picker ──
|
|
3258
4058
|
function openImagePicker() {
|
|
4059
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3259
4060
|
const input = document.createElement('input');
|
|
3260
4061
|
input.type = 'file';
|
|
3261
4062
|
input.accept = 'image/*';
|
|
@@ -3346,7 +4147,7 @@ function ensureImgChip() {
|
|
|
3346
4147
|
return chip;
|
|
3347
4148
|
}
|
|
3348
4149
|
function handleMountImgOver(e) {
|
|
3349
|
-
if (activeView || modifyMutex) return;
|
|
4150
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3350
4151
|
const img = e.target instanceof Element ? e.target.closest('img') : null;
|
|
3351
4152
|
const m = document.getElementById('rwa-doc-mount');
|
|
3352
4153
|
if (!img || !m || !m.contains(img)) return;
|
|
@@ -4206,6 +5007,7 @@ HARD RULES: colors are hex strings only (e.g. "#c0392b"); fonts are ONE of the f
|
|
|
4206
5007
|
const setP = document.getElementById('rwa-set-panel'); if (setP) setP.classList.remove('open');
|
|
4207
5008
|
const infoP = document.getElementById('rwa-info-panel'); if (infoP) infoP.classList.remove('open');
|
|
4208
5009
|
const shareP = document.getElementById('rwa-share-panel'); if (shareP) shareP.classList.remove('open');
|
|
5010
|
+
const modeP = document.getElementById('rwa-mode-panel'); if (modeP) modeP.classList.remove('open');
|
|
4209
5011
|
const active = currentSkinName();
|
|
4210
5012
|
panel.innerHTML =
|
|
4211
5013
|
'<div class="rwa-skin-hd"><span>Skins</span><span>' + (active || '—') + '</span></div>'
|
|
@@ -5588,9 +6390,16 @@ function runtimeSetView(name) {
|
|
|
5588
6390
|
if (!spec || spec.name !== name) throw new Error('no registered view named ' + name);
|
|
5589
6391
|
validateViewOutput(spec.render(currentDocCache, viewCtx()), spec); // throws → never activates
|
|
5590
6392
|
releaseAnchor();
|
|
6393
|
+
if (rwaMode !== 'document') {
|
|
6394
|
+
hideEditTransients();
|
|
6395
|
+
closeRuntimePanels();
|
|
6396
|
+
rwaMode = 'document';
|
|
6397
|
+
emitRuntimeEvent('mode', { mode: rwaMode });
|
|
6398
|
+
}
|
|
5591
6399
|
activeView = spec;
|
|
5592
6400
|
sessionStorage.setItem(rwaViewKey(), name);
|
|
5593
6401
|
}
|
|
6402
|
+
if (typeof syncModeChrome === 'function') syncModeChrome();
|
|
5594
6403
|
if (typeof syncViewChrome === 'function') syncViewChrome();
|
|
5595
6404
|
getDoc().then(d => renderDoc(canonLF(d)));
|
|
5596
6405
|
}
|
|
@@ -7241,7 +8050,11 @@ async function commit() {
|
|
|
7241
8050
|
document.addEventListener('keydown', e => {
|
|
7242
8051
|
const mod = navigator.platform.includes('Mac') ? e.metaKey : e.ctrlKey;
|
|
7243
8052
|
if (!mod) return;
|
|
7244
|
-
if (e.key === 'k') {
|
|
8053
|
+
if (e.key === 'k') {
|
|
8054
|
+
e.preventDefault();
|
|
8055
|
+
try { if (rwaMode !== 'edit') runtimeSetMode('edit'); } catch (_) { return; }
|
|
8056
|
+
document.getElementById('rwa-pal').classList.contains('open') ? closePal() : openPal();
|
|
8057
|
+
}
|
|
7245
8058
|
else if (e.key === 'z') { e.preventDefault(); undo(); }
|
|
7246
8059
|
else if (e.key === 's') { e.preventDefault(); commit(); }
|
|
7247
8060
|
});
|
|
@@ -7326,11 +8139,16 @@ document.addEventListener('keydown', e => {
|
|
|
7326
8139
|
del: runtimeFsDel,
|
|
7327
8140
|
list: runtimeFsList,
|
|
7328
8141
|
},
|
|
8142
|
+
bus: {
|
|
8143
|
+
publish: runtimeBusPublish,
|
|
8144
|
+
subscribe: runtimeBusSubscribe,
|
|
8145
|
+
},
|
|
7329
8146
|
modify: runtimeModify,
|
|
7330
8147
|
commit: runtimeCommit,
|
|
7331
8148
|
undo: runtimeUndo,
|
|
7332
8149
|
applyEnvelope: runtimeApplyEnvelope,
|
|
7333
8150
|
on: runtimeOn,
|
|
8151
|
+
setMode: runtimeSetMode,
|
|
7334
8152
|
provide: runtimeProvide, // spec §5.10 — register a view provider
|
|
7335
8153
|
setView: runtimeSetView, // spec §5.10 — activate/deactivate a render mode
|
|
7336
8154
|
describe: runtimeDescribe, // rwa-identity/1 — what this container is + can do
|
|
@@ -7351,6 +8169,12 @@ document.addEventListener('keydown', e => {
|
|
|
7351
8169
|
enumerable: true,
|
|
7352
8170
|
configurable: false,
|
|
7353
8171
|
});
|
|
8172
|
+
Object.defineProperty(window.runtime, 'mode', {
|
|
8173
|
+
get: () => rwaMode,
|
|
8174
|
+
enumerable: true,
|
|
8175
|
+
configurable: false,
|
|
8176
|
+
});
|
|
8177
|
+
startWorkspacePresence();
|
|
7354
8178
|
// §5.10: the presentation render mode ships ONLY for presentation
|
|
7355
8179
|
// containers. For every other kind this block is skipped entirely —
|
|
7356
8180
|
// activeView stays null, no provider is registered, no chrome is built,
|