@worca/app 1.2.0-rc.3 → 1.2.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/package.json +1 -1
- package/ui/public/app.js +22 -1
- package/ui/public/ask-panel.mjs +238 -6
- package/ui/public/index.html +24 -4
- package/ui/public/style.css +52 -3
- package/ui/server.mjs +23 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@worca/app",
|
|
3
|
-
"version": "1.2.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Worca — deterministic multi-agent pipeline that drives Claude Code (headless) through Plan -> Refine -> Implement -> Review, with a CLI, an installable /worca skill, and a web UI.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Sinisha Djukic",
|
package/ui/public/app.js
CHANGED
|
@@ -296,6 +296,10 @@ const el = {
|
|
|
296
296
|
chatSettingsMsg: $('#chatSettingsMsg'),
|
|
297
297
|
settingsTabs: $('#settings-tabs'),
|
|
298
298
|
|
|
299
|
+
// About (Settings card): read-only app identity, painted from /api/settings
|
|
300
|
+
aboutVersion: $('#aboutVersion'),
|
|
301
|
+
aboutRepoLink: $('#aboutRepoLink'),
|
|
302
|
+
|
|
299
303
|
// Guardrails view
|
|
300
304
|
guardrailsList: $('#guardrails-list'),
|
|
301
305
|
guardrailsMsg: $('#guardrails-msg'),
|
|
@@ -7754,7 +7758,7 @@ function setFormMsg(text, kind) {
|
|
|
7754
7758
|
}
|
|
7755
7759
|
|
|
7756
7760
|
// ---------------------------------------------------------------------------
|
|
7757
|
-
// Settings view: the machine-wide Worca
|
|
7761
|
+
// Settings view: the machine-wide Worca root folder + the projects root
|
|
7758
7762
|
// (§5.1) whose CLAUDE.md / .claude/skills / .mcp.json every pipeline agent sees.
|
|
7759
7763
|
// ---------------------------------------------------------------------------
|
|
7760
7764
|
function setSettingsMsg(text, kind) {
|
|
@@ -7782,6 +7786,22 @@ function paintSettings(data) {
|
|
|
7782
7786
|
// fallback so an older/partial response still fills the placeholder.
|
|
7783
7787
|
const projectsRootFallback = (data) => data.projectsRootDefault || data.default || '';
|
|
7784
7788
|
|
|
7789
|
+
// About card: painted from the `app` block of GET /api/settings. A missing or
|
|
7790
|
+
// malformed payload leaves the static markup (including target/rel) untouched.
|
|
7791
|
+
function paintAbout(info) {
|
|
7792
|
+
if (!info) return;
|
|
7793
|
+
if (el.aboutVersion && info.version) {
|
|
7794
|
+
el.aboutVersion.textContent = info.version;
|
|
7795
|
+
// Links the version to its release tag; a missing/malformed URL leaves the
|
|
7796
|
+
// anchor href-less, i.e. plain text.
|
|
7797
|
+
if (typeof info.releaseUrl === 'string' && info.releaseUrl) el.aboutVersion.href = info.releaseUrl;
|
|
7798
|
+
}
|
|
7799
|
+
if (el.aboutRepoLink && typeof info.repoUrl === 'string' && info.repoUrl) {
|
|
7800
|
+
el.aboutRepoLink.href = info.repoUrl;
|
|
7801
|
+
el.aboutRepoLink.textContent = info.repoUrl.replace(/^https?:\/\//, '');
|
|
7802
|
+
}
|
|
7803
|
+
}
|
|
7804
|
+
|
|
7785
7805
|
async function loadSettings() {
|
|
7786
7806
|
if (!el.settingsRoot) return;
|
|
7787
7807
|
try {
|
|
@@ -7789,6 +7809,7 @@ async function loadSettings() {
|
|
|
7789
7809
|
const data = await safeJson(res);
|
|
7790
7810
|
if (!res.ok) { setSettingsMsg(data.error || `HTTP ${res.status}`, 'err'); return; }
|
|
7791
7811
|
paintSettings(data);
|
|
7812
|
+
paintAbout(data.app);
|
|
7792
7813
|
paintBudgetSettings(data);
|
|
7793
7814
|
paintAskSettings(data);
|
|
7794
7815
|
paintDebugSpawnSettings(data);
|
package/ui/public/ask-panel.mjs
CHANGED
|
@@ -85,6 +85,22 @@ export function shortcutLabel(win) {
|
|
|
85
85
|
return /mac|iphone|ipad|ipod/i.test(platform) ? '⌘K' : 'Ctrl K';
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Sheet geometry shared with style.css: .ask-dock{padding:0 28px 26px} and
|
|
90
|
+
* .ask-sheet{width:min(782px,100%);height:min(669px,calc(100% - 20px))}. The
|
|
91
|
+
* user's size is clamped to [minW×minH, dock inner box]. The floor IS the
|
|
92
|
+
* stylesheet default: the sheet grows from what it always was and never shrinks
|
|
93
|
+
* below it, so every layout the fixed-size sheet was designed around (the
|
|
94
|
+
* non-wrapping composer row, the popovers written in 100vh terms) still holds.
|
|
95
|
+
* Border-box px throughout — the sheet has no padding.
|
|
96
|
+
*/
|
|
97
|
+
export const ASK_SHEET_SIZE = Object.freeze({
|
|
98
|
+
defaultW: 782, defaultH: 669,
|
|
99
|
+
minW: 782, minH: 669,
|
|
100
|
+
dockPadX: 28, dockPadBottom: 26, topGap: 20,
|
|
101
|
+
});
|
|
102
|
+
const SIZE_KEY = 'worca-cc.ask.size';
|
|
103
|
+
|
|
88
104
|
export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContext, openNewPipeline, loadMarkdown, hljsLoader, storage, raf, now }) {
|
|
89
105
|
const storedPick = readStoredModel(); // hoisted declaration (defined below); null when nothing is stored
|
|
90
106
|
const st = {
|
|
@@ -109,6 +125,9 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
109
125
|
worktrees: [], // P4 §10: the chat's open worktrees (snapshot-fed)
|
|
110
126
|
pinned: true,
|
|
111
127
|
prevFocus: null,
|
|
128
|
+
size: readStoredSize(), // {w,h} the user's persisted sheet size (hoisted reader); null = stylesheet default
|
|
129
|
+
applied: null, // {w,h} the inline size currently on the sheet (clamped); null = default
|
|
130
|
+
drag: null, // the active resize gesture — see startResize()
|
|
112
131
|
pendingFiles: [],
|
|
113
132
|
sending: false,
|
|
114
133
|
subscribedFor: null,
|
|
@@ -161,6 +180,32 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
161
180
|
else storage.removeItem('worca-cc.ask.thread');
|
|
162
181
|
} catch { /* ignore */ }
|
|
163
182
|
}
|
|
183
|
+
/** The persisted sheet size, or null when nothing usable is stored. Clamped later, against a live dock. */
|
|
184
|
+
function readStoredSize() {
|
|
185
|
+
try {
|
|
186
|
+
const raw = storage.getItem(SIZE_KEY);
|
|
187
|
+
const v = raw ? JSON.parse(raw) : null;
|
|
188
|
+
if (v && Number.isFinite(v.w) && Number.isFinite(v.h)) return { w: Math.round(v.w), h: Math.round(v.h) };
|
|
189
|
+
} catch { /* storage unavailable */ }
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
function storeSize(size) {
|
|
193
|
+
try {
|
|
194
|
+
if (size) storage.setItem(SIZE_KEY, JSON.stringify({ w: size.w, h: size.h }));
|
|
195
|
+
else storage.removeItem(SIZE_KEY);
|
|
196
|
+
} catch { /* ignore */ }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Size the composer textarea to its content, one line minimum, 120px maximum.
|
|
201
|
+
* Bound to the input event; every programmatic write to el.input.value (the
|
|
202
|
+
* post-send clear, appendToComposer) must call it too, since assignment
|
|
203
|
+
* fires no input event and the box would keep the previous draft's height.
|
|
204
|
+
*/
|
|
205
|
+
function fitInput() {
|
|
206
|
+
el.input.style.height = 'auto';
|
|
207
|
+
el.input.style.height = `${Math.min(el.input.scrollHeight || 0, 120)}px`;
|
|
208
|
+
}
|
|
164
209
|
|
|
165
210
|
// ---- tiny DOM helpers -----------------------------------------------------
|
|
166
211
|
function make(tag, className, text) {
|
|
@@ -257,10 +302,12 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
257
302
|
el.jump.hidden = true;
|
|
258
303
|
el.jump.addEventListener('click', jumpToLatest);
|
|
259
304
|
sheet.appendChild(el.jump);
|
|
305
|
+
for (const edge of ['n', 'e', 'w', 'ne', 'nw']) sheet.appendChild(buildResizeHandle(edge));
|
|
260
306
|
dock.appendChild(sheet);
|
|
261
307
|
dock.appendChild(pill);
|
|
262
308
|
el.pill = pill;
|
|
263
309
|
el.sheet = sheet;
|
|
310
|
+
el.dock = dock; // measured by dockInner(); `root` is TDZ here
|
|
264
311
|
return dock;
|
|
265
312
|
}
|
|
266
313
|
|
|
@@ -430,6 +477,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
430
477
|
// No provisional title from the prompt: the header keeps "Ask Worca" until
|
|
431
478
|
// the ask-title frame lands (ask-model marks title dirty, flushExtra repaints).
|
|
432
479
|
el.input.value = '';
|
|
480
|
+
fitInput(); // a programmatic clear fires no input event
|
|
433
481
|
st.pendingFiles = [];
|
|
434
482
|
renderChips();
|
|
435
483
|
subscribe(id);
|
|
@@ -455,10 +503,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
455
503
|
el.input.addEventListener('keydown', (e) => {
|
|
456
504
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage(); }
|
|
457
505
|
});
|
|
458
|
-
el.input.addEventListener('input',
|
|
459
|
-
el.input.style.height = 'auto';
|
|
460
|
-
el.input.style.height = `${Math.min(el.input.scrollHeight || 0, 120)}px`;
|
|
461
|
-
});
|
|
506
|
+
el.input.addEventListener('input', fitInput);
|
|
462
507
|
wrap.appendChild(el.input);
|
|
463
508
|
|
|
464
509
|
el.composerMsg = make('div', 'ask-composer-msg');
|
|
@@ -581,6 +626,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
581
626
|
st.prevFocus = doc.activeElement;
|
|
582
627
|
el.pill.hidden = true;
|
|
583
628
|
el.sheet.hidden = false;
|
|
629
|
+
restoreSize(); // the sheet has a box now — clamp the stored size to the dock
|
|
584
630
|
st.pinned = true;
|
|
585
631
|
ensureFirstOpen();
|
|
586
632
|
focusComposer();
|
|
@@ -598,8 +644,8 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
598
644
|
openSheet(); // no-op when already open…
|
|
599
645
|
const cur = el.input.value;
|
|
600
646
|
el.input.value = cur ? `${cur.replace(/\s*$/, '')}\n${add}` : add;
|
|
601
|
-
// …so the autosize
|
|
602
|
-
|
|
647
|
+
// …so the autosize and the focus have to be driven here.
|
|
648
|
+
fitInput();
|
|
603
649
|
focusComposer();
|
|
604
650
|
try { el.input.selectionStart = el.input.selectionEnd = el.input.value.length; } catch { /* jsdom */ }
|
|
605
651
|
return true;
|
|
@@ -619,6 +665,180 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
619
665
|
|
|
620
666
|
function toggleSheet() { (st.open ? closeSheet : openSheet)(); }
|
|
621
667
|
|
|
668
|
+
// ---- resize ---------------------------------------------------------------
|
|
669
|
+
// The sheet is a bottom-anchored, horizontally centred flex child of the dock,
|
|
670
|
+
// so a top-edge drag is a pure height change and a side drag grows the width
|
|
671
|
+
// symmetrically about the centre (each edge moves half the delta, which keeps
|
|
672
|
+
// the grabbed edge under the cursor). Sizes are border-box px applied as
|
|
673
|
+
// inline width/height, so the stylesheet's default rule stays byte-identical
|
|
674
|
+
// and its max-width/max-height backstop still bounds a stale inline size.
|
|
675
|
+
//
|
|
676
|
+
// A gesture ends on pointerup/pointercancel, and — like graph/composer.mjs —
|
|
677
|
+
// on window blur, lostpointercapture (the sheet was hidden, the capture was
|
|
678
|
+
// taken) and a pointermove that reports no button held (the release landed
|
|
679
|
+
// outside the window and never reached us). All of those COMMIT: a half-done
|
|
680
|
+
// resize is still a size the user chose, unlike a half-drawn wire. Escape is
|
|
681
|
+
// the one CANCEL: the pre-drag size comes back and nothing is stored. Only a
|
|
682
|
+
// gesture that actually moved persists, and only the axis it dragged — the
|
|
683
|
+
// other axis keeps the stored preference, so a click on a dock-clamped sheet
|
|
684
|
+
// can never write the clamp back over a larger preference.
|
|
685
|
+
function buildResizeHandle(edge) {
|
|
686
|
+
const h = make('div', `ask-resize ask-resize-${edge}`);
|
|
687
|
+
h.setAttribute('data-ask-resize', edge);
|
|
688
|
+
h.setAttribute('aria-hidden', 'true');
|
|
689
|
+
h.addEventListener('pointerdown', (e) => startResize(e, edge, h));
|
|
690
|
+
h.addEventListener('lostpointercapture', () => { if (st.drag && st.drag.handle === h) finishResize(); });
|
|
691
|
+
h.addEventListener('dblclick', (e) => { e.preventDefault(); resetSize(); });
|
|
692
|
+
return h;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/** The dock's content box in px; zeros when there is no layout (hidden, detached, jsdom). */
|
|
696
|
+
function dockInner() {
|
|
697
|
+
const d = el.dock;
|
|
698
|
+
const w = d ? d.clientWidth : 0;
|
|
699
|
+
const h = d ? d.clientHeight : 0;
|
|
700
|
+
return {
|
|
701
|
+
w: w > 0 ? w - 2 * ASK_SHEET_SIZE.dockPadX : 0,
|
|
702
|
+
h: h > 0 ? h - ASK_SHEET_SIZE.dockPadBottom - ASK_SHEET_SIZE.topGap : 0,
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Clamp to [min, dock inner]. The dock bound wins when the two conflict (a narrow viewport). */
|
|
707
|
+
function clampSize(size) {
|
|
708
|
+
const inner = dockInner();
|
|
709
|
+
const maxW = inner.w > 0 ? inner.w : Infinity;
|
|
710
|
+
const maxH = inner.h > 0 ? inner.h : Infinity;
|
|
711
|
+
return {
|
|
712
|
+
w: Math.round(Math.min(Math.max(size.w, ASK_SHEET_SIZE.minW), maxW)),
|
|
713
|
+
h: Math.round(Math.min(Math.max(size.h, ASK_SHEET_SIZE.minH), maxH)),
|
|
714
|
+
};
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/** Write the inline size, or clear it (null) so min(782px,100%) rules again. */
|
|
718
|
+
function applySize(size) {
|
|
719
|
+
if (size) {
|
|
720
|
+
el.sheet.style.width = `${size.w}px`;
|
|
721
|
+
el.sheet.style.height = `${size.h}px`;
|
|
722
|
+
} else {
|
|
723
|
+
el.sheet.style.removeProperty('width');
|
|
724
|
+
el.sheet.style.removeProperty('height');
|
|
725
|
+
}
|
|
726
|
+
st.applied = size;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Re-apply the persisted preference against the current dock — on open and on window resize. */
|
|
730
|
+
function restoreSize() {
|
|
731
|
+
applySize(st.size ? clampSize(st.size) : null);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
/** The sheet's live border-box size; the applied/default size when there is no layout. */
|
|
735
|
+
function currentSize() {
|
|
736
|
+
const w = el.sheet.offsetWidth;
|
|
737
|
+
const h = el.sheet.offsetHeight;
|
|
738
|
+
if (w > 0 && h > 0) return { w, h };
|
|
739
|
+
return st.applied ? { w: st.applied.w, h: st.applied.h } : { w: ASK_SHEET_SIZE.defaultW, h: ASK_SHEET_SIZE.defaultH };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function startResize(e, edge, handle) {
|
|
743
|
+
if (st.drag || st.destroyed || (e.button != null && e.button !== 0)) return;
|
|
744
|
+
const start = currentSize();
|
|
745
|
+
st.drag = {
|
|
746
|
+
edge, handle, pointerId: e.pointerId, x: e.clientX, y: e.clientY, w: start.w, h: start.h,
|
|
747
|
+
moved: false, // set by the first pointermove that applies a size
|
|
748
|
+
before: st.applied ? { w: st.applied.w, h: st.applied.h } : null, // what Escape restores
|
|
749
|
+
};
|
|
750
|
+
handle.classList.add('is-active');
|
|
751
|
+
el.sheet.classList.add('is-resizing');
|
|
752
|
+
// Capture is a bonus, never a precondition (Chrome throws for a synthetic
|
|
753
|
+
// pointerId; jsdom has no such method) — the document listeners carry the
|
|
754
|
+
// gesture either way, exactly like graph/composer.mjs.
|
|
755
|
+
try { handle.setPointerCapture?.(e.pointerId); } catch { /* synthetic pointer */ }
|
|
756
|
+
doc.addEventListener('pointermove', onResizeMove);
|
|
757
|
+
doc.addEventListener('pointerup', onResizeEnd);
|
|
758
|
+
doc.addEventListener('pointercancel', onResizeEnd);
|
|
759
|
+
win.addEventListener('blur', onResizeBlur);
|
|
760
|
+
e.preventDefault(); // no text selection / focus steal mid-drag
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function samePointer(g, e) {
|
|
764
|
+
return g.pointerId == null || e.pointerId == null || e.pointerId === g.pointerId;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function onResizeMove(e) {
|
|
768
|
+
const g = st.drag;
|
|
769
|
+
if (!g || !samePointer(g, e)) return;
|
|
770
|
+
if (e.buttons === 0) { finishResize(); return; } // the release never reached us
|
|
771
|
+
g.moved = true;
|
|
772
|
+
const dx = e.clientX - g.x;
|
|
773
|
+
const dy = e.clientY - g.y;
|
|
774
|
+
let w = g.w;
|
|
775
|
+
let h = g.h;
|
|
776
|
+
if (g.edge === 'w' || g.edge === 'nw') w = g.w - 2 * dx; // left edge: leftwards grows
|
|
777
|
+
if (g.edge === 'e' || g.edge === 'ne') w = g.w + 2 * dx; // right edge: rightwards grows
|
|
778
|
+
if (g.edge === 'n' || g.edge === 'ne' || g.edge === 'nw') h = g.h - dy; // top edge: upwards grows
|
|
779
|
+
applySize(clampSize({ w, h }));
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function onResizeEnd(e) {
|
|
783
|
+
const g = st.drag;
|
|
784
|
+
if (!g || !samePointer(g, e)) return;
|
|
785
|
+
finishResize();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function onResizeBlur() { finishResize(); }
|
|
789
|
+
|
|
790
|
+
/** Idempotent; also run from destroy() so a mid-drag unmount leaves no document listeners. */
|
|
791
|
+
function finishResize() {
|
|
792
|
+
const g = endResize();
|
|
793
|
+
if (!g || !g.moved || !st.applied) return;
|
|
794
|
+
// Persist the dragged axis only; the other keeps the stored preference (or,
|
|
795
|
+
// with none stored, the size it had) — the clamp is never written back.
|
|
796
|
+
const movesW = g.edge !== 'n';
|
|
797
|
+
const movesH = g.edge !== 'e' && g.edge !== 'w';
|
|
798
|
+
const prev = st.size;
|
|
799
|
+
st.size = {
|
|
800
|
+
w: movesW || !prev ? st.applied.w : prev.w,
|
|
801
|
+
h: movesH || !prev ? st.applied.h : prev.h,
|
|
802
|
+
};
|
|
803
|
+
storeSize(st.size);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/** Escape: the pre-drag size comes back and nothing is stored. */
|
|
807
|
+
function cancelResize() {
|
|
808
|
+
const g = endResize();
|
|
809
|
+
if (!g) return;
|
|
810
|
+
applySize(g.before);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
/** Tear the gesture down (listeners, classes, capture) and hand it back; null when none. */
|
|
814
|
+
function endResize() {
|
|
815
|
+
const g = st.drag;
|
|
816
|
+
if (!g) return null;
|
|
817
|
+
st.drag = null;
|
|
818
|
+
doc.removeEventListener('pointermove', onResizeMove);
|
|
819
|
+
doc.removeEventListener('pointerup', onResizeEnd);
|
|
820
|
+
doc.removeEventListener('pointercancel', onResizeEnd);
|
|
821
|
+
win.removeEventListener('blur', onResizeBlur);
|
|
822
|
+
g.handle.classList.remove('is-active');
|
|
823
|
+
el.sheet.classList.remove('is-resizing');
|
|
824
|
+
try { if (g.handle.hasPointerCapture?.(g.pointerId)) g.handle.releasePointerCapture(g.pointerId); } catch { /* already gone */ }
|
|
825
|
+
return g;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/** Double-click on any grip: back to the stylesheet default and forget the stored size. */
|
|
829
|
+
function resetSize() {
|
|
830
|
+
finishResize();
|
|
831
|
+
st.size = null;
|
|
832
|
+
applySize(null);
|
|
833
|
+
storeSize(null);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Window resize or dock resize (the rail toggling, the dock's slide): re-clamp — never under a held pointer. */
|
|
837
|
+
function onWinResize() {
|
|
838
|
+
if (st.destroyed || !st.open || !st.size || st.drag) return;
|
|
839
|
+
restoreSize();
|
|
840
|
+
}
|
|
841
|
+
|
|
622
842
|
// ---- keyboard + pointer routing ------------------------------------------
|
|
623
843
|
function containsNode(rootEl, t) { return !!(t && t.nodeType && rootEl.contains(t)); }
|
|
624
844
|
|
|
@@ -633,6 +853,7 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
633
853
|
|
|
634
854
|
function onDocKeydown(e) {
|
|
635
855
|
if (st.destroyed) return;
|
|
856
|
+
if (st.drag && e.key === 'Escape') { e.preventDefault(); cancelResize(); return; }
|
|
636
857
|
if (isToggleCombo(e)) {
|
|
637
858
|
if (e.repeat || e.isComposing) return;
|
|
638
859
|
e.preventDefault();
|
|
@@ -2104,15 +2325,26 @@ export function createAskPanel({ doc, win, fetch, sendWs, confirm, getPageContex
|
|
|
2104
2325
|
const root = buildRoot();
|
|
2105
2326
|
doc.addEventListener('keydown', onDocKeydown, true);
|
|
2106
2327
|
doc.addEventListener('pointerdown', onDocPointerdown, true);
|
|
2328
|
+
win.addEventListener('resize', onWinResize);
|
|
2329
|
+
// The rail collapsing changes the dock width by 222px with no window resize;
|
|
2330
|
+
// observe the dock itself (guarded: jsdom has no ResizeObserver — P5's idiom).
|
|
2331
|
+
let dockRo = null;
|
|
2332
|
+
if (typeof win.ResizeObserver === 'function') {
|
|
2333
|
+
dockRo = new win.ResizeObserver(onWinResize);
|
|
2334
|
+
dockRo.observe(el.dock);
|
|
2335
|
+
}
|
|
2107
2336
|
|
|
2108
2337
|
function destroy() {
|
|
2109
2338
|
if (st.destroyed) return;
|
|
2110
2339
|
st.destroyed = true;
|
|
2340
|
+
finishResize(); // a mid-drag unmount leaves no document listeners
|
|
2111
2341
|
closePopover({ focusTrigger: false });
|
|
2112
2342
|
if (st.elapsedTimer) { clearInterval(st.elapsedTimer); st.elapsedTimer = null; }
|
|
2113
2343
|
if (el.orb) el.orb.stop();
|
|
2114
2344
|
doc.removeEventListener('keydown', onDocKeydown, true);
|
|
2115
2345
|
doc.removeEventListener('pointerdown', onDocPointerdown, true);
|
|
2346
|
+
win.removeEventListener('resize', onWinResize);
|
|
2347
|
+
if (dockRo) { dockRo.disconnect(); dockRo = null; }
|
|
2116
2348
|
root.remove();
|
|
2117
2349
|
}
|
|
2118
2350
|
|
package/ui/public/index.html
CHANGED
|
@@ -1024,12 +1024,12 @@
|
|
|
1024
1024
|
<section class="card settings-card">
|
|
1025
1025
|
<div class="field">
|
|
1026
1026
|
<div class="label-row">
|
|
1027
|
-
<label for="settingsRoot">Worca
|
|
1028
|
-
<button type="button" class="info-tip" aria-label="About Worca
|
|
1029
|
-
The folder under which Worca
|
|
1027
|
+
<label for="settingsRoot">Worca root folder</label>
|
|
1028
|
+
<button type="button" class="info-tip" aria-label="About Worca root folder">i<span class="tip-content hidden">
|
|
1029
|
+
The folder under which Worca keeps its data — a <code>.worca-cc/</code> directory holding the
|
|
1030
1030
|
history store, the project list, and saved workflows. Leave empty to use the default.
|
|
1031
1031
|
Changes apply to new runs immediately. Changing the root does <b>not</b> move existing
|
|
1032
|
-
data: Worca
|
|
1032
|
+
data: Worca starts a fresh <code>.worca-cc/</code> under the new folder; history under
|
|
1033
1033
|
the old root is left in place.
|
|
1034
1034
|
</span></button>
|
|
1035
1035
|
</div>
|
|
@@ -1231,6 +1231,26 @@
|
|
|
1231
1231
|
</div>
|
|
1232
1232
|
<small class="hint" id="chatSettingsMsg"></small>
|
|
1233
1233
|
</section>
|
|
1234
|
+
|
|
1235
|
+
<!-- About: intentionally no info-tip and no .hint (test/ui-settings-tooltips.test.mjs pins both). -->
|
|
1236
|
+
<section class="card settings-card" id="about-card">
|
|
1237
|
+
<div class="label-row">
|
|
1238
|
+
<h2>About</h2>
|
|
1239
|
+
</div>
|
|
1240
|
+
<div class="about-row">
|
|
1241
|
+
<span class="about-key">Version</span>
|
|
1242
|
+
<!-- href is painted from app.releaseUrl (the worca-app-v<version> tag);
|
|
1243
|
+
without one the anchor is plain text, so the placeholder never links. -->
|
|
1244
|
+
<a class="about-val about-version" id="aboutVersion"
|
|
1245
|
+
target="_blank" rel="noopener noreferrer">—</a>
|
|
1246
|
+
</div>
|
|
1247
|
+
<div class="about-row">
|
|
1248
|
+
<span class="about-key">Repository</span>
|
|
1249
|
+
<a class="about-val about-link" id="aboutRepoLink"
|
|
1250
|
+
href="https://github.com/SinishaDjukic/worca-cc"
|
|
1251
|
+
target="_blank" rel="noopener noreferrer">github.com/SinishaDjukic/worca-cc</a>
|
|
1252
|
+
</div>
|
|
1253
|
+
</section>
|
|
1234
1254
|
</section>
|
|
1235
1255
|
<!-- /pane general -->
|
|
1236
1256
|
|
package/ui/public/style.css
CHANGED
|
@@ -1574,10 +1574,23 @@ fieldset.source legend{font-weight:600;font-size:13px;padding:0 6px;color:var(--
|
|
|
1574
1574
|
.topnav button.active .topnav-amt{color:inherit;}
|
|
1575
1575
|
|
|
1576
1576
|
/* ---------- Settings cards ---------- */
|
|
1577
|
-
/* One width/centering/gap rule for
|
|
1577
|
+
/* One width/centering/gap rule for every Settings card. */
|
|
1578
1578
|
.settings-card{max-width:760px;padding:22px;margin-left:auto;margin-right:auto;}
|
|
1579
1579
|
.settings-card + .settings-card{margin-top:18px;}
|
|
1580
1580
|
|
|
1581
|
+
/* About card rows. .about-val and .about-link share specificity on one anchor, so
|
|
1582
|
+
.about-link must stay AFTER .about-val (test/ui-settings-about.test.mjs pins it). */
|
|
1583
|
+
.about-row{display:flex;align-items:baseline;gap:10px;font-size:13px;}
|
|
1584
|
+
.about-row + .about-row{margin-top:10px;}
|
|
1585
|
+
.about-key{flex:0 0 96px;font-weight:600;color:var(--ink-2);}
|
|
1586
|
+
.about-val{color:var(--ink);min-width:0;}
|
|
1587
|
+
.about-version{font-family:var(--mono);}
|
|
1588
|
+
/* Once painted with a release-tag href the version reads as a link. */
|
|
1589
|
+
.about-version[href]{color:var(--green-ink);text-decoration:none;}
|
|
1590
|
+
.about-version[href]:hover{text-decoration:underline;}
|
|
1591
|
+
.about-link{color:var(--green-ink);font-weight:600;text-decoration:none;overflow-wrap:anywhere;}
|
|
1592
|
+
.about-link:hover{text-decoration:underline;}
|
|
1593
|
+
|
|
1581
1594
|
/* ---------- Budget readout (Settings) ---------- */
|
|
1582
1595
|
/* Section gap before the limit fields (the disclaimer hint that used to sit
|
|
1583
1596
|
here moved into the card heading's ⓘ tooltip). */
|
|
@@ -2860,7 +2873,8 @@ body.rail-collapsed .ask-dock{left:76px;}
|
|
|
2860
2873
|
font-family:var(--mono);font-weight:400;font-size:10.5px;}
|
|
2861
2874
|
|
|
2862
2875
|
.ask-sheet{pointer-events:auto;position:relative;display:flex;flex-direction:column;overflow:hidden;
|
|
2863
|
-
width:min(782px,100%);height:min(669px,calc(100% - 20px));
|
|
2876
|
+
width:min(782px,100%);height:min(669px,calc(100% - 20px));max-width:100%;max-height:calc(100% - 20px);
|
|
2877
|
+
background:var(--panel);
|
|
2864
2878
|
border:1px solid var(--line-2);border-radius:var(--r-card);
|
|
2865
2879
|
box-shadow:0 18px 60px rgba(25,25,27,.14),0 2px 6px rgba(25,25,27,.06);
|
|
2866
2880
|
animation:wr-rise .26s cubic-bezier(.2,.7,.3,1) both;}
|
|
@@ -2957,7 +2971,7 @@ body.rail-collapsed .ask-dock{left:76px;}
|
|
|
2957
2971
|
.ask-agent-log-t{flex:0 0 auto;color:var(--seq);}
|
|
2958
2972
|
.ask-agent-log-text{min-width:0;white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
2959
2973
|
|
|
2960
|
-
.ask-answer{max-width:
|
|
2974
|
+
.ask-answer{max-width:100%;font-size:13.5px;line-height:1.72;color:var(--ink);}
|
|
2961
2975
|
.ask-answer-plain{white-space:pre-wrap;overflow-wrap:anywhere;}
|
|
2962
2976
|
.ask-md :is(h1,h2,h3,h4,h5,h6){margin:14px 0 6px;font-size:13.5px;font-weight:600;color:var(--ink);}
|
|
2963
2977
|
.ask-md h1{font-size:15px;}
|
|
@@ -3145,6 +3159,41 @@ body.rail-collapsed .ask-dock{left:76px;}
|
|
|
3145
3159
|
.ask-wt-row .ask-runinfo-sub.ask-wt-path{cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:260px;}
|
|
3146
3160
|
.ask-wt-row .ask-thread-trash{flex:none;}
|
|
3147
3161
|
|
|
3162
|
+
/* Resize grips (spec §10.3 addendum). Invisible edge zones INSIDE the sheet —
|
|
3163
|
+
overflow:hidden would clip anything outside it — that only change the cursor;
|
|
3164
|
+
a 2px inset line appears on hover and stays while dragging (.is-active). No
|
|
3165
|
+
bottom grip: the sheet is bottom-anchored. ask-panel.mjs applies the user's
|
|
3166
|
+
size as inline width/height and clamps it to the dock; the max-* pair on
|
|
3167
|
+
.ask-sheet above is the backstop that keeps a stale inline size inside the
|
|
3168
|
+
dock (the rail collapsing changes the dock width without a window resize).
|
|
3169
|
+
The floor (JS-only, the stylesheet default 782×669: the sheet grows, never
|
|
3170
|
+
shrinks) is not a CSS min-* on purpose — that would overflow narrow viewports
|
|
3171
|
+
that width:min(782px,100%) handles today.
|
|
3172
|
+
Corners: the sheet clips at its rounded corner (inner radius = --r-card minus
|
|
3173
|
+
the 1px border), so a square corner grip's straight inset lines fall almost
|
|
3174
|
+
entirely outside the clip and only a ~3px sliver ever shows. Each corner is
|
|
3175
|
+
therefore exactly one radius wide and carries the same outer radius, so its
|
|
3176
|
+
inset shadow bends along the arc, flush with the border like the straight
|
|
3177
|
+
edges; the straight grips start where the arc ends (--ask-corner). */
|
|
3178
|
+
.ask-resize{position:absolute;z-index:5;background:transparent;touch-action:none;--ask-grip:transparent;
|
|
3179
|
+
--ask-corner:calc(var(--r-card) - 1px);transition:box-shadow .15s;}
|
|
3180
|
+
.ask-resize:hover{--ask-grip:var(--ink-3);}
|
|
3181
|
+
.ask-resize.is-active{--ask-grip:var(--ink);}
|
|
3182
|
+
.ask-resize-n{top:0;left:var(--ask-corner);right:var(--ask-corner);height:6px;cursor:ns-resize;box-shadow:inset 0 2px 0 var(--ask-grip);}
|
|
3183
|
+
.ask-resize-e{top:var(--ask-corner);right:0;bottom:var(--ask-corner);width:6px;cursor:ew-resize;box-shadow:inset -2px 0 0 var(--ask-grip);}
|
|
3184
|
+
.ask-resize-w{top:var(--ask-corner);left:0;bottom:var(--ask-corner);width:6px;cursor:ew-resize;box-shadow:inset 2px 0 0 var(--ask-grip);}
|
|
3185
|
+
.ask-resize-ne{top:0;right:0;width:var(--ask-corner);height:var(--ask-corner);cursor:nesw-resize;
|
|
3186
|
+
border-top-right-radius:var(--ask-corner);box-shadow:inset -2px 2px 0 var(--ask-grip);}
|
|
3187
|
+
.ask-resize-nw{top:0;left:0;width:var(--ask-corner);height:var(--ask-corner);cursor:nwse-resize;
|
|
3188
|
+
border-top-left-radius:var(--ask-corner);box-shadow:inset 2px 2px 0 var(--ask-grip);}
|
|
3189
|
+
.ask-sheet.is-resizing{user-select:none;}
|
|
3190
|
+
/* The ne corner grip (one radius square, z-index:5) overlaps the top-right
|
|
3191
|
+
corner of the header's last icon button (30×30 at padding 13px/14px): the
|
|
3192
|
+
buttons sit above the grips so a click there still closes/opens instead of
|
|
3193
|
+
starting a resize, and a hurried double-click never resets the size. The
|
|
3194
|
+
grip's visible band hugs the border, well outside the button box. */
|
|
3195
|
+
.ask-header .ask-icon-btn{position:relative;z-index:6;}
|
|
3196
|
+
|
|
3148
3197
|
/* ---------- Ask Worca dock clearance ---------- */
|
|
3149
3198
|
/* The dock is a fixed overlay -- .ask-dock above: 26px dock padding-bottom plus the
|
|
3150
3199
|
~47px pill occludes a ~73px strip at the bottom of every scrollport, while the page
|
package/ui/server.mjs
CHANGED
|
@@ -183,7 +183,24 @@ const PUBLIC_DIR = path.join(__dirname, 'public');
|
|
|
183
183
|
const AGENTS_DIR = path.join(PROJECT_ROOT, 'agents');
|
|
184
184
|
const SKILLS_DIR = path.join(PROJECT_ROOT, 'skills');
|
|
185
185
|
const require = createRequire(import.meta.url);
|
|
186
|
-
const
|
|
186
|
+
const PKG = require('../package.json');
|
|
187
|
+
const PKG_VERSION = PKG.version;
|
|
188
|
+
// Settings ▸ About identity, fixed at module load. repository.url is normalised
|
|
189
|
+
// from npm's git forms (git+https, ssh://git@, git://, scp-style git@host:path)
|
|
190
|
+
// to a browsable https URL; releaseUrl points at the tag the release workflow
|
|
191
|
+
// publishes from (.github/workflows/release-npm-app.yml: worca-app-v<version>).
|
|
192
|
+
const repoWebUrl = (raw) => String(raw || '')
|
|
193
|
+
.replace(/^git\+/, '')
|
|
194
|
+
.replace(/^ssh:\/\/git@/, 'https://')
|
|
195
|
+
.replace(/^git:\/\//, 'https://')
|
|
196
|
+
.replace(/^git@([^:/]+):/, 'https://$1/')
|
|
197
|
+
.replace(/\.git$/, '');
|
|
198
|
+
const APP_REPO_URL = repoWebUrl(PKG.repository && PKG.repository.url);
|
|
199
|
+
const APP_INFO = Object.freeze({
|
|
200
|
+
version: PKG_VERSION || '',
|
|
201
|
+
repoUrl: APP_REPO_URL,
|
|
202
|
+
releaseUrl: APP_REPO_URL && PKG_VERSION ? `${APP_REPO_URL}/releases/tag/worca-app-v${PKG_VERSION}` : '',
|
|
203
|
+
});
|
|
187
204
|
const HLJS_LANGUAGE_FILE_RE = /^[a-z0-9][a-z0-9-]{0,63}\.min\.js$/;
|
|
188
205
|
// Primaries plus the sub-language grammars their instances register
|
|
189
206
|
// (hljs-loader.mjs); a shipped but unmapped grammar stays a plain 404.
|
|
@@ -2777,6 +2794,10 @@ app.get('/api/workspaces/:id/runs/:runId/artifact', async (req, res) => {
|
|
|
2777
2794
|
// projectsRootDefault : what applies when projectsRoot is blank — the env tier
|
|
2778
2795
|
// when exported, else defaultRoot(). The UI placeholder.
|
|
2779
2796
|
// Additive; `default` keeps its `root` meaning.
|
|
2797
|
+
// app : { version, repoUrl, releaseUrl } — static identity for
|
|
2798
|
+
// the About card (APP_INFO, from package.json). GET-only:
|
|
2799
|
+
// it is not a setting, so POST keeps echoing
|
|
2800
|
+
// settingsState() + chat unchanged.
|
|
2780
2801
|
// POST /api/settings -> set either key and return the resulting full state.
|
|
2781
2802
|
// Only keys PRESENT in the body are written, so a projectsRoot-only POST can
|
|
2782
2803
|
// never reset `root` (and vice versa). An explicitly empty value still resets
|
|
@@ -2845,7 +2866,7 @@ app.post('/api/shutdown', (req, res) => {
|
|
|
2845
2866
|
});
|
|
2846
2867
|
|
|
2847
2868
|
app.get('/api/settings', (_req, res) => {
|
|
2848
|
-
res.json({ ...settingsState(), chat: chatPrefs() });
|
|
2869
|
+
res.json({ ...settingsState(), chat: chatPrefs(), app: APP_INFO });
|
|
2849
2870
|
});
|
|
2850
2871
|
|
|
2851
2872
|
app.get('/api/budget', (_req, res) => {
|