lampson 0.2.9 → 0.2.10
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/public/css/mobile.css +95 -0
- package/public/index.html +3 -1
- package/public/js/mobile.js +122 -0
- package/public/js/terminal.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lampson",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.10",
|
|
4
4
|
"description": "A coding agent written in Synsema: terminal + web, tools confined to the mounted project, plugins (your own tools, any language), LSP, MCP, sub-agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* mobile.css — la vista de celular (≤ 900 px), con js/mobile.js. Va ÚLTIMA en la cascada: solo pisa lo que cambia.
|
|
2
|
+
Barra inferior Chat · Sesiones · Archivos · Más; chips de agente/permisos/modelo sobre la caja de texto; paneles y
|
|
3
|
+
modales a pantalla completa; nada más ancho que la pantalla (el código y las tablas se deslizan dentro de su caja). */
|
|
4
|
+
@media (min-width:901px) { .mtabs, .mchips, .msheet, .mnew { display:none !important; } }
|
|
5
|
+
|
|
6
|
+
@media (max-width:900px) {
|
|
7
|
+
html, body { height:100%; overflow:hidden; }
|
|
8
|
+
body { height:100dvh; grid-template-columns:minmax(0,1fr); grid-template-rows:auto minmax(0,1fr) auto; grid-template-areas:"top" "main" "tabs"; }
|
|
9
|
+
|
|
10
|
+
/* header: una fila — dónde estás, ⚙ y tema; la segunda fila se reparte entre los chips y «Más» */
|
|
11
|
+
header { padding-top:env(safe-area-inset-top); }
|
|
12
|
+
header .hrow { height:48px; padding:0 8px 0 14px; gap:4px; }
|
|
13
|
+
header .hrow.sub { display:none; }
|
|
14
|
+
header #term, header #new { display:none; }
|
|
15
|
+
.brand { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; font-size:15px; }
|
|
16
|
+
header .spacer { display:none; }
|
|
17
|
+
.theme-toggle { padding:10px; }
|
|
18
|
+
|
|
19
|
+
/* paneles: uno por pestaña, a pantalla completa en el lugar del chat */
|
|
20
|
+
aside { display:none; }
|
|
21
|
+
body[data-mtab="side"] aside:not(.right), body[data-mtab="files"] aside.right { display:flex; grid-area:main; border:0; overflow:auto; }
|
|
22
|
+
body[data-mtab="side"] main, body[data-mtab="files"] main { display:none; }
|
|
23
|
+
aside .fold, aside .rail { display:none !important; }
|
|
24
|
+
aside.closed .sec { display:flex; }
|
|
25
|
+
aside .sec h2 { padding:15px 16px 12px; font-size:11.5px; }
|
|
26
|
+
aside.right h2, aside:not(.right) > .sec:first-of-type h2 { padding-right:16px; }
|
|
27
|
+
aside .sec .body { padding:0 12px 12px 16px; font-size:15px; }
|
|
28
|
+
aside .sec .body > * { min-height:34px; }
|
|
29
|
+
.mnew { flex:none; margin:12px 14px 4px; padding:12px; font:600 11.5px/1 var(--mono); text-transform:uppercase; letter-spacing:.09em; color:var(--accent); background:var(--accent-bg); border:1px solid var(--accent); border-radius:var(--r); cursor:pointer; }
|
|
30
|
+
|
|
31
|
+
/* chat: todo el ancho, nada más ancho que la pantalla */
|
|
32
|
+
#log { padding:14px 16px 18px; max-width:none; }
|
|
33
|
+
#log.hero { padding:18px 16px; align-items:flex-start; }
|
|
34
|
+
.user, .assistant, .step { overflow-wrap:anywhere; }
|
|
35
|
+
.assistant pre, .step pre { max-width:100%; }
|
|
36
|
+
.assistant table { display:block; max-width:100%; overflow-x:auto; }
|
|
37
|
+
.assistant img { max-width:100%; height:auto; }
|
|
38
|
+
.empty code { white-space:normal; }
|
|
39
|
+
.empty .grid { grid-template-columns:minmax(0,1fr); }
|
|
40
|
+
.approval .card { max-width:none; }
|
|
41
|
+
.approval .card button { min-height:40px; }
|
|
42
|
+
|
|
43
|
+
/* composer: chips arriba, texto de 16 px (iOS no hace zoom), botón grande para el dedo */
|
|
44
|
+
form { padding:8px 10px 10px; }
|
|
45
|
+
/* el texto y el botón en la misma fila (como en cualquier app de mensajes); las imágenes pegadas, arriba */
|
|
46
|
+
.composer { display:flex; flex-wrap:wrap; align-items:flex-end; gap:8px; padding:6px 6px 6px 12px; }
|
|
47
|
+
.composer .attach { flex-basis:100%; }
|
|
48
|
+
.composer textarea { flex:1; width:auto; min-width:0; min-height:0; font-size:16px; max-height:30vh; padding:7px 0; }
|
|
49
|
+
.crow { flex:none; margin:0; }
|
|
50
|
+
.crow .keys, .crow .tools { display:none; }
|
|
51
|
+
#send { width:44px; height:40px; font-size:17px; }
|
|
52
|
+
#stop { height:40px; }
|
|
53
|
+
/* chips: UNA fila que se desliza de costado (en 4 renglones le comían medio chat) */
|
|
54
|
+
.mchips { display:flex; flex-wrap:nowrap; gap:6px; margin:0 -10px 8px; padding:0 10px; overflow-x:auto; scrollbar-width:none; }
|
|
55
|
+
.mchips::-webkit-scrollbar { display:none; }
|
|
56
|
+
.mchips #wsPill { display:none !important; }
|
|
57
|
+
.mchips select, .mchips .pill { flex:none; height:30px; font:400 12px/1 var(--mono); color:var(--ink-2); background:var(--paper-2); border:1px solid var(--rule-2); border-radius:15px; padding:0 10px; }
|
|
58
|
+
.mchips select { padding-right:4px; }
|
|
59
|
+
.mchips .pill { display:inline-flex; align-items:center; max-width:62vw; }
|
|
60
|
+
.mchips .pill.on { color:var(--accent); }
|
|
61
|
+
.mchips .pill.nokey { color:var(--rubric); }
|
|
62
|
+
|
|
63
|
+
/* barra inferior */
|
|
64
|
+
.mtabs { grid-area:tabs; display:flex; background:var(--paper); border-top:1px solid var(--rule); padding-bottom:env(safe-area-inset-bottom); }
|
|
65
|
+
.mtabs button { flex:1; position:relative; display:flex; flex-direction:column; align-items:center; gap:3px; padding:9px 0 7px; background:none; border:0; color:var(--ink-3); font:400 10.5px/1 var(--mono); letter-spacing:.04em; cursor:pointer; -webkit-tap-highlight-color:transparent; }
|
|
66
|
+
.mtabs button.on { color:var(--accent); }
|
|
67
|
+
.mtabs .badge { position:absolute; top:4px; left:calc(50% + 8px); min-width:16px; height:16px; padding:0 4px; border-radius:8px; background:var(--rubric); color:#fff; font:600 10px/16px var(--mono); font-style:normal; }
|
|
68
|
+
|
|
69
|
+
/* «Más»: hoja desde abajo */
|
|
70
|
+
.msheet { position:fixed; inset:0; z-index:70; display:flex; flex-direction:column; justify-content:flex-end; }
|
|
71
|
+
.msheet .mback { position:absolute; inset:0; background:rgba(0,0,0,.45); }
|
|
72
|
+
.msheet .mcard { position:relative; background:var(--paper); border-top:1px solid var(--rule-2); border-radius:12px 12px 0 0; padding:10px 12px calc(12px + env(safe-area-inset-bottom)); display:flex; flex-direction:column; gap:4px; }
|
|
73
|
+
.msheet .mwhere { padding:8px 6px 10px; border-bottom:1px solid var(--rule); margin-bottom:4px; font:400 12px/1.5 var(--mono); color:var(--ink-3); overflow-wrap:anywhere; }
|
|
74
|
+
.msheet .mgit { color:var(--ink-2); margin-bottom:2px; }
|
|
75
|
+
.msheet button { text-align:left; padding:14px 8px; background:none; border:0; border-radius:var(--r); color:var(--ink); font:400 16px/1 var(--serif); text-transform:none; letter-spacing:0; cursor:pointer; }
|
|
76
|
+
.msheet button:active { background:var(--paper-2); }
|
|
77
|
+
.msheet .mcancel { text-align:center; color:var(--ink-3); font:400 12px/1 var(--mono); text-transform:uppercase; letter-spacing:.09em; margin-top:4px; border-top:1px solid var(--rule); border-radius:0; }
|
|
78
|
+
|
|
79
|
+
/* modales (configuración, sesiones, plugins…): pantalla completa; en «browse» la lista arriba y el detalle abajo */
|
|
80
|
+
.modal { align-items:stretch; justify-content:stretch; }
|
|
81
|
+
.panel, .panel.lg, .panel.md, .panel.sm { width:100vw; height:100dvh; max-height:none; border:0; border-radius:0; box-shadow:none; padding:calc(10px + env(safe-area-inset-top)) 14px calc(12px + env(safe-area-inset-bottom)); }
|
|
82
|
+
.panel .browse { grid-template-columns:minmax(0,1fr); grid-template-rows:minmax(0,38%) minmax(0,1fr); }
|
|
83
|
+
.panel .blist { border-right:0; border-bottom:1px solid var(--rule); padding:10px 0 8px; }
|
|
84
|
+
.panel .bdetail { padding:12px 2px 8px; }
|
|
85
|
+
.panel input, .panel select, .panel textarea { font-size:16px; }
|
|
86
|
+
.panel .pghost { width:36px; height:36px; flex:none; }
|
|
87
|
+
/* cabecera del modal: la ✕ siempre a la vista (la ruta de «sub» la empujaba fuera de la pantalla) */
|
|
88
|
+
.panel .phead { gap:8px; min-width:0; }
|
|
89
|
+
.panel .phead .sub { display:none; }
|
|
90
|
+
.panel .phead h3 { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
91
|
+
|
|
92
|
+
/* terminal y visor de archivos: el ancho de la pantalla */
|
|
93
|
+
#termpane .vh { overflow-x:auto; }
|
|
94
|
+
#viewer pre { font-size:12px; }
|
|
95
|
+
}
|
package/public/index.html
CHANGED
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
|
12
12
|
<link rel="manifest" href="/manifest.webmanifest">
|
|
13
13
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
|
14
|
-
<!-- estilos: tokens (fuentes, paleta, base) → layout (grid, cabecera, paneles, chat, composer) → sidebar → chat → panel (el modal) → code (editor de archivos + tema de highlight.js) -->
|
|
14
|
+
<!-- estilos: tokens (fuentes, paleta, base) → layout (grid, cabecera, paneles, chat, composer) → sidebar → chat → panel (el modal) → code (editor de archivos + tema de highlight.js) → mobile (la vista de celular, ≤ 900 px) -->
|
|
15
15
|
<link rel="stylesheet" href="/css/tokens.css">
|
|
16
16
|
<link rel="stylesheet" href="/css/layout.css">
|
|
17
17
|
<link rel="stylesheet" href="/css/sidebar.css">
|
|
18
18
|
<link rel="stylesheet" href="/css/chat.css">
|
|
19
19
|
<link rel="stylesheet" href="/css/panel.css">
|
|
20
20
|
<link rel="stylesheet" href="/css/code.css">
|
|
21
|
+
<link rel="stylesheet" href="/css/mobile.css">
|
|
21
22
|
<link rel="stylesheet" href="/vendor/xterm.css">
|
|
22
23
|
<script src="/vendor/xterm.js"></script>
|
|
23
24
|
<script src="/vendor/xterm-addon-fit.js"></script>
|
|
@@ -138,6 +139,7 @@
|
|
|
138
139
|
<script src="/js/sessions.js"></script>
|
|
139
140
|
<script src="/js/chat.js"></script>
|
|
140
141
|
<script src="/js/events.js"></script>
|
|
142
|
+
<script src="/js/mobile.js"></script>
|
|
141
143
|
<script src="/js/app.js"></script>
|
|
142
144
|
<script src="/js/pwa.js"></script>
|
|
143
145
|
</body>
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// mobile.js — la vista de celular (≤ 900 px; en escritorio no hace nada). Decisiones del usuario (2026-09-28):
|
|
2
|
+
// * barra inferior al alcance del pulgar: Chat · Sesiones (el panel izquierdo entero) · Archivos · Más
|
|
3
|
+
// * agente, permisos, modelo y plugins como chips sobre la caja de texto (hoy la fila del header rompía el ancho:
|
|
4
|
+
// la página medía 1060 px en una pantalla de 390)
|
|
5
|
+
// Los controles se MUEVEN (appendChild), no se copian: sus listeners siguen andando; al volver a escritorio vuelven
|
|
6
|
+
// a su lugar. Todo lo que se muestra en el centro pasa por showPane() (sesión, archivo, log, terminal): envuelto,
|
|
7
|
+
// en el celular te lleva solo a la pestaña Chat.
|
|
8
|
+
(function () {
|
|
9
|
+
const mq = window.matchMedia('(max-width: 900px)');
|
|
10
|
+
const ICON = {
|
|
11
|
+
chat: '<path d="M21 12a8 8 0 0 1-11.6 7.1L4 20l1-4.6A8 8 0 1 1 21 12z"/>',
|
|
12
|
+
side: '<path d="M4 6h16M4 12h16M4 18h10"/>',
|
|
13
|
+
files: '<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>',
|
|
14
|
+
more: '<circle cx="5" cy="12" r="1.4"/><circle cx="12" cy="12" r="1.4"/><circle cx="19" cy="12" r="1.4"/>',
|
|
15
|
+
};
|
|
16
|
+
const svg = (k) => `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">${ICON[k]}</svg>`;
|
|
17
|
+
let bar = null, chips = null, sheet = null, newBtn = null, moved = [], on = false, placeholder = null;
|
|
18
|
+
|
|
19
|
+
function setTab(t) {
|
|
20
|
+
document.body.dataset.mtab = t;
|
|
21
|
+
if (bar) bar.querySelectorAll('button').forEach(b => b.classList.toggle('on', b.dataset.t === t));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function build() {
|
|
25
|
+
bar = document.createElement('nav');
|
|
26
|
+
bar.className = 'mtabs';
|
|
27
|
+
bar.innerHTML = [['chat', 'Chat'], ['side', 'Sesiones'], ['files', 'Archivos'], ['more', 'Más']]
|
|
28
|
+
.map(([t, l]) => `<button type="button" data-t="${t}">${svg(t)}<span>${l}</span><i class="badge" hidden></i></button>`).join('');
|
|
29
|
+
bar.onclick = (e) => {
|
|
30
|
+
const b = e.target.closest('button'); if (!b) return;
|
|
31
|
+
if (b.dataset.t === 'more') openMore(); else setTab(b.dataset.t);
|
|
32
|
+
};
|
|
33
|
+
document.body.appendChild(bar);
|
|
34
|
+
chips = document.createElement('div');
|
|
35
|
+
chips.className = 'mchips';
|
|
36
|
+
// «+ nueva sesión» arriba de la lista de sesiones (en escritorio está en el header)
|
|
37
|
+
newBtn = document.createElement('button');
|
|
38
|
+
newBtn.type = 'button'; newBtn.className = 'mnew'; newBtn.textContent = '+ nueva sesión';
|
|
39
|
+
newBtn.onclick = () => { const n = document.getElementById('new'); if (n) n.click(); setTab('chat'); };
|
|
40
|
+
// aprobaciones pendientes: un número sobre «Sesiones» para no perderlas de vista
|
|
41
|
+
const cnt = document.getElementById('apprCount');
|
|
42
|
+
if (cnt) new MutationObserver(badge).observe(cnt, { childList: true, characterData: true, subtree: true });
|
|
43
|
+
// todo lo que se abre en el centro (sesión, archivo, log, terminal) vuelve a la pestaña Chat
|
|
44
|
+
if (typeof window.showPane === 'function') {
|
|
45
|
+
const orig = window.showPane;
|
|
46
|
+
window.showPane = function (which) { const r = orig.apply(this, arguments); if (on) setTab('chat'); return r; };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function badge() {
|
|
51
|
+
const b = bar && bar.querySelector('button[data-t="side"] .badge'); if (!b) return;
|
|
52
|
+
const n = parseInt((document.getElementById('apprCount') || {}).textContent || '0', 10);
|
|
53
|
+
b.hidden = !(n > 0); b.textContent = n > 0 ? String(n) : '';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// los controles del header que van como chips sobre la caja de texto
|
|
57
|
+
const CHIP_IDS = ['agent', 'perm', 'model', 'plugins', 'wsPill'];
|
|
58
|
+
|
|
59
|
+
function attach() {
|
|
60
|
+
if (!bar) build();
|
|
61
|
+
const form = document.getElementById('f');
|
|
62
|
+
if (form && chips.parentNode !== form) form.insertBefore(chips, form.firstChild);
|
|
63
|
+
moved = [];
|
|
64
|
+
for (const id of CHIP_IDS) {
|
|
65
|
+
const el = document.getElementById(id); if (!el) continue;
|
|
66
|
+
moved.push({ el, parent: el.parentNode, next: el.nextSibling });
|
|
67
|
+
chips.appendChild(el);
|
|
68
|
+
}
|
|
69
|
+
const aside = document.querySelector('aside:not(.right)');
|
|
70
|
+
if (aside && newBtn.parentNode !== aside) aside.insertBefore(newBtn, aside.firstChild);
|
|
71
|
+
const ta = document.getElementById('in');
|
|
72
|
+
if (ta) { placeholder = ta.placeholder; ta.placeholder = 'Escribí… · !comando lo corrés vos'; }
|
|
73
|
+
document.body.classList.add('mobile');
|
|
74
|
+
on = true;
|
|
75
|
+
setTab(document.body.dataset.mtab || 'chat');
|
|
76
|
+
badge();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function detach() {
|
|
80
|
+
for (const m of moved.reverse()) m.parent.insertBefore(m.el, m.next && m.next.parentNode === m.parent ? m.next : null);
|
|
81
|
+
moved = [];
|
|
82
|
+
if (newBtn && newBtn.parentNode) newBtn.parentNode.removeChild(newBtn);
|
|
83
|
+
const ta = document.getElementById('in');
|
|
84
|
+
if (ta && placeholder != null) ta.placeholder = placeholder;
|
|
85
|
+
closeMore();
|
|
86
|
+
document.body.classList.remove('mobile');
|
|
87
|
+
delete document.body.dataset.mtab;
|
|
88
|
+
on = false;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// «Más»: lo que en escritorio vive en el header (terminal, configuración, tema, workspaces) y dónde estás
|
|
92
|
+
function openMore() {
|
|
93
|
+
closeMore();
|
|
94
|
+
const click = (id) => () => { closeMore(); const el = document.getElementById(id); if (el) el.click(); };
|
|
95
|
+
const path = (document.getElementById('hpath') || {}).textContent || '';
|
|
96
|
+
const git = (document.getElementById('git') || {}).innerHTML || '';
|
|
97
|
+
const upd = document.getElementById('update');
|
|
98
|
+
sheet = document.createElement('div');
|
|
99
|
+
sheet.className = 'msheet';
|
|
100
|
+
sheet.innerHTML = `<div class="mback"></div><div class="mcard">
|
|
101
|
+
<div class="mwhere">${git ? `<div class="mgit">${git}</div>` : ''}<div class="mpath">${esc(path)}</div></div>
|
|
102
|
+
${upd && upd.style.display !== 'none' ? `<button type="button" data-a="update">${esc(upd.textContent || 'actualizar')}</button>` : ''}
|
|
103
|
+
<button type="button" data-a="term">>_ terminal</button>
|
|
104
|
+
<button type="button" data-a="cfg">⚙ configuración</button>
|
|
105
|
+
<button type="button" data-a="theme">◐ cambiar tema</button>
|
|
106
|
+
<button type="button" data-a="hub">⌂ workspaces</button>
|
|
107
|
+
<button type="button" data-a="close" class="mcancel">cerrar</button></div>`;
|
|
108
|
+
const acts = { update: click('update'), term: click('term'), cfg: click('cfgBtn'), theme: click('theme'), hub: () => { location.href = '/'; }, close: closeMore };
|
|
109
|
+
sheet.onclick = (e) => {
|
|
110
|
+
if (e.target.classList.contains('mback')) return closeMore();
|
|
111
|
+
const b = e.target.closest('button[data-a]'); if (b && acts[b.dataset.a]) acts[b.dataset.a]();
|
|
112
|
+
};
|
|
113
|
+
document.body.appendChild(sheet);
|
|
114
|
+
}
|
|
115
|
+
function closeMore() { if (sheet) { sheet.remove(); sheet = null; } }
|
|
116
|
+
function esc(s) { return String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); }
|
|
117
|
+
|
|
118
|
+
function apply() { if (mq.matches && !on) attach(); else if (!mq.matches && on) detach(); }
|
|
119
|
+
// después de que app.js armó el header (los selects se llenan al arrancar)
|
|
120
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', apply); else apply();
|
|
121
|
+
(mq.addEventListener ? mq.addEventListener('change', apply) : mq.addListener(apply));
|
|
122
|
+
})();
|
package/public/js/terminal.js
CHANGED
|
@@ -65,7 +65,7 @@ function newTerm(id, show) {
|
|
|
65
65
|
if (terms.length >= MAX_TERMS) return;
|
|
66
66
|
if (show !== false) { showPane('term'); procOpen = null; clearInterval(procTimer); }
|
|
67
67
|
const el = document.createElement('div'); el.className = 'xt'; $('#xterms').appendChild(el);
|
|
68
|
-
const term = new Terminal({ cursorBlink: true, fontFamily: getComputedStyle(document.documentElement).getPropertyValue('--mono'), fontSize: 13, lineHeight: 1
|
|
68
|
+
const term = new Terminal({ cursorBlink: true, fontFamily: getComputedStyle(document.documentElement).getPropertyValue('--mono'), fontSize: 13, lineHeight: 1, theme: termTheme(), scrollback: 5000, allowProposedApi: true });
|
|
69
69
|
const fit = new FitAddon.FitAddon(); term.loadAddon(fit);
|
|
70
70
|
const t = { id: id || '', el, term, fit, ws: null, ro: null, box: '', meta: 'conectando…', live: false };
|
|
71
71
|
terms.push(t);
|