mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-30 09:42:21 +01:00
Compare commits
4
Commits
d36879bd50
...
c12b8ab6c9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c12b8ab6c9 | ||
|
|
e812a29233 | ||
|
|
ca4973c41f | ||
|
|
91b4171b3f |
@@ -86,6 +86,7 @@ Bundled in `static/fonts/`:
|
||||
| [Fira Code](https://github.com/tonsky/FiraCode) | SIL Open Font License 1.1 | Nikita Prokopov & contributors |
|
||||
| [Inter](https://github.com/rsms/inter) | SIL Open Font License 1.1 | Rasmus Andersson |
|
||||
| [GohuFont](https://font.gohu.org/) (`fonts/custom/GohuFont.ttf`) | WTFPL | Hugo Chargois |
|
||||
| [OpenDyslexic](https://opendyslexic.org/) (`fonts/OpenDyslexic-{Regular,Bold}.woff2`) | SIL Open Font License 1.1 ([`licenses/OpenDyslexic-OFL.txt`](licenses/OpenDyslexic-OFL.txt)) | Abbie Gonzalez |
|
||||
|
||||
## Python dependencies
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
Copyright (c) 2019-07-29, Abbie Gonzalez (https://abbiecod.es|support@abbiecod.es),
|
||||
with Reserved Font Name OpenDyslexic.
|
||||
Copyright (c) 12/2012 - 2019
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
+12
-1
@@ -44,6 +44,17 @@ from routes.email_helpers import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Recovers a `[{"action": ...}, ...]` JSON array from raw LLM output when the
|
||||
# fenced-block strip leaves nothing usable. Runs on model output influenced by
|
||||
# untrusted email bodies, so it must not backtrack: the object content class is
|
||||
# `[^{}]` (brace-delimited, greedy) rather than the old `[^[\]]*?` lazy runs,
|
||||
# which exploded exponentially on inputs like `[{"action"},{` + `}},{{` * N
|
||||
# (CodeQL py/redos #198).
|
||||
_CAL_ACTION_ARRAY_RE = re.compile(
|
||||
r'\[\s*\{[^{}]*"action"[^{}]*\}\s*(?:,\s*\{[^{}]*\}\s*)*\]',
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
|
||||
def _owner_for_email_account(account_id: str | None) -> str:
|
||||
if not account_id:
|
||||
@@ -558,7 +569,7 @@ async def _auto_summarize_pass_single(days_back: int = 1, account_id: str | None
|
||||
cal_extract = _strip_think(_raw_original)
|
||||
cal_extract = re.sub(r"^```(?:json)?\s*|\s*```$", "", cal_extract, flags=re.MULTILINE).strip()
|
||||
if not cal_extract and _raw_original:
|
||||
matches = list(re.finditer(r'\[\s*\{[^[\]]*?"action"[^[\]]*?\}\s*(?:,\s*\{[^[\]]*?\}\s*)*\]', _raw_original, re.DOTALL))
|
||||
matches = list(_CAL_ACTION_ARRAY_RE.finditer(_raw_original))
|
||||
if matches:
|
||||
cal_extract = matches[-1].group()
|
||||
logger.info(f"[cal-extract] uid={uid.decode() if isinstance(uid, bytes) else uid} folder={_folder} subj={subject[:50]!r} raw_len={len(cal_extract)} orig_len={len(_raw_original)} raw={cal_extract[:800]!r}")
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+16
-2
@@ -76,7 +76,7 @@
|
||||
}
|
||||
// Apply font early
|
||||
if (t && t.font) {
|
||||
var fm = {mono:"'Fira Code', monospace",sans:"system-ui, -apple-system, 'Segoe UI', sans-serif",serif:"Georgia, 'Times New Roman', serif"};
|
||||
var fm = {mono:"'Fira Code', monospace",sans:"system-ui, -apple-system, 'Segoe UI', sans-serif",serif:"Georgia, 'Times New Roman', serif",opendyslexic:"'OpenDyslexic', sans-serif"};
|
||||
if (fm[t.font]) { s.setProperty('--font-family', fm[t.font]); }
|
||||
else { s.setProperty('--font-family', "'" + t.font.replace(/'/g,'') + "', sans-serif"); }
|
||||
}
|
||||
@@ -84,6 +84,12 @@
|
||||
if (t && t.density && t.density !== 'comfortable') {
|
||||
document.documentElement.classList.add('density-' + t.density);
|
||||
}
|
||||
// Apply UI text-size scale early (global accessibility pref, independent
|
||||
// of the active theme) so there's no flash on load.
|
||||
try {
|
||||
var _us = localStorage.getItem('odysseus-ui-scale');
|
||||
if (_us && _us !== '100') document.documentElement.classList.add('ui-scale-' + _us);
|
||||
} catch(e){}
|
||||
// Apply background pattern on body once available
|
||||
if (t && t.bgPattern && t.bgPattern !== 'none') {
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
@@ -581,6 +587,7 @@
|
||||
<option value="mono">Monospace</option>
|
||||
<option value="sans">Sans-serif</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="opendyslexic">OpenDyslexic (dyslexia-friendly)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="theme-fd-group">
|
||||
@@ -591,6 +598,13 @@
|
||||
<option value="spacious">Spacious</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="theme-fd-group">
|
||||
<label class="theme-fd-label">Text size</label>
|
||||
<select id="theme-text-size-select" class="theme-fd-select" aria-label="Text size">
|
||||
<option value="100">Default</option>
|
||||
<option value="125">Larger</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="theme-fd-group" id="theme-frosted-group">
|
||||
<label class="theme-fd-label" for="theme-frosted-toggle">Frosted</label>
|
||||
<label class="admin-switch" style="margin-top:4px;">
|
||||
@@ -1318,7 +1332,7 @@
|
||||
|
||||
<!-- Cookbook Modal -->
|
||||
<div id="cookbook-modal" class="modal hidden">
|
||||
<div class="modal-content" role="dialog" aria-label="Cookbook" style="width: min(780px, 92vw); height: 94vh; max-height: 94vh; background: var(--bg);">
|
||||
<div class="modal-content" role="dialog" aria-label="Cookbook" style="width: min(780px, 92vw); background: var(--bg);">
|
||||
<div class="modal-header">
|
||||
<h4 style="margin:0;margin-right:auto"><svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:6px"><path d="M12 7v14"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/></svg>Cookbook</h4>
|
||||
<button class="close-btn" id="close-cookbook-modal" aria-label="Close cookbook">✖</button>
|
||||
|
||||
@@ -666,7 +666,7 @@ import * as Modals from './modalManager.js';
|
||||
overlay.className = 'modal pdf-export-overlay';
|
||||
overlay.style.cssText = 'pointer-events:auto;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);';
|
||||
overlay.innerHTML = `
|
||||
<div class="modal-content" style="width:min(780px,94vw);max-height:86vh;">
|
||||
<div class="modal-content" style="width:min(780px,94vw);">
|
||||
<div class="modal-header">
|
||||
<h4>Export filled PDF</h4>
|
||||
<button id="pdf-export-close" class="modal-close" title="Close">×</button>
|
||||
|
||||
@@ -1595,7 +1595,7 @@ let _libraryArchivedView = false; // Documents tab showing archived docs?
|
||||
modal.className = 'modal';
|
||||
modal.id = 'doclib-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content doclib-modal-content" style="width:min(640px, 92vw);max-height:85vh;background:var(--bg);">
|
||||
<div class="modal-content doclib-modal-content" style="width:min(640px, 92vw);background:var(--bg);">
|
||||
<div class="modal-header">
|
||||
<!-- Header title + icon mirror the currently-active sub-tab (Chats /
|
||||
Documents / Research / Archive) so the user sees ONE icon at
|
||||
|
||||
@@ -858,7 +858,7 @@ export function openEmailLibrary(opts = {}) {
|
||||
modal.className = 'modal';
|
||||
modal.id = 'email-lib-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content doclib-modal-content" style="width:min(720px, 92vw);max-height:85vh;background:var(--bg);">
|
||||
<div class="modal-content doclib-modal-content" style="width:min(720px, 92vw);background:var(--bg);">
|
||||
<div class="modal-header">
|
||||
<h4>
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="vertical-align:-2px;margin-right:4px;">
|
||||
@@ -4866,7 +4866,7 @@ async function _openEmailAsTab(em, folder) {
|
||||
modal.className = 'modal email-reader-tab-modal';
|
||||
modal.id = modalId;
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content doclib-modal-content email-reader-tab-content" style="background:var(--bg);width:min(720px, 92vw);max-height:85vh;display:flex;flex-direction:column;">
|
||||
<div class="modal-content doclib-modal-content email-reader-tab-content" style="background:var(--bg);width:min(720px, 92vw);display:flex;flex-direction:column;">
|
||||
<div class="modal-header">
|
||||
<h4 style="display:flex;align-items:center;gap:6px;min-width:0;flex:1;">
|
||||
<span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-left:8px;">${_esc(em.subject || '(no subject)')}</span>
|
||||
@@ -5101,7 +5101,7 @@ async function _openEmailWindow(em, folder) {
|
||||
modal.id = winId;
|
||||
modal.style.cssText = 'pointer-events:none;background:transparent;';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content email-window-content" style="width:min(640px, 92vw);max-height:80vh;display:flex;flex-direction:column;background:var(--bg);">
|
||||
<div class="modal-content email-window-content" style="width:min(640px, 92vw);display:flex;flex-direction:column;background:var(--bg);">
|
||||
<div class="modal-header">
|
||||
<h4 style="display:flex;align-items:center;gap:6px;min-width:0;flex:1;">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
|
||||
|
||||
+22
-6
@@ -483,6 +483,7 @@ export function processWithThinking(text) {
|
||||
export function mdToHtml(src, opts) {
|
||||
const allowedHtmlBlocks = [];
|
||||
const codeBlocks = [];
|
||||
const inlineCodeBlocks = [];
|
||||
const mermaidBlocks = [];
|
||||
let s = (src ?? '');
|
||||
|
||||
@@ -521,6 +522,19 @@ export function mdToHtml(src, opts) {
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Extract inline code spans before the link/autolink/HTML passes, mirroring
|
||||
// the fenced-block handling above. A URL inside `inline code` (e.g.
|
||||
// `irm http://127.0.0.1:3000/x`) is preceded by a space, so the bare-URL
|
||||
// autolink matches it, wraps it in an <a> tag, and swaps that for an
|
||||
// ___ALLOWED_HTML_ placeholder — corrupting the command. The old inline-code
|
||||
// pass ran after those passes, too late to protect it.
|
||||
s = s.replace(/`([^`]+?)`/g, (match, code) => {
|
||||
if (code.startsWith('___CODE_BLOCK_') || code.startsWith('___MERMAID_BLOCK_')) return match;
|
||||
const placeholder = `___INLINE_CODE_${inlineCodeBlocks.length}___`;
|
||||
inlineCodeBlocks.push(`<code>${escapeHtml(code)}</code>`);
|
||||
return placeholder;
|
||||
});
|
||||
|
||||
// Repair common ways the agent mangles the entity-anchor convention
|
||||
// (`[Name](#kind-<id>)`). Models reliably get the single-link case
|
||||
// right but slip into other formats when listing many in a table.
|
||||
@@ -678,12 +692,6 @@ export function mdToHtml(src, opts) {
|
||||
return html;
|
||||
});
|
||||
|
||||
// Inline code (but not placeholders)
|
||||
s = s.replace(/`([^`]+?)`/g, (match, code) => {
|
||||
if (code.startsWith('___CODE_BLOCK_') || code.startsWith('___ALLOWED_HTML_')) return match;
|
||||
return `<code>${code}</code>`;
|
||||
});
|
||||
|
||||
// Horizontal rules (must come before bold/italic to avoid * conflicts)
|
||||
s = s.replace(/^(?:---|\*\*\*|___)\s*$/gm, '<hr>');
|
||||
|
||||
@@ -756,6 +764,14 @@ export function mdToHtml(src, opts) {
|
||||
s = s.replace(`___CODE_BLOCK_${index}___`, block);
|
||||
});
|
||||
|
||||
// Restore inline code spans last, so placeholders carried inside restored
|
||||
// <a>/allowed-HTML blocks are resolved too. The function replacer keeps the
|
||||
// escaped code literal — e.g. a shell snippet like `echo $1` is not treated
|
||||
// as a regex back-reference.
|
||||
inlineCodeBlocks.forEach((block, index) => {
|
||||
s = s.replace(`___INLINE_CODE_${index}___`, () => block);
|
||||
});
|
||||
|
||||
return _useSvgEmoji() ? svgifyEmoji(s, opts) : s;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -1802,7 +1802,7 @@ async function _showSkillSource(name) {
|
||||
wrap.className = 'modal';
|
||||
wrap.style.display = 'block';
|
||||
wrap.innerHTML = `
|
||||
<div class="modal-content" style="max-width:760px;max-height:85vh;display:flex;flex-direction:column">
|
||||
<div class="modal-content" style="max-width:760px;display:flex;flex-direction:column">
|
||||
<div class="modal-header">
|
||||
<h4>SKILL.md — <code>${esc(name)}</code></h4>
|
||||
<span style="flex:1"></span>
|
||||
|
||||
@@ -101,6 +101,8 @@ function _setupProviderFromInput(input) {
|
||||
xai: 'xai',
|
||||
grok: 'xai',
|
||||
nvidia: 'nvidia',
|
||||
opencodezen: 'opencode-zen',
|
||||
opencodego: 'opencode-go',
|
||||
};
|
||||
return SETUP_PROVIDER_URLS[aliases[raw] || raw] || null;
|
||||
}
|
||||
@@ -129,6 +131,8 @@ function _extractSetupProviderCredential(input) {
|
||||
['google', 'gemini'], ['gemini', 'gemini'],
|
||||
['x ai', 'xai'], ['xai', 'xai'], ['grok', 'xai'],
|
||||
['nvidia', 'nvidia'],
|
||||
['opencode zen', 'opencode-zen'], ['opencode-zen', 'opencode-zen'],
|
||||
['opencode go', 'opencode-go'], ['opencode-go', 'opencode-go'],
|
||||
];
|
||||
for (const [alias, key] of providerAliases) {
|
||||
const re = new RegExp('(^|\\s|[,;:])(' + alias.replace(/\s+/g, '\\s+') + ')(?=$|\\s|[,;:])', 'i');
|
||||
|
||||
@@ -24,6 +24,7 @@ export const KEYS = {
|
||||
SECTION_ORDER: 'sidebar-section-order',
|
||||
ADMIN_LAST_TAB: 'admin-last-tab',
|
||||
DENSITY: 'odysseus-density',
|
||||
UI_SCALE: 'odysseus-ui-scale',
|
||||
WORKSPACE: 'odysseus-workspace'
|
||||
};
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ const FONT_MAP = {
|
||||
mono: "'Fira Code', monospace",
|
||||
sans: "system-ui, -apple-system, 'Segoe UI', sans-serif",
|
||||
serif: "Georgia, 'Times New Roman', serif",
|
||||
opendyslexic: "'OpenDyslexic', sans-serif",
|
||||
};
|
||||
const DEFAULT_FONT = 'mono';
|
||||
const DEFAULT_DENSITY = 'comfortable';
|
||||
@@ -387,6 +388,20 @@ export function applyFontDensity(font, density) {
|
||||
if (d !== 'comfortable') document.documentElement.classList.add('density-' + d);
|
||||
}
|
||||
|
||||
// UI text-size scale (accessibility). Global and independent of the active
|
||||
// theme, so the chosen size persists across theme switches. Stored as a plain
|
||||
// percentage string ('100' | '110' | '125' | '150').
|
||||
const UI_SCALE_KEY = 'odysseus-ui-scale';
|
||||
const DEFAULT_UI_SCALE = '100';
|
||||
|
||||
export function applyUiScale(scale) {
|
||||
const s = scale || DEFAULT_UI_SCALE;
|
||||
// Only one non-default scale ('125'). Remove any legacy classes too so an
|
||||
// older stored value can't leave a stale zoom applied.
|
||||
document.documentElement.classList.remove('ui-scale-110', 'ui-scale-125', 'ui-scale-140');
|
||||
if (s === '125') document.documentElement.classList.add('ui-scale-125');
|
||||
}
|
||||
|
||||
const _BG_CLASSES = ['bg-pattern-dots',
|
||||
'bg-pattern-synapse', 'bg-pattern-rain', 'bg-pattern-constellations',
|
||||
'bg-pattern-perlin-flow',
|
||||
@@ -1133,6 +1148,18 @@ export function initThemeUI() {
|
||||
const s = getSaved(); if (s) _saveFull(s.name, s.colors);
|
||||
});
|
||||
}
|
||||
const textSizeSelect = document.getElementById('theme-text-size-select');
|
||||
if (textSizeSelect) {
|
||||
const nts = textSizeSelect.cloneNode(true); textSizeSelect.parentNode.replaceChild(nts, textSizeSelect);
|
||||
let initScale = DEFAULT_UI_SCALE;
|
||||
try { initScale = localStorage.getItem(UI_SCALE_KEY) || DEFAULT_UI_SCALE; } catch (e) {}
|
||||
nts.value = initScale;
|
||||
applyUiScale(initScale);
|
||||
nts.addEventListener('change', () => {
|
||||
applyUiScale(nts.value);
|
||||
try { localStorage.setItem(UI_SCALE_KEY, nts.value); } catch (e) {}
|
||||
});
|
||||
}
|
||||
if (patternSelect) {
|
||||
const np = patternSelect.cloneNode(true); patternSelect.parentNode.replaceChild(np, patternSelect);
|
||||
np.value = _initPattern;
|
||||
|
||||
@@ -114,6 +114,10 @@ body {
|
||||
@font-face { font-family: 'Fira Code'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-Regular.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'Fira Code'; font-weight: 600; font-style: normal; font-display: swap; src: url('/static/fonts/FiraCode-SemiBold.woff2') format('woff2'); }
|
||||
|
||||
/* Self-hosted OpenDyslexic — dyslexia-friendly accessibility font option (SIL OFL 1.1) */
|
||||
@font-face { font-family: 'OpenDyslexic'; font-weight: 400; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Regular.woff2') format('woff2'); }
|
||||
@font-face { font-family: 'OpenDyslexic'; font-weight: 700; font-style: normal; font-display: swap; src: url('/static/fonts/OpenDyslexic-Bold.woff2') format('woff2'); }
|
||||
|
||||
/* Code block baseline */
|
||||
pre, code, .hljs {
|
||||
font-size: 0.95em;
|
||||
@@ -158,6 +162,39 @@ html {
|
||||
:root.density-spacious .list-item { padding: 8px 12px; }
|
||||
:root.density-spacious .sidebar .section { padding: 0; }
|
||||
|
||||
/* ── UI text-size scale (accessibility) ──
|
||||
Density only changes the root font-size, which can't move the many
|
||||
hard-coded px sizes. `zoom` scales the whole UI uniformly (px text
|
||||
included) while keeping layout intact, unlike `transform: scale`. */
|
||||
:root.ui-scale-125 { zoom: 1.25; }
|
||||
/* `zoom` makes the 100dvh shell render taller than the real viewport, which
|
||||
pushes the bottom-pinned sidebar account/settings row below the fold (and
|
||||
body's overflow:hidden then clips it). Shrink the shell by the same factor
|
||||
so it fits the viewport exactly. */
|
||||
:root.ui-scale-125 body { height: calc(100dvh / 1.25); }
|
||||
/* Modals/panels under the 1.25x scale: zoom renders a centred, viewport-sized
|
||||
panel ~1.25x taller, pushing its draggable header + close button off-screen
|
||||
(a catch-22 — you can't reach the control to turn the size back down). Divide
|
||||
each max-height by the same factor to keep the original on-screen footprint.
|
||||
Desktop only — the mobile `!important` full-sheet rules win on small screens
|
||||
and stay top-anchored, so their headers are already visible. */
|
||||
:root.ui-scale-125 .modal-content { max-height: calc(85dvh / 1.25); }
|
||||
:root.ui-scale-125 .cal-modal-content { max-height: calc(88dvh / 1.25); }
|
||||
:root.ui-scale-125 .settings-modal-content { max-height: calc(85dvh / 1.25); }
|
||||
:root.ui-scale-125 #theme-popup { max-height: min(calc(85dvh / 1.25), 480px); }
|
||||
/* Cookbook is the one modal that set its height inline (94vh), which beat the
|
||||
.modal-content compensation above and overflowed the viewport at 1.25x
|
||||
(header + close button pushed off-screen). Own its height here so the same
|
||||
zoom compensation applies. */
|
||||
#cookbook-modal .modal-content { height: 94vh; max-height: 94vh; }
|
||||
:root.ui-scale-125 #cookbook-modal .modal-content { height: calc(94dvh / 1.25); max-height: calc(94dvh / 1.25); }
|
||||
/* PDF export modal also set its height inline (86vh) at v1.0; that inline cap
|
||||
beat the .modal-content compensation above and shifted ~1vh at Default when
|
||||
removed. Own its height here so Default is byte-for-byte 86vh and the same
|
||||
1.25x compensation applies. */
|
||||
.pdf-export-overlay .modal-content { max-height: 86vh; }
|
||||
:root.ui-scale-125 .pdf-export-overlay .modal-content { max-height: calc(86dvh / 1.25); }
|
||||
|
||||
/* ── Background Patterns ── */
|
||||
|
||||
:root { --bg-effect-intensity: 1; }
|
||||
|
||||
@@ -170,6 +170,36 @@ def test_extract_thinking_blocks_handles_thought_tag(node_available):
|
||||
assert result["content"] == "Final answer."
|
||||
|
||||
|
||||
def test_url_inside_inline_code_is_not_autolinked(node_available):
|
||||
# A URL inside a backtick span is preceded by a space, so the bare-URL
|
||||
# autolink used to wrap it in an <a> tag (then swap it for an
|
||||
# ___ALLOWED_HTML_ placeholder), corrupting the command shown to the user.
|
||||
html = _run_markdown_case("Run `$j = irm http://127.0.0.1:3000/x` to fetch.")
|
||||
|
||||
assert "<code>$j = irm http://127.0.0.1:3000/x</code>" in html
|
||||
assert "___ALLOWED_HTML_" not in html
|
||||
assert "<a " not in html
|
||||
assert 'href="http://127.0.0.1:3000/x"' not in html
|
||||
|
||||
|
||||
def test_url_outside_inline_code_is_still_autolinked(node_available):
|
||||
# Inline code must not disable autolinking for bare URLs elsewhere in the
|
||||
# same line.
|
||||
html = _run_markdown_case("Use `irm` then visit https://example.com/page now.")
|
||||
|
||||
assert "<code>irm</code>" in html
|
||||
assert 'href="https://example.com/page"' in html
|
||||
|
||||
|
||||
def test_inline_code_content_is_html_escaped(node_available):
|
||||
# Inline code is now extracted before the global escape pass, so it must be
|
||||
# escaped at extraction time (matching the fenced-code-block handling).
|
||||
html = _run_markdown_case("Render `<b>$1 & 'q'</b>` literally.")
|
||||
|
||||
assert "<code><b>$1 & 'q'</b></code>" in html
|
||||
assert "<b>" not in html
|
||||
|
||||
|
||||
def test_dotted_python_import_paths_are_not_autolinked(node_available):
|
||||
html = _run_markdown_case(
|
||||
"from imblearn.combine import SMOTETomek\n"
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
r"""Regression test for ReDoS in the calendar-extract fallback regex.
|
||||
|
||||
CodeQL `py/redos` (#198) flagged the inline array-matcher in
|
||||
`email_pollers.py` that recovers a `[{"action": ...}, ...]` JSON array from
|
||||
raw LLM output (influenced by attacker-supplied email bodies). The original
|
||||
pattern used `[^[\]]*?` lazy runs inside a `(...)*` repetition, which
|
||||
backtracks *exponentially* on inputs like `[{"action"},{` + `}},{{` * N.
|
||||
|
||||
The regex is now a module-level constant so it can be pinned here. These tests
|
||||
assert it (a) still extracts well-formed action arrays and (b) returns
|
||||
promptly on the adversarial input that hung the old pattern.
|
||||
"""
|
||||
|
||||
import time
|
||||
|
||||
from routes.email_pollers import _CAL_ACTION_ARRAY_RE
|
||||
|
||||
|
||||
def _matches(s):
|
||||
return [m.group() for m in _CAL_ACTION_ARRAY_RE.finditer(s)]
|
||||
|
||||
|
||||
def test_extracts_action_array_from_prose():
|
||||
s = 'Here you go:\n[{"action":"add","title":"Standup","start":"2026-07-01T09:00"}]\nThanks!'
|
||||
assert _matches(s) == ['[{"action":"add","title":"Standup","start":"2026-07-01T09:00"}]']
|
||||
|
||||
|
||||
def test_extracts_multi_object_array():
|
||||
s = 'prose [{"action":"add","title":"A"},{"action":"cancel","uid":"x"}] tail'
|
||||
assert _matches(s) == ['[{"action":"add","title":"A"},{"action":"cancel","uid":"x"}]']
|
||||
|
||||
|
||||
def test_no_array_returns_no_match():
|
||||
assert _matches("no array here at all") == []
|
||||
|
||||
|
||||
def test_bracket_in_string_value_still_extracts():
|
||||
# The old `[^[\]]` class bailed on a '[' inside a value and matched nothing;
|
||||
# the linear `[^{}]` form correctly recovers the array.
|
||||
s = '[{"action":"add","title":"Meeting [urgent]","start":"x"}]'
|
||||
assert _matches(s) == [s]
|
||||
|
||||
|
||||
def test_adversarial_input_is_fast():
|
||||
evil = '[{"action"},{' + '}},{{' * 100_000 # exploded the old exponential pattern
|
||||
start = time.perf_counter()
|
||||
_CAL_ACTION_ARRAY_RE.search(evil)
|
||||
dt = time.perf_counter() - start
|
||||
assert dt < 1.0, f"_CAL_ACTION_ARRAY_RE took {dt:.2f}s on adversarial input"
|
||||
@@ -0,0 +1,29 @@
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def test_opencode_setup_provider_aliases_resolve():
|
||||
source = Path("static/js/slashCommands.js").read_text()
|
||||
match = re.search(
|
||||
r"const SETUP_PROVIDER_URLS = \{[\s\S]*?\nfunction _normalizeSetupBaseUrl",
|
||||
source,
|
||||
)
|
||||
assert match, "setup provider helper block not found"
|
||||
helper_source = match.group(0).removesuffix("\nfunction _normalizeSetupBaseUrl")
|
||||
script = helper_source + r"""
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
const zenFromCommand = _setupProviderFromInput('opencode zen');
|
||||
assert(zenFromCommand && zenFromCommand.url === 'https://opencode.ai/zen/v1', 'opencode zen command alias failed');
|
||||
const goFromCommand = _setupProviderFromInput('opencode-go');
|
||||
assert(goFromCommand && goFromCommand.url === 'https://opencode.ai/zen/go/v1', 'opencode-go command alias failed');
|
||||
const zenCredential = _extractSetupProviderCredential('opencode-zen sk-test');
|
||||
assert(zenCredential && zenCredential.provider.name === 'OpenCode Zen', 'opencode-zen credential provider failed');
|
||||
assert(zenCredential.credential === 'sk-test', 'opencode-zen credential extraction failed');
|
||||
const goCredential = _extractSetupProviderCredential('opencode go sk-test');
|
||||
assert(goCredential && goCredential.provider.name === 'OpenCode Go', 'opencode go credential provider failed');
|
||||
assert(goCredential.credential === 'sk-test', 'opencode go credential extraction failed');
|
||||
"""
|
||||
subprocess.run(["node", "-e", script], check=True)
|
||||
Reference in New Issue
Block a user