rewritable 0.6.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 +24 -4
- package/bin/rwa.mjs +56 -11
- package/package.json +1 -1
- package/seeds/rewritable.html +885 -48
- package/src/agent-loop.mjs +5 -3
- package/src/backend.mjs +24 -6
- package/src/commands.mjs +1 -1
- package/src/create.mjs +2 -1
- 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
|
|
@@ -380,6 +429,13 @@ const RWA = {
|
|
|
380
429
|
// not block these requests even when the container is served over HTTPS.
|
|
381
430
|
DEFAULT_OLLAMA_URL:'http://localhost:11434/v1',
|
|
382
431
|
DEFAULT_LMSTUDIO_URL:'http://localhost:1234/v1',
|
|
432
|
+
// atomic.chat: a local OpenAI-compatible inference server (MLX-backed on
|
|
433
|
+
// Apple Silicon) with the same /v1/chat/completions + /v1/models shape and
|
|
434
|
+
// real multi-turn tool_calls. CORS note: it allows http(s) page origins out
|
|
435
|
+
// of the box but NOT the file:// null origin — serve the container from a
|
|
436
|
+
// local origin (or a hosted projection) to use it.
|
|
437
|
+
DEFAULT_ATOMIC_URL:'http://127.0.0.1:1337/v1',
|
|
438
|
+
K_BASE_URL_ATOMIC:'rwa_base_url_atomic',
|
|
383
439
|
// Optional alternative agent backend: a localhost CLI bridge
|
|
384
440
|
// (https://github.com/martintreiber/web_cli_bridge style) that runs
|
|
385
441
|
// arbitrary shell commands. When the user picks "bridge" in settings, ⌘K
|
|
@@ -411,7 +467,7 @@ try {
|
|
|
411
467
|
qs.delete('key');
|
|
412
468
|
}
|
|
413
469
|
const b = qs.get('backend');
|
|
414
|
-
if (b && ['openrouter','ollama','lmstudio','bridge','bridge-session'].includes(b)) {
|
|
470
|
+
if (b && ['openrouter','ollama','lmstudio','atomic','bridge','bridge-session'].includes(b)) {
|
|
415
471
|
sessionStorage.setItem(RWA.K_BACKEND, b);
|
|
416
472
|
qs.delete('backend');
|
|
417
473
|
}
|
|
@@ -844,12 +900,13 @@ async function runtimeFsList(prefix) {
|
|
|
844
900
|
const runtimeEvents = {
|
|
845
901
|
commit: new Set(),
|
|
846
902
|
modify: new Set(),
|
|
903
|
+
mode: new Set(),
|
|
847
904
|
status: new Set(),
|
|
848
905
|
};
|
|
849
906
|
|
|
850
907
|
function runtimeOn(event, callback) {
|
|
851
908
|
if (!Object.prototype.hasOwnProperty.call(runtimeEvents, event)) {
|
|
852
|
-
throw new Error("unknown event '" + event + "' (use 'commit', 'modify', or 'status')");
|
|
909
|
+
throw new Error("unknown event '" + event + "' (use 'commit', 'modify', 'mode', or 'status')");
|
|
853
910
|
}
|
|
854
911
|
if (typeof callback !== 'function') throw new TypeError('callback must be a function');
|
|
855
912
|
runtimeEvents[event].add(callback);
|
|
@@ -869,6 +926,147 @@ function emitRuntimeEvent(event, payload) {
|
|
|
869
926
|
}
|
|
870
927
|
}
|
|
871
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
|
+
|
|
872
1070
|
// === Status surface (spec §7) ==============================================
|
|
873
1071
|
// FSA permission state. Tracked here rather than re-derived each call because
|
|
874
1072
|
// the permission events that move it (showSaveFilePicker, queryPermission,
|
|
@@ -1012,6 +1210,7 @@ function renderDoc(html) {
|
|
|
1012
1210
|
// the agent's view of the document is invisible-by-construction to any render mode.
|
|
1013
1211
|
setSourceMap(html);
|
|
1014
1212
|
rebuildLockedRanges(html);
|
|
1213
|
+
const editReady = rwaMode === 'edit' && !activeView;
|
|
1015
1214
|
// rwa-lens/1: click-to-anchor (Task 5.1). renderDoc runs on every commit and
|
|
1016
1215
|
// on bootstrap; remove first so listeners don't multiply across renders. Same
|
|
1017
1216
|
// function reference, so removing the previous instance is safe. With a
|
|
@@ -1019,12 +1218,22 @@ function renderDoc(html) {
|
|
|
1019
1218
|
// listener is removed and not re-added (§5.10). For activeView===null this is
|
|
1020
1219
|
// byte-identical to the previous behavior.
|
|
1021
1220
|
m.removeEventListener('click', handleMountClick);
|
|
1022
|
-
if (
|
|
1023
|
-
// 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
|
|
1024
1226
|
// remove-then-add discipline as click-to-anchor; not registered under an
|
|
1025
1227
|
// active view (byte-offset resolution is undefined there).
|
|
1228
|
+
if (typeof refreshInlineEditAffordances === 'function') refreshInlineEditAffordances(m);
|
|
1229
|
+
m.removeEventListener('pointerdown', handleMountPointerDown);
|
|
1026
1230
|
m.removeEventListener('dblclick', handleMountDblClick);
|
|
1027
|
-
|
|
1231
|
+
m.removeEventListener('keydown', handleMountEditKeydown);
|
|
1232
|
+
if (editReady) {
|
|
1233
|
+
m.addEventListener('pointerdown', handleMountPointerDown);
|
|
1234
|
+
m.addEventListener('dblclick', handleMountDblClick);
|
|
1235
|
+
m.addEventListener('keydown', handleMountEditKeydown);
|
|
1236
|
+
}
|
|
1028
1237
|
// images-v1: drag-drop insert + hover-✕ chip. Same remove-then-add
|
|
1029
1238
|
// discipline; inert under a render mode (byte-offset resolution undefined).
|
|
1030
1239
|
// The chip/drop-mark reference DOM the innerHTML reset just destroyed.
|
|
@@ -1036,7 +1245,7 @@ function renderDoc(html) {
|
|
|
1036
1245
|
m.removeEventListener('drop', handleMountDrop);
|
|
1037
1246
|
m.removeEventListener('mouseover', handleMountImgOver);
|
|
1038
1247
|
m.removeEventListener('mouseout', handleMountImgOut);
|
|
1039
|
-
if (
|
|
1248
|
+
if (editReady) {
|
|
1040
1249
|
m.addEventListener('dragover', handleMountDragOver);
|
|
1041
1250
|
m.addEventListener('dragleave', handleMountDragLeave);
|
|
1042
1251
|
m.addEventListener('drop', handleMountDrop);
|
|
@@ -1049,6 +1258,136 @@ function renderDoc(html) {
|
|
|
1049
1258
|
if (activeView && typeof activeView.mounted === 'function') {
|
|
1050
1259
|
activeView.mounted(m, viewCtx());
|
|
1051
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
|
+
}
|
|
1052
1391
|
}
|
|
1053
1392
|
|
|
1054
1393
|
// ─── URL fragment scroll (rwa-bootstrap 0.9) ───────────────────────
|
|
@@ -1109,6 +1448,12 @@ function buildFile(doc) {
|
|
|
1109
1448
|
function buildUI() {
|
|
1110
1449
|
document.getElementById('rwa-runtime').innerHTML = `
|
|
1111
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>
|
|
1112
1457
|
<button class="rwa-st-btn" id="rwa-st-status">● ready</button>
|
|
1113
1458
|
<button class="rwa-st-btn" id="rwa-st-info" title="what is this?" aria-label="what is this?">ⓘ</button>
|
|
1114
1459
|
<button class="rwa-st-btn" id="rwa-st-cog">⚙</button>
|
|
@@ -1117,7 +1462,7 @@ function buildUI() {
|
|
|
1117
1462
|
<button class="rwa-st-btn pri" id="rwa-st-commit">⌘S</button>
|
|
1118
1463
|
</div>
|
|
1119
1464
|
<div id="rwa-set-panel">
|
|
1120
|
-
<div class="rwa-set-row"><label>Backend</label><select id="rwa-backend"><option value="openrouter">OpenRouter (API key)</option><option value="ollama">Ollama (localhost)</option><option value="lmstudio">LM Studio (localhost)</option><option value="bridge">Bridge (claude -p, localhost)</option><option value="bridge-session">Bridge session (claude, persistent)</option></select></div>
|
|
1465
|
+
<div class="rwa-set-row"><label>Backend</label><select id="rwa-backend"><option value="openrouter">OpenRouter (API key)</option><option value="ollama">Ollama (localhost)</option><option value="lmstudio">LM Studio (localhost)</option><option value="atomic">atomic.chat (localhost)</option><option value="bridge">Bridge (claude -p, localhost)</option><option value="bridge-session">Bridge session (claude, persistent)</option></select></div>
|
|
1121
1466
|
<div class="rwa-set-row" id="rwa-set-row-key"><label>OpenRouter Key</label><input type="password" id="rwa-key" placeholder="sk-or-..." autocomplete="off"></div>
|
|
1122
1467
|
<div class="rwa-set-row" id="rwa-set-row-bridge-token" style="display:none;"><label>Bridge Token</label><input type="password" id="rwa-bridge-token" placeholder="WEB_CLI_BRIDGE_TOKEN" autocomplete="off"></div>
|
|
1123
1468
|
<div class="rwa-set-row" id="rwa-set-row-bridge-cwd" style="display:none;"><label>Session Dir</label><input type="text" id="rwa-bridge-cwd" placeholder="/path/on/bridge/host" autocomplete="off" spellcheck="false"></div>
|
|
@@ -1128,6 +1473,7 @@ function buildUI() {
|
|
|
1128
1473
|
<div id="rwa-info-panel"></div>
|
|
1129
1474
|
<div id="rwa-skin-panel"></div>
|
|
1130
1475
|
<div id="rwa-share-panel"></div>
|
|
1476
|
+
<div id="rwa-mode-panel"></div>
|
|
1131
1477
|
<div id="rwa-pal">
|
|
1132
1478
|
<div id="rwa-pal-box">
|
|
1133
1479
|
<div class="rwa-pal-top">
|
|
@@ -1149,6 +1495,12 @@ function buildUI() {
|
|
|
1149
1495
|
<div id="rwa-lens-paste-hint" hidden></div>
|
|
1150
1496
|
<div id="rwa-lens-hint"></div>
|
|
1151
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>
|
|
1152
1504
|
<div id="rwa-lens-hist-panel" hidden></div>`;
|
|
1153
1505
|
|
|
1154
1506
|
const k = document.getElementById('rwa-key'), m = document.getElementById('rwa-model');
|
|
@@ -1156,6 +1508,7 @@ function buildUI() {
|
|
|
1156
1508
|
m.value = sessionStorage.getItem(RWA.K_MODEL) || RWA.MODEL;
|
|
1157
1509
|
k.oninput = e => sessionStorage.setItem(RWA.K_API, e.target.value.trim());
|
|
1158
1510
|
m.oninput = e => sessionStorage.setItem(RWA.K_MODEL, e.target.value.trim() || RWA.MODEL);
|
|
1511
|
+
attachModeTabs();
|
|
1159
1512
|
|
|
1160
1513
|
// Bridge SESSION backend config: the bearer token the bridge requires on every
|
|
1161
1514
|
// /session/* call, and a server-side working dir for the claude session.
|
|
@@ -1198,6 +1551,12 @@ function buildUI() {
|
|
|
1198
1551
|
defaultUrl: RWA.DEFAULT_LMSTUDIO_URL,
|
|
1199
1552
|
storageKey: RWA.K_BASE_URL_LMSTUDIO,
|
|
1200
1553
|
},
|
|
1554
|
+
atomic: {
|
|
1555
|
+
showKey: false, showBaseUrl: true, showModel: true, showHint: true,
|
|
1556
|
+
hintHTML: 'atomic.chat serves an OpenAI-compatible API on <code>127.0.0.1:1337</code>, no key needed — use <code>Test</code> to list its models. CORS allows http(s) page origins out of the box but <strong>not <code>file://</code> pages</strong> (the null origin): open this container from a local web server or a hosted projection to use it.',
|
|
1557
|
+
defaultUrl: RWA.DEFAULT_ATOMIC_URL,
|
|
1558
|
+
storageKey: RWA.K_BASE_URL_ATOMIC,
|
|
1559
|
+
},
|
|
1201
1560
|
bridge: {
|
|
1202
1561
|
showKey: false, showBaseUrl: false, showModel: false, showHint: false,
|
|
1203
1562
|
hintHTML: '',
|
|
@@ -1288,11 +1647,14 @@ function buildUI() {
|
|
|
1288
1647
|
opt.value = id;
|
|
1289
1648
|
modelOptsEl.appendChild(opt);
|
|
1290
1649
|
}
|
|
1291
|
-
//
|
|
1292
|
-
|
|
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();
|
|
1293
1654
|
const isLocal = backendEl.value === 'ollama' || backendEl.value === 'lmstudio';
|
|
1294
|
-
const
|
|
1295
|
-
|
|
1655
|
+
const shouldSuggestLocal = isLocal && models.length > 0
|
|
1656
|
+
&& (currentModel === '' || currentModel === RWA.MODEL || !models.includes(currentModel));
|
|
1657
|
+
if (shouldSuggestLocal) {
|
|
1296
1658
|
m.value = models[0];
|
|
1297
1659
|
sessionStorage.setItem(RWA.K_MODEL, models[0]);
|
|
1298
1660
|
}
|
|
@@ -1310,6 +1672,7 @@ function buildUI() {
|
|
|
1310
1672
|
document.getElementById('rwa-info-panel').classList.remove('open'); // panels are mutually exclusive
|
|
1311
1673
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1312
1674
|
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1675
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1313
1676
|
document.getElementById('rwa-set-panel').classList.toggle('open');
|
|
1314
1677
|
};
|
|
1315
1678
|
// ⓘ "what is this?" — render the rwa-identity/1 bundle as prose on open so it
|
|
@@ -1319,6 +1682,7 @@ function buildUI() {
|
|
|
1319
1682
|
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1320
1683
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1321
1684
|
document.getElementById('rwa-share-panel').classList.remove('open');
|
|
1685
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1322
1686
|
const panel = document.getElementById('rwa-info-panel');
|
|
1323
1687
|
if (!panel.classList.contains('open')) panel.innerHTML = renderInfoPanel();
|
|
1324
1688
|
panel.classList.toggle('open');
|
|
@@ -1338,6 +1702,7 @@ function buildUI() {
|
|
|
1338
1702
|
document.getElementById('rwa-set-panel').classList.remove('open');
|
|
1339
1703
|
document.getElementById('rwa-info-panel').classList.remove('open');
|
|
1340
1704
|
document.getElementById('rwa-skin-panel').classList.remove('open');
|
|
1705
|
+
document.getElementById('rwa-mode-panel').classList.remove('open');
|
|
1341
1706
|
renderSharePanel().then(() => panel.classList.add('open'));
|
|
1342
1707
|
};
|
|
1343
1708
|
document.getElementById('rwa-st-commit').onclick = commit;
|
|
@@ -1447,6 +1812,24 @@ function buildUI() {
|
|
|
1447
1812
|
});
|
|
1448
1813
|
}
|
|
1449
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
|
+
|
|
1450
1833
|
// rwa-lens/1: Esc releases the anchor when one is held. Listener is on
|
|
1451
1834
|
// `window` so Esc anywhere works (including with focus outside the lens
|
|
1452
1835
|
// input — e.g. on the doc itself after a click).
|
|
@@ -1638,6 +2021,139 @@ async function renderHistoryPanel(panel) {
|
|
|
1638
2021
|
if (closeBtn) closeBtn.addEventListener('click', () => { panel.hidden = true; });
|
|
1639
2022
|
}
|
|
1640
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
|
+
|
|
1641
2157
|
// ─── Agent (rwa-edit/1) ─────────────────────────────────────────────
|
|
1642
2158
|
// Spec: rwa-edit-spec.md (v1.4). The agent edits the doc via tool calls,
|
|
1643
2159
|
// not by emitting a full rewritten document. unchanged regions are byte-
|
|
@@ -1821,6 +2337,15 @@ The stored document is ordinary prose HTML inside a single <article>: <h1>/<h2>
|
|
|
1821
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.
|
|
1822
2338
|
|
|
1823
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.
|
|
1824
2349
|
${SYSTEM_PROMPT_RULES}`,
|
|
1825
2350
|
};
|
|
1826
2351
|
// rwa:extract:end SYSTEM_PROMPTS
|
|
@@ -2576,6 +3101,10 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2576
3101
|
// live mount produces the same order via the same outer-wins traversal — so
|
|
2577
3102
|
// the i-th live anchorable corresponds to map[i].
|
|
2578
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;
|
|
2579
3108
|
// Audit R3 (scoped): respect the per-kind click-to-anchor flag. When
|
|
2580
3109
|
// false (e.g. workflow files), all clicks pass through without anchoring
|
|
2581
3110
|
// — the lens stays in default state and every command runs against the
|
|
@@ -2736,10 +3265,11 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2736
3265
|
}
|
|
2737
3266
|
|
|
2738
3267
|
// ─── Inline manual edit (edit-surface: direct, no-LLM block editing) ──────
|
|
2739
|
-
//
|
|
3268
|
+
// Click a leaf text block and type in place; Enter or blur
|
|
2740
3269
|
// commits through the existing non-agent commit path (runtimeApplyEnvelope,
|
|
2741
|
-
// actor 'user:edit-surface') — no model call, works offline.
|
|
2742
|
-
// 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:
|
|
2743
3273
|
// docs/plans/2026-06-08-inline-manual-edit-design.md.
|
|
2744
3274
|
//
|
|
2745
3275
|
// Editable set = the leaf-text members of ANCHORABLE_TAGS, so the
|
|
@@ -2747,6 +3277,7 @@ if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
|
2747
3277
|
// containers; FIGCAPTION is not independently anchorable (lives in FIGURE).
|
|
2748
3278
|
const INLINE_EDITABLE = new Set(['P','H1','H2','H3','H4','H5','H6','BLOCKQUOTE','LI','TD']);
|
|
2749
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]';
|
|
2750
3281
|
|
|
2751
3282
|
// Controlled serializer: turn an edited contenteditable node into a clean
|
|
2752
3283
|
// replace string. Emits ONLY escaped text and <br> (the Shift+Enter soft
|
|
@@ -2766,6 +3297,270 @@ function serializeLeafSafe(el) {
|
|
|
2766
3297
|
}
|
|
2767
3298
|
window.serializeLeafSafe = serializeLeafSafe;
|
|
2768
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
|
+
|
|
2769
3564
|
function enterInlineEdit(el, entry) {
|
|
2770
3565
|
if (inlineEdit) exitInlineEdit();
|
|
2771
3566
|
inlineEdit = { el, entry, original: '', commandMode: false, demoted: false };
|
|
@@ -2983,34 +3778,39 @@ async function commitInlineEdit() {
|
|
|
2983
3778
|
}
|
|
2984
3779
|
window.commitInlineEdit = commitInlineEdit;
|
|
2985
3780
|
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
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.
|
|
2991
3799
|
function handleMountDblClick(e) {
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
if (
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
if (el.closest('[data-rwa-frozen]') || el.closest('.rwa-locked')) return;
|
|
3003
|
-
const ord = anchorableOrdinal(mount, el);
|
|
3004
|
-
const entry = (ord >= 0 && sourceMap) ? sourceMap[ord] : null;
|
|
3005
|
-
// A nested anchorable (an inner <li>, or a <p> inside a <td>) is not recorded
|
|
3006
|
-
// by the outer-wins descent, so ord is -1 and we no-op — consistent with the
|
|
3007
|
-
// lens's anchoring model, which can't target sub-blocks either.
|
|
3008
|
-
if (!entry) return;
|
|
3009
|
-
if (isWithinLockedRange(entry.start, entry.end)) return; // marker-form frozen backstop
|
|
3010
|
-
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);
|
|
3011
3810
|
}
|
|
3012
3811
|
if (typeof navigator !== 'undefined' && /jsdom/i.test(navigator.userAgent)) {
|
|
3013
3812
|
window.__handleMountDblClick = handleMountDblClick;
|
|
3813
|
+
window.__handleMountPointerDown = handleMountPointerDown;
|
|
3014
3814
|
}
|
|
3015
3815
|
|
|
3016
3816
|
// ─── Image ingestion (images-v1) ──────────────────────────────────────
|
|
@@ -3198,7 +3998,7 @@ function dragHasFiles(e) {
|
|
|
3198
3998
|
return Array.from(t.types || []).includes('Files');
|
|
3199
3999
|
}
|
|
3200
4000
|
function handleMountDragOver(e) {
|
|
3201
|
-
if (modifyMutex || !dragHasFiles(e)) return;
|
|
4001
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex || !dragHasFiles(e)) return;
|
|
3202
4002
|
e.preventDefault();
|
|
3203
4003
|
if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';
|
|
3204
4004
|
clearDropMark();
|
|
@@ -3213,6 +4013,7 @@ function handleMountDragLeave(e) {
|
|
|
3213
4013
|
if (!m || !e.relatedTarget || !m.contains(e.relatedTarget)) clearDropMark();
|
|
3214
4014
|
}
|
|
3215
4015
|
function handleMountDrop(e) {
|
|
4016
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3216
4017
|
if (!dragHasFiles(e)) return;
|
|
3217
4018
|
e.preventDefault();
|
|
3218
4019
|
const target = findImageDropTarget(e);
|
|
@@ -3227,7 +4028,7 @@ function handleMountDrop(e) {
|
|
|
3227
4028
|
|
|
3228
4029
|
// ── paste (a screenshot is the #1 case) ──
|
|
3229
4030
|
function handleDocumentImagePaste(e) {
|
|
3230
|
-
if (activeView) return;
|
|
4031
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3231
4032
|
if (typeof inlineEdit !== 'undefined' && inlineEdit) return; // hand-edit owns its paste
|
|
3232
4033
|
const files = Array.from((e.clipboardData && e.clipboardData.files) || []).filter(f => /^image\//.test(f.type || ''));
|
|
3233
4034
|
if (files.length === 0) return;
|
|
@@ -3243,6 +4044,7 @@ document.addEventListener('paste', handleDocumentImagePaste);
|
|
|
3243
4044
|
|
|
3244
4045
|
// ── /image picker ──
|
|
3245
4046
|
function openImagePicker() {
|
|
4047
|
+
if (rwaMode !== 'edit' || activeView) return;
|
|
3246
4048
|
const input = document.createElement('input');
|
|
3247
4049
|
input.type = 'file';
|
|
3248
4050
|
input.accept = 'image/*';
|
|
@@ -3333,7 +4135,7 @@ function ensureImgChip() {
|
|
|
3333
4135
|
return chip;
|
|
3334
4136
|
}
|
|
3335
4137
|
function handleMountImgOver(e) {
|
|
3336
|
-
if (activeView || modifyMutex) return;
|
|
4138
|
+
if (rwaMode !== 'edit' || activeView || modifyMutex) return;
|
|
3337
4139
|
const img = e.target instanceof Element ? e.target.closest('img') : null;
|
|
3338
4140
|
const m = document.getElementById('rwa-doc-mount');
|
|
3339
4141
|
if (!img || !m || !m.contains(img)) return;
|
|
@@ -4193,6 +4995,7 @@ HARD RULES: colors are hex strings only (e.g. "#c0392b"); fonts are ONE of the f
|
|
|
4193
4995
|
const setP = document.getElementById('rwa-set-panel'); if (setP) setP.classList.remove('open');
|
|
4194
4996
|
const infoP = document.getElementById('rwa-info-panel'); if (infoP) infoP.classList.remove('open');
|
|
4195
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');
|
|
4196
4999
|
const active = currentSkinName();
|
|
4197
5000
|
panel.innerHTML =
|
|
4198
5001
|
'<div class="rwa-skin-hd"><span>Skins</span><span>' + (active || '—') + '</span></div>'
|
|
@@ -4321,6 +5124,18 @@ function resolveBackendConfig() {
|
|
|
4321
5124
|
apiKey: null, extraHeaders: {}, requiresKey: false,
|
|
4322
5125
|
};
|
|
4323
5126
|
}
|
|
5127
|
+
if (backend === 'atomic') {
|
|
5128
|
+
return {
|
|
5129
|
+
backend, kind:'openai_compat',
|
|
5130
|
+
baseUrl: (sessionStorage.getItem(RWA.K_BASE_URL_ATOMIC) || '').trim() || RWA.DEFAULT_ATOMIC_URL,
|
|
5131
|
+
apiKey: null, extraHeaders: {}, requiresKey: false,
|
|
5132
|
+
// atomic.chat REJECTS (400) requests whose prompt + max generation
|
|
5133
|
+
// exceed its MAX_KV_SIZE (16384 default) rather than clamping like
|
|
5134
|
+
// ollama/lmstudio — half the window for generation, half for the
|
|
5135
|
+
// prompt + document. Callers read cfg.maxTokens || 32000.
|
|
5136
|
+
maxTokens: 8192,
|
|
5137
|
+
};
|
|
5138
|
+
}
|
|
4324
5139
|
if (backend === 'bridge') {
|
|
4325
5140
|
return { backend, kind:'bridge' };
|
|
4326
5141
|
}
|
|
@@ -4379,7 +5194,7 @@ async function callAgentSingleShot(prompt) {
|
|
|
4379
5194
|
const model = sessionStorage.getItem(RWA.K_MODEL) || RWA.MODEL;
|
|
4380
5195
|
const data = await openAiCompatChat(cfg, {
|
|
4381
5196
|
model,
|
|
4382
|
-
max_tokens: 32000,
|
|
5197
|
+
max_tokens: cfg.maxTokens || 32000,
|
|
4383
5198
|
messages: [{ role:'user', content: prompt }],
|
|
4384
5199
|
});
|
|
4385
5200
|
const msg = data.choices?.[0]?.message;
|
|
@@ -5563,9 +6378,16 @@ function runtimeSetView(name) {
|
|
|
5563
6378
|
if (!spec || spec.name !== name) throw new Error('no registered view named ' + name);
|
|
5564
6379
|
validateViewOutput(spec.render(currentDocCache, viewCtx()), spec); // throws → never activates
|
|
5565
6380
|
releaseAnchor();
|
|
6381
|
+
if (rwaMode !== 'document') {
|
|
6382
|
+
hideEditTransients();
|
|
6383
|
+
closeRuntimePanels();
|
|
6384
|
+
rwaMode = 'document';
|
|
6385
|
+
emitRuntimeEvent('mode', { mode: rwaMode });
|
|
6386
|
+
}
|
|
5566
6387
|
activeView = spec;
|
|
5567
6388
|
sessionStorage.setItem(rwaViewKey(), name);
|
|
5568
6389
|
}
|
|
6390
|
+
if (typeof syncModeChrome === 'function') syncModeChrome();
|
|
5569
6391
|
if (typeof syncViewChrome === 'function') syncViewChrome();
|
|
5570
6392
|
getDoc().then(d => renderDoc(canonLF(d)));
|
|
5571
6393
|
}
|
|
@@ -6578,7 +7400,7 @@ async function modify(instr, lensMeta = null, opts = null) {
|
|
|
6578
7400
|
try {
|
|
6579
7401
|
data = await openAiCompatChat(cfg, {
|
|
6580
7402
|
model,
|
|
6581
|
-
max_tokens: 32000,
|
|
7403
|
+
max_tokens: cfg.maxTokens || 32000,
|
|
6582
7404
|
messages,
|
|
6583
7405
|
tools: TOOL_SCHEMAS,
|
|
6584
7406
|
tool_choice: 'auto',
|
|
@@ -7216,7 +8038,11 @@ async function commit() {
|
|
|
7216
8038
|
document.addEventListener('keydown', e => {
|
|
7217
8039
|
const mod = navigator.platform.includes('Mac') ? e.metaKey : e.ctrlKey;
|
|
7218
8040
|
if (!mod) return;
|
|
7219
|
-
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
|
+
}
|
|
7220
8046
|
else if (e.key === 'z') { e.preventDefault(); undo(); }
|
|
7221
8047
|
else if (e.key === 's') { e.preventDefault(); commit(); }
|
|
7222
8048
|
});
|
|
@@ -7301,11 +8127,16 @@ document.addEventListener('keydown', e => {
|
|
|
7301
8127
|
del: runtimeFsDel,
|
|
7302
8128
|
list: runtimeFsList,
|
|
7303
8129
|
},
|
|
8130
|
+
bus: {
|
|
8131
|
+
publish: runtimeBusPublish,
|
|
8132
|
+
subscribe: runtimeBusSubscribe,
|
|
8133
|
+
},
|
|
7304
8134
|
modify: runtimeModify,
|
|
7305
8135
|
commit: runtimeCommit,
|
|
7306
8136
|
undo: runtimeUndo,
|
|
7307
8137
|
applyEnvelope: runtimeApplyEnvelope,
|
|
7308
8138
|
on: runtimeOn,
|
|
8139
|
+
setMode: runtimeSetMode,
|
|
7309
8140
|
provide: runtimeProvide, // spec §5.10 — register a view provider
|
|
7310
8141
|
setView: runtimeSetView, // spec §5.10 — activate/deactivate a render mode
|
|
7311
8142
|
describe: runtimeDescribe, // rwa-identity/1 — what this container is + can do
|
|
@@ -7326,6 +8157,12 @@ document.addEventListener('keydown', e => {
|
|
|
7326
8157
|
enumerable: true,
|
|
7327
8158
|
configurable: false,
|
|
7328
8159
|
});
|
|
8160
|
+
Object.defineProperty(window.runtime, 'mode', {
|
|
8161
|
+
get: () => rwaMode,
|
|
8162
|
+
enumerable: true,
|
|
8163
|
+
configurable: false,
|
|
8164
|
+
});
|
|
8165
|
+
startWorkspacePresence();
|
|
7329
8166
|
// §5.10: the presentation render mode ships ONLY for presentation
|
|
7330
8167
|
// containers. For every other kind this block is skipped entirely —
|
|
7331
8168
|
// activeView stays null, no provider is registered, no chrome is built,
|