@jossuealcala/madre 0.7.1 → 0.7.2
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/CHANGELOG.md +61 -1
- package/SECURITY.md +1 -0
- package/docs/REFERENCE.md +4 -1
- package/package.json +1 -1
- package/public/app.js +467 -65
- package/public/es.js +102 -5
- package/public/styles.css +126 -19
- package/src/adapters/claude.mjs +4 -3
- package/src/adapters/codex.mjs +4 -3
- package/src/choices.mjs +30 -6
- package/src/file-ops.mjs +19 -0
- package/src/models.mjs +25 -1
- package/src/modules/index.mjs +2 -1
- package/src/modules/ollama.mjs +5 -1
- package/src/modules/runs.mjs +34 -0
- package/src/ollama.mjs +19 -0
- package/src/room/control.mjs +18 -1
- package/src/room/prompt.mjs +16 -4
- package/src/room/runs.mjs +88 -0
- package/src/room.mjs +73 -11
- package/src/runs.mjs +119 -0
- package/src/server.mjs +55 -7
package/public/es.js
CHANGED
|
@@ -81,11 +81,17 @@ export const ES = {
|
|
|
81
81
|
'La sala ha arrastrado esto a {n} turno(s) y sigue sin respuesta.',
|
|
82
82
|
'The archivist recorded this as open and nothing has answered it.':
|
|
83
83
|
'El archivista lo registró como abierto y nada lo ha contestado.',
|
|
84
|
-
'
|
|
85
|
-
'
|
|
86
|
-
|
|
87
|
-
'
|
|
88
|
-
'
|
|
84
|
+
' asks for #{n} {label} to go on': ' pide #{n} {label} para seguir',
|
|
85
|
+
'You will type the project designation, as always. Then @{agent} gets your permission and continues.':
|
|
86
|
+
'Vas a escribir la designación del proyecto, como siempre. Después @{agent} recibe tu permiso y sigue.',
|
|
87
|
+
'@{agent} gets your permission for one message and continues.': '@{agent} recibe tu permiso para un mensaje y sigue.',
|
|
88
|
+
'GRANT #{n} AND CONTINUE': 'CONCEDER #{n} Y SEGUIR',
|
|
89
|
+
'WRITE IT MYSELF': 'ESCRIBIRLE YO',
|
|
90
|
+
'Arms #{n} in the box below, so you can add instructions before sending.':
|
|
91
|
+
'Arma #{n} en la caja de abajo, para que agregues instrucciones antes de mandar.',
|
|
92
|
+
'#{n} {label} granted · @{agent} continues': '#{n} {label} concedido · @{agent} sigue',
|
|
93
|
+
'#{n} {label} granted': '#{n} {label} concedido',
|
|
94
|
+
'Permission granted: #{n} {label}. Go ahead with what you proposed.': 'Permiso concedido: #{n} {label}. Sigue con lo que propusiste.',
|
|
89
95
|
'SERVERS': 'SERVIDORES',
|
|
90
96
|
'MADRE version': 'Versión de MADRE',
|
|
91
97
|
'Local servers answering on this computer': 'Servidores locales que contestan en esta computadora',
|
|
@@ -106,6 +112,22 @@ export const ES = {
|
|
|
106
112
|
'Move': 'Mover',
|
|
107
113
|
'Paste here': 'Pegar aquí',
|
|
108
114
|
'Paste beside it': 'Pegar al lado',
|
|
115
|
+
'Paste in the project root': 'Pegar en la raíz del proyecto',
|
|
116
|
+
'REPLACE FOR EVERY ROOM': 'REEMPLAZAR PARA TODAS LAS SALAS',
|
|
117
|
+
'REPLACE FOR THIS PROJECT': 'REEMPLAZAR PARA ESTE PROYECTO',
|
|
118
|
+
'{name} {version} is already installed for {where}; it keeps running until this file replaces it.':
|
|
119
|
+
'{name} {version} ya está instalado para {where}; sigue funcionando hasta que este archivo lo reemplace.',
|
|
120
|
+
'Right-click a folder, or an empty spot for the project root, to paste it there.':
|
|
121
|
+
'Clic derecho en una carpeta para pegarlo ahí, o en un espacio vacío para la raíz del proyecto.',
|
|
122
|
+
'in {path}': 'en {path}',
|
|
123
|
+
'the project root': 'la raíz del proyecto',
|
|
124
|
+
'copy': 'copia',
|
|
125
|
+
'That could not be done: {error}': 'Eso no se pudo hacer: {error}',
|
|
126
|
+
'A name is needed.': 'Hace falta un nombre.',
|
|
127
|
+
'That is not a name.': 'Eso no es un nombre.',
|
|
128
|
+
'Both a source and a destination are needed.': 'Hacen falta un origen y un destino.',
|
|
129
|
+
'The source and the destination are the same path.': 'El origen y el destino son la misma ruta.',
|
|
130
|
+
'A folder cannot be moved inside itself.': 'Una carpeta no se puede mover dentro de sí misma.',
|
|
109
131
|
'Rename': 'Renombrar',
|
|
110
132
|
'HOLD': 'BODEGA',
|
|
111
133
|
'BRIDGE': 'PUENTE',
|
|
@@ -962,6 +984,7 @@ export const ES = {
|
|
|
962
984
|
'MU/TH/UR › updating. The output is in the room, and the card refreshes when it finishes.':
|
|
963
985
|
'MU/TH/UR › actualizando. La salida está en la sala, y la ficha se refresca al terminar.',
|
|
964
986
|
'NOT NOW': 'AHORA NO',
|
|
987
|
+
'The room rejected the message.': 'La sala rechazó el mensaje.',
|
|
965
988
|
'ECONOMY · THIS ROOM': 'ECONOMÍA · ESTA SALA',
|
|
966
989
|
'NO TURNS WEIGHED YET · SEND A MESSAGE AND THIS FILLS': 'TODAVÍA NO HAY NINGÚN TURNO CON FACTURA · MANDA UN MENSAJE Y ESTO SE LLENA',
|
|
967
990
|
'ECONOMY UNAVAILABLE': 'ECONOMÍA NO DISPONIBLE',
|
|
@@ -1396,6 +1419,10 @@ export const ES = {
|
|
|
1396
1419
|
'Ollama is not on this computer yet.': 'Ollama todavía no está en esta computadora.',
|
|
1397
1420
|
'Ollama did not answer. Open the Ollama app, then press RECHECK.':
|
|
1398
1421
|
'Ollama no contestó. Abre la app de Ollama y dale REVISAR OTRA VEZ.',
|
|
1422
|
+
'Ollama {client} is installed, but {running} is still the one answering. Quit Ollama (or restart its service) and open it again, then RECHECK.':
|
|
1423
|
+
'Ollama {client} está instalado, pero el que sigue contestando es {running}. Cierra Ollama (o reinicia su servicio), ábrelo otra vez y dale REVISAR OTRA VEZ.',
|
|
1424
|
+
'The old Ollama ({running}) did not stop. Quit it by hand, then press START OLLAMA.':
|
|
1425
|
+
'El Ollama viejo ({running}) no se detuvo. Ciérralo a mano y dale ARRANCAR OLLAMA.',
|
|
1399
1426
|
'Installing a module writes into the project; send { "confirm": true } to proceed.':
|
|
1400
1427
|
'Instalar un módulo escribe en el proyecto; manda { "confirm": true } para seguir.',
|
|
1401
1428
|
'RIPLEY is off. Enable it in MODULES to render files.': 'RIPLEY está apagado. Enciéndelo en MÓDULOS para dibujar archivos.',
|
|
@@ -2423,6 +2450,7 @@ export const ES = {
|
|
|
2423
2450
|
'@{agent} sign-in did not complete': 'el inicio de sesión de @{agent} no se completó',
|
|
2424
2451
|
' · exit {code}': ' · salida {code}',
|
|
2425
2452
|
'signing in @{agent} · {command}': 'iniciando sesión de @{agent} · {command}',
|
|
2453
|
+
'@{agent}: open {url} to finish signing in': '@{agent}: abre {url} para terminar de iniciar sesión',
|
|
2426
2454
|
'installing {label} · {command}': 'instalando {label} · {command}',
|
|
2427
2455
|
'MU/TH/UR › {label} is on this computer. Sign in and the room opens.':
|
|
2428
2456
|
'MU/TH/UR › {label} ya está en esta computadora. Inicia sesión y la sala se abre.',
|
|
@@ -2595,6 +2623,75 @@ export const ES = {
|
|
|
2595
2623
|
"Conversation · {n} messages. The project's memory is shared by all of them.":
|
|
2596
2624
|
'Conversación · {n} mensajes. La memoria del proyecto es la misma para todos ellos.',
|
|
2597
2625
|
|
|
2626
|
+
// Esfuerzo: cuánto piensa el modelo antes de contestar.
|
|
2627
|
+
'effort': 'esfuerzo',
|
|
2628
|
+
'effort {level}': 'esfuerzo {level}',
|
|
2629
|
+
' · effort for this request': ' · esfuerzo de esta petición',
|
|
2630
|
+
'Effort for this agent · {level}': 'Esfuerzo de este agente · {level}',
|
|
2631
|
+
'Effort for this agent · the CLI default ({level})': 'Esfuerzo de este agente · el del CLI ({level})',
|
|
2632
|
+
"Effort for this agent · the CLI's own default": 'Esfuerzo de este agente · el que traiga el CLI',
|
|
2633
|
+
'Default': 'Por defecto',
|
|
2634
|
+
'Sends nothing: the CLI uses its own setting, {level} right now.': 'No manda nada: el CLI usa su propio ajuste, ahora {level}.',
|
|
2635
|
+
'Sends nothing: the CLI uses its own setting.': 'No manda nada: el CLI usa su propio ajuste.',
|
|
2636
|
+
'Minimal': 'Mínimo',
|
|
2637
|
+
'Low': 'Bajo',
|
|
2638
|
+
'Medium': 'Medio',
|
|
2639
|
+
'High': 'Alto',
|
|
2640
|
+
'Extra high': 'Muy alto',
|
|
2641
|
+
'Max': 'Máximo',
|
|
2642
|
+
'Answers almost at once. For lookups and one-line edits.': 'Contesta casi al instante. Para consultas y cambios de una línea.',
|
|
2643
|
+
'Quick and light. For clear, contained requests.': 'Rápido y ligero. Para peticiones claras y acotadas.',
|
|
2644
|
+
'The everyday balance of speed and depth.': 'El equilibrio de todos los días entre rapidez y profundidad.',
|
|
2645
|
+
'Thinks it through. For design calls and tricky bugs.': 'Lo piensa a fondo. Para decisiones de diseño y bugs difíciles.',
|
|
2646
|
+
'Longer reasoning for hard, multi-step problems.': 'Razonamiento más largo para problemas difíciles de varios pasos.',
|
|
2647
|
+
'Everything it has. The slowest and the most expensive.': 'Todo lo que tiene. El más lento y el más caro.',
|
|
2648
|
+
'More effort is a slower, longer answer, and on a metered plan a more expensive one. It applies to every message to this agent until you change it.':
|
|
2649
|
+
'Más esfuerzo es una respuesta más lenta y más larga, y en un plan medido, más cara. Vale para cada mensaje a este agente hasta que lo cambies.',
|
|
2650
|
+
|
|
2651
|
+
// Una elección que devuelve un agente.
|
|
2652
|
+
'@{agent} asks you to choose': '@{agent} te pide elegir',
|
|
2653
|
+
'RECOMMENDED': 'RECOMENDADA',
|
|
2654
|
+
'CLI': 'CLI',
|
|
2655
|
+
|
|
2656
|
+
// Un comando que un agente te pide correr.
|
|
2657
|
+
'on · agents may ask you to run commands': 'encendido · los agentes pueden pedirte que corras comandos',
|
|
2658
|
+
'off · agents describe the command in prose': 'apagado · los agentes describen el comando en prosa',
|
|
2659
|
+
'Lets an agent ask you to run a command it cannot run in its mode (tests, a build, a linter). Each command is a button: nothing runs until you press it, it runs from the project root with no shell, a checkpoint is taken first, and the output comes back to the room. Costs about 190 tokens of instruction per turn below AIRLOCK while it is on.':
|
|
2660
|
+
'Deja que un agente te pida correr un comando que en su modo no puede correr (las pruebas, un build, un linter). Cada comando es un botón: nada corre hasta que lo presionas, corre desde la raíz del proyecto sin shell, antes se toma un checkpoint y la salida vuelve a la sala. Cuesta unos 190 tokens de instrucción por turno debajo de AIRLOCK mientras está encendido.',
|
|
2661
|
+
'Name the request and the line to run.': 'Di qué petición y qué línea correr.',
|
|
2662
|
+
'@{agent} asks you to run': '@{agent} te pide correr',
|
|
2663
|
+
'RUNNING': 'CORRIENDO',
|
|
2664
|
+
'WILL NOT RUN': 'NO CORRE',
|
|
2665
|
+
'This line does not run: it {why}.': 'Esta línea no corre: {why}.',
|
|
2666
|
+
'Does not run: it {why}.': 'No corre: {why}.',
|
|
2667
|
+
'Runs {command} from the project root, with no shell. A checkpoint is taken first.':
|
|
2668
|
+
'Corre {command} desde la raíz del proyecto, sin shell. Antes se toma un checkpoint.',
|
|
2669
|
+
'Each line runs only when you press it · from the project root, no shell · the output comes back to the room':
|
|
2670
|
+
'Cada línea corre solo cuando la presionas · desde la raíz del proyecto, sin shell · la salida vuelve a la sala',
|
|
2671
|
+
'Put the project files back exactly as they were before this command. What it sent off this machine does not come back.':
|
|
2672
|
+
'Deja los archivos del proyecto exactamente como estaban antes de este comando. Lo que haya mandado fuera de esta máquina no regresa.',
|
|
2673
|
+
'project restored to the checkpoint before the command · {restored} restored · {removed} removed':
|
|
2674
|
+
'proyecto restaurado al checkpoint anterior al comando · {restored} restaurados · {removed} quitados',
|
|
2675
|
+
'expands a variable or a command inside quotes; there is no shell to expand it':
|
|
2676
|
+
'expande una variable o un comando dentro de comillas; no hay shell que lo expanda',
|
|
2677
|
+
'ends in a lone backslash': 'termina en una diagonal invertida suelta',
|
|
2678
|
+
'uses "{char}", which needs a shell; put each command on its own line':
|
|
2679
|
+
'usa «{char}», que necesita un shell; pon cada comando en su propia línea',
|
|
2680
|
+
'expands a variable or a command; there is no shell to expand it':
|
|
2681
|
+
'expande una variable o un comando; no hay shell que lo expanda',
|
|
2682
|
+
'uses "{char}" as a glob; there is no shell to expand it, name the files':
|
|
2683
|
+
'usa «{char}» como comodín; no hay shell que lo expanda, nombra los archivos',
|
|
2684
|
+
'starts a word with "~"; there is no shell to expand it, write the path':
|
|
2685
|
+
'empieza una palabra con «~»; no hay shell que lo expanda, escribe la ruta',
|
|
2686
|
+
'carries a "#" comment, which only a shell would drop; say it in the reason':
|
|
2687
|
+
'lleva un comentario con «#», que solo un shell quitaría; dilo en el motivo',
|
|
2688
|
+
'leaves a quote open': 'deja una comilla abierta',
|
|
2689
|
+
'is empty': 'está vacía',
|
|
2690
|
+
'"{name}" is a shell builtin; every line already runs from the project root':
|
|
2691
|
+
'«{name}» es un comando interno del shell; cada línea ya corre desde la raíz del proyecto',
|
|
2692
|
+
'sets an environment variable, which needs a shell': 'fija una variable de entorno, y eso necesita un shell',
|
|
2693
|
+
'is too long to read before pressing it': 'es demasiado larga para leerla antes de presionarla',
|
|
2694
|
+
|
|
2598
2695
|
// El interruptor de idioma.
|
|
2599
2696
|
'Interface in English': 'Interfaz en inglés',
|
|
2600
2697
|
'Interface in Spanish': 'Interfaz en español',
|
package/public/styles.css
CHANGED
|
@@ -135,6 +135,10 @@ body {
|
|
|
135
135
|
height: 100dvh;
|
|
136
136
|
width: 100%;
|
|
137
137
|
display: grid;
|
|
138
|
+
/* A track that can shrink. Without it the one implicit column is as wide as its widest child's
|
|
139
|
+
content, and on a phone the composer's chip row made the whole page 580px wide — the bar on
|
|
140
|
+
top, the thread and every bubble with it. */
|
|
141
|
+
grid-template-columns: minmax(0, 1fr);
|
|
138
142
|
grid-template-rows: auto 1fr auto auto auto;
|
|
139
143
|
}
|
|
140
144
|
.bar, .onboarding, .composer, .safety, .thread-inner { width: var(--column); margin-left: auto; margin-right: auto; }
|
|
@@ -736,7 +740,7 @@ pre.core-egress-log { margin: 0; max-height: 34vh; overflow: auto; padding: 10px
|
|
|
736
740
|
.picker .pick { --size: 30px; cursor: pointer; border: 1px solid transparent; background: color-mix(in srgb, var(--agent) 10%, transparent); transition: transform .12s ease, box-shadow .12s ease; }
|
|
737
741
|
.picker .pick:hover { transform: translateY(-1px); }
|
|
738
742
|
.picker .pick[aria-checked="true"] { box-shadow: 0 0 0 2px var(--bg), 0 0 0 3.5px var(--agent); background: color-mix(in srgb, var(--agent) 22%, transparent); }
|
|
739
|
-
.picker .pick-label { display: flex; align-items: center; gap:
|
|
743
|
+
.picker .pick-label { display: flex; align-items: center; gap: 10px; min-width: 0; font-family: var(--mono); font-size: 10.5px; letter-spacing: .1em; text-transform: uppercase; color: var(--text-2); margin-left: 2px; }
|
|
740
744
|
.picker .pick-label > .mode-chip, .picker .pick-label > .model-chip { flex: 0 0 auto; }
|
|
741
745
|
.picker .pick-label b { color: var(--agent, var(--text)); font-weight: 600; }
|
|
742
746
|
.field { position: relative; display: grid; grid-template-columns: auto 1fr auto; gap: 10px; align-items: end; padding: 6px 6px 6px 18px; border: 1px solid var(--line-2); background: var(--panel); border-radius: 24px; }
|
|
@@ -768,12 +772,17 @@ textarea::placeholder { color: var(--text-3); }
|
|
|
768
772
|
.shell.with-browser .colophon { display: none; }
|
|
769
773
|
|
|
770
774
|
/* toast */
|
|
771
|
-
.toast { position: fixed; left: var(--composer-center, 50%); bottom: calc(100vh - var(--composer-top, 70vh) + 22px); transform: translateX(-50%); text-align: center; padding: 9px 14px; animation: toast-in .22s ease; z-index: 40; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent); background: var(--panel); color: var(--danger); font-size: 13px; box-shadow: var(--shadow); max-width: min(560px, calc(100% - 32px)); }
|
|
775
|
+
.toast { position: fixed; left: var(--composer-center, 50%); bottom: calc(100vh - var(--composer-top, 70vh) + 22px); transform: translateX(-50%); text-align: center; padding: 9px 14px; animation: toast-in .22s ease; z-index: 40; border-radius: 12px; border: 1px solid color-mix(in srgb, var(--danger) 40%, transparent); background: var(--panel); color: var(--danger); font-size: 13px; box-shadow: var(--shadow); max-width: min(560px, calc(100% - 32px)); overflow-wrap: anywhere; }
|
|
772
776
|
|
|
773
777
|
@keyframes toast-in { from { opacity: 0; transform: translate(-50%, 6px); } to { opacity: 1; transform: translate(-50%, 0); } }
|
|
774
778
|
|
|
775
779
|
@media (max-width: 680px) {
|
|
776
780
|
.bar { flex-wrap: wrap; gap: 10px; }
|
|
781
|
+
/* The spheres keep their row; who the message goes to and its chips take the one below, and
|
|
782
|
+
wrap among themselves. On one line they were what made the page wider than the phone. */
|
|
783
|
+
.picker { flex-wrap: wrap; row-gap: 10px; }
|
|
784
|
+
.picker .pick-label { flex: 1 0 100%; flex-wrap: wrap; row-gap: 8px; margin-left: 0; }
|
|
785
|
+
.picker .pick-label > .mode-chip, .picker .pick-label > .model-chip, .picker .pick-label > .effort-chip { margin-left: 0; }
|
|
777
786
|
.project { display: none; }
|
|
778
787
|
.row { max-width: 92%; }
|
|
779
788
|
.who .reply { max-width: 150px; }
|
|
@@ -1093,10 +1102,43 @@ dialog.mother::backdrop { background: rgba(0, 0, 0, .82); backdrop-filter: blur(
|
|
|
1093
1102
|
.agent-pop .foot { margin-top: 10px; font-size: 9.5px; color: var(--text-3); letter-spacing: .1em; text-transform: uppercase; }
|
|
1094
1103
|
|
|
1095
1104
|
/* model chip & menu in the composer */
|
|
1096
|
-
.picker .model-chip { margin-left:
|
|
1105
|
+
.picker .model-chip { margin-left: 4px; border: 1px solid var(--line-2); background: transparent; color: var(--text-2); font: inherit; font-family: var(--mono); font-size: 10px; letter-spacing: .08em; text-transform: none; padding: 2px 8px; border-radius: 999px; cursor: pointer; }
|
|
1097
1106
|
.picker .model-chip:hover { color: var(--agent, var(--text)); border-color: var(--agent, var(--line-2)); }
|
|
1107
|
+
/* Effort chip: a signal meter in the agent's colour, beside the model. Unset, it is a ghost of
|
|
1108
|
+
the CLI's own level; set, it fills. No glow at any level: the meter already says how high, and
|
|
1109
|
+
light that bleeds says nothing the bars do not. */
|
|
1110
|
+
.picker .pick-label > .effort-chip { flex: 0 0 auto; }
|
|
1111
|
+
.picker .effort-chip { --tone: var(--agent, var(--phosphor)); display: inline-flex; align-items: center; gap: 8px; height: 22px; border: 1px solid var(--line-2); background: transparent; color: var(--text-2); font: inherit; font-family: var(--mono); font-size: 10px; letter-spacing: .08em; text-transform: none; padding: 0 10px 0 9px; border-radius: 999px; cursor: pointer; transition: color .2s, border-color .2s, background .2s; }
|
|
1112
|
+
.picker .effort-chip:hover { color: var(--text); border-color: color-mix(in srgb, var(--tone) 45%, var(--line-2)); }
|
|
1113
|
+
.picker .effort-chip.set { color: var(--text); border-color: color-mix(in srgb, var(--tone) 38%, var(--line-2)); background: color-mix(in srgb, var(--tone) 7%, transparent); }
|
|
1114
|
+
.picker .effort-chip .caret { color: var(--text-3); font-size: 9px; }
|
|
1115
|
+
.effort-meter { --tone: var(--agent, var(--phosphor)); display: inline-flex; align-items: flex-end; gap: 2px; height: 10px; }
|
|
1116
|
+
.effort-meter i { width: 3px; border-radius: 1px; background: color-mix(in srgb, var(--text-3) 45%, transparent); transition: background .2s; }
|
|
1117
|
+
.effort-meter i:nth-child(1) { height: 30%; } .effort-meter i:nth-child(2) { height: 47%; } .effort-meter i:nth-child(3) { height: 64%; } .effort-meter i:nth-child(4) { height: 82%; } .effort-meter i:nth-child(5) { height: 100%; }
|
|
1118
|
+
.effort-meter i.on { background: var(--tone); }
|
|
1119
|
+
.effort-meter.ghost i.on { background: color-mix(in srgb, var(--tone) 45%, var(--text-3)); }
|
|
1120
|
+
|
|
1121
|
+
.effort-menu { width: min(368px, calc(100vw - 24px)); padding: 8px; border-radius: 16px; box-shadow: 0 1px 0 rgba(255,255,255,.04) inset, 0 24px 60px -18px rgba(0,0,0,.6), 0 2px 8px rgba(0,0,0,.25); }
|
|
1122
|
+
.effort-menu .model-menu-title { display: flex; align-items: baseline; gap: 6px; margin: 8px 12px 10px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
1123
|
+
.effort-ladder { display: grid; gap: 4px; }
|
|
1124
|
+
.effort-option { --tone: var(--agent, var(--phosphor)); display: grid; grid-template-columns: 28px minmax(0, 1fr) auto; gap: 14px; align-items: center; width: 100%; text-align: left; border: 1px solid transparent; background: transparent; color: var(--text); font: inherit; font-family: var(--mono); padding: 11px 12px; border-radius: 11px; cursor: pointer; transition: background .15s, border-color .15s; }
|
|
1125
|
+
.effort-option:hover, .effort-option:focus-visible { background: var(--panel-2, color-mix(in srgb, var(--text) 4%, transparent)); outline: 0; }
|
|
1126
|
+
.effort-option:focus-visible { border-color: var(--line-2); }
|
|
1127
|
+
.effort-option.auto { margin-bottom: 4px; }
|
|
1128
|
+
.effort-option .effort-meter { height: 14px; gap: 2.5px; justify-self: center; }
|
|
1129
|
+
.effort-option .effort-meter i { width: 3.5px; }
|
|
1130
|
+
.effort-option .body { display: grid; gap: 4px; min-width: 0; }
|
|
1131
|
+
.effort-option .name { font-size: 10.5px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; color: var(--text); }
|
|
1132
|
+
.effort-option .hint { font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", Inter, "Segoe UI", sans-serif; font-size: 11.5px; line-height: 1.45; letter-spacing: 0; color: var(--text-2); }
|
|
1133
|
+
.effort-option.auto .name { color: var(--text-2); }
|
|
1134
|
+
.effort-option.current { background: color-mix(in srgb, var(--tone) 8%, var(--panel)); border-color: color-mix(in srgb, var(--tone) 28%, var(--line-2)); }
|
|
1135
|
+
.effort-option .tag { display: inline-flex; align-items: center; gap: 6px; font-size: 8.5px; font-weight: 600; letter-spacing: .16em; color: var(--text-3); white-space: nowrap; }
|
|
1136
|
+
.effort-option.current .tag { color: var(--tone); }
|
|
1137
|
+
.effort-option .tag .dot { width: 5px; height: 5px; border-radius: 50%; background: var(--tone); }
|
|
1138
|
+
.effort-menu .model-note { margin: 10px 12px 6px; padding-top: 10px; border-top: 1px solid var(--line); font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", Inter, "Segoe UI", sans-serif; font-size: 11px; letter-spacing: 0; line-height: 1.5; }
|
|
1139
|
+
.who .badge.effort { text-transform: none; }
|
|
1098
1140
|
/* Permission mode chip: TO @agent #1 EXCHANGE ▾ model ▾ */
|
|
1099
|
-
.picker .mode-chip { margin-left:
|
|
1141
|
+
.picker .mode-chip { margin-left: 4px; border: 1px solid var(--mode); background: transparent; color: var(--mode); font: inherit; font-family: var(--mono); font-size: 10px; letter-spacing: .1em; padding: 2px 9px; border-radius: 999px; cursor: pointer; display: inline-flex; align-items: center; gap: 6px; transition: background .2s, color .2s, border-color .2s; }
|
|
1100
1142
|
.picker .mode-chip b { font-weight: 700; color: inherit; }
|
|
1101
1143
|
.picker .mode-chip:hover { background: color-mix(in srgb, var(--mode) 12%, transparent); }
|
|
1102
1144
|
.picker .mode-chip.m0 { --mode: var(--mode-0); }
|
|
@@ -1326,7 +1368,7 @@ dialog.viewer { border: 0; padding: 0; width: min(1100px, 96vw); max-width: 96vw
|
|
|
1326
1368
|
.dock-grip::after { content: ''; position: absolute; left: 3px; top: 50%; transform: translateY(-50%); width: 2px; height: 48px; border-radius: 999px; background: var(--line-2); opacity: 0; transition: opacity .15s; }
|
|
1327
1369
|
.dock-grip:hover::after, .shell.dragging-dock .dock-grip::after { opacity: 1; background: var(--phosphor); }
|
|
1328
1370
|
.shell.dragging-dock .browser-dock iframe { pointer-events: none; }
|
|
1329
|
-
@media (max-width: 900px) { .shell.with-browser { grid-template-columns: 1fr; } .shell.with-browser > .browser-dock { grid-column: 1; grid-row: 3 / 4; height: auto; border-left: 0; border-top: 1px solid var(--line); } }
|
|
1371
|
+
@media (max-width: 900px) { .shell.with-browser { grid-template-columns: minmax(0, 1fr); } .shell.with-browser > .browser-dock { grid-column: 1; grid-row: 3 / 4; height: auto; border-left: 0; border-top: 1px solid var(--line); } }
|
|
1330
1372
|
|
|
1331
1373
|
/* The browser is its own window on the rail, beside the conversations. */
|
|
1332
1374
|
|
|
@@ -1724,8 +1766,18 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1724
1766
|
and a tinted ground behind a sentence that is not clickable — so the eye read a panel and had
|
|
1725
1767
|
to find the button inside it. The sentence is a sentence now; only the thing you can press
|
|
1726
1768
|
looks pressable, and it looks it properly: filled, not outlined. */
|
|
1727
|
-
|
|
1728
|
-
|
|
1769
|
+
/* The way up is a card under the reply, shaped like a decision handed back (.choice): what is
|
|
1770
|
+
asked, what pressing will do, and the answers together under it — not one button alone at the
|
|
1771
|
+
far edge of a line, where it read as part of the reply's stamp. */
|
|
1772
|
+
.mode-ask { display: grid; gap: 6px; margin: 10px 12px 2px; padding: 12px 14px; border: 1px solid color-mix(in srgb, var(--warn, #e2a022) 45%, transparent); border-radius: 14px; background: color-mix(in srgb, var(--warn, #e2a022) 6%, transparent); }
|
|
1773
|
+
.mode-what { min-width: 0; font-size: 13px; line-height: 1.45; color: var(--text); }
|
|
1774
|
+
.mode-what .who { display: inline; margin: 0; padding: 0; font: inherit; font-weight: 700; letter-spacing: 0; text-transform: none; color: var(--agent, var(--text)); }
|
|
1775
|
+
.mode-why { font-size: 11.5px; line-height: 1.45; color: var(--text-2); }
|
|
1776
|
+
.mode-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin-top: 4px; }
|
|
1777
|
+
.mode-alt { border: 1px solid var(--line-2); border-radius: 7px; background: transparent; color: var(--text-2); font-family: var(--mono); font-size: 10px; letter-spacing: .1em; padding: 6px 11px; cursor: pointer; }
|
|
1778
|
+
.mode-alt:hover { color: var(--text); border-color: var(--text-3); }
|
|
1779
|
+
.mode-ask.settled { padding: 8px 14px; border-style: dashed; background: none; }
|
|
1780
|
+
.mode-ask.settled .mode-what { font-family: var(--mono); font-size: 10.5px; letter-spacing: .06em; color: var(--text-3); }
|
|
1729
1781
|
.mode-go {
|
|
1730
1782
|
flex: none; border: 0; border-radius: 7px; cursor: pointer;
|
|
1731
1783
|
background: var(--warn, #e2a022); color: var(--warn-ink, #1a1403);
|
|
@@ -1735,12 +1787,47 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1735
1787
|
.mode-go:hover { background: color-mix(in srgb, var(--warn, #e2a022) 86%, #fff); }
|
|
1736
1788
|
.mode-go:active { transform: translateY(1px); box-shadow: none; }
|
|
1737
1789
|
|
|
1738
|
-
/* A decision handed back, in the agent's colour: read at a glance, answered with one press.
|
|
1739
|
-
|
|
1740
|
-
.
|
|
1741
|
-
|
|
1742
|
-
.choice
|
|
1743
|
-
.choice-
|
|
1790
|
+
/* A decision handed back, in the agent's colour: read at a glance, answered with one press.
|
|
1791
|
+
Options stack as rows, not pills: an option is a sentence, and a sentence wrapped inside a pill
|
|
1792
|
+
reads as a broken button. The agent's pick leads, with its tag on the right. The colour is in
|
|
1793
|
+
the edges and the numbers, never in a glow. */
|
|
1794
|
+
.choice { margin: 14px 12px 4px; padding: 18px 18px 14px; border: 1px solid color-mix(in srgb, var(--agent, var(--text-3)) 22%, var(--line-2)); border-radius: 18px; background: color-mix(in srgb, var(--agent, var(--text-3)) 3%, var(--bg-2)); box-shadow: 0 1px 0 rgba(255,255,255,.03) inset; max-width: 760px; }
|
|
1795
|
+
.choice-head { display: flex; align-items: center; gap: 8px; margin: 0 2px 8px; }
|
|
1796
|
+
.choice-kicker { font-family: var(--mono); font-size: 9.5px; font-weight: 600; letter-spacing: .18em; text-transform: uppercase; color: var(--agent, var(--text-3)); }
|
|
1797
|
+
.choice-q { margin: 0 2px 16px; font-size: 14px; line-height: 1.5; font-weight: 500; color: var(--text); text-wrap: pretty; }
|
|
1798
|
+
.choice-list { display: grid; gap: 8px; }
|
|
1799
|
+
.choice-option { display: grid; grid-template-columns: 24px minmax(0, 1fr) auto; align-items: start; gap: 14px; width: 100%; border: 1px solid var(--line-2); background: var(--panel); color: var(--text); font: inherit; font-size: 13px; line-height: 1.55; text-align: left; padding: 12px 14px; border-radius: 12px; cursor: pointer; transition: background .15s, border-color .15s, transform .08s; }
|
|
1800
|
+
.choice-option:hover { background: var(--panel-2, var(--panel)); border-color: color-mix(in srgb, var(--agent, var(--text-3)) 40%, var(--line-2)); }
|
|
1801
|
+
.choice-option:active { transform: translateY(1px); }
|
|
1802
|
+
.choice-option:focus-visible { outline: 2px solid color-mix(in srgb, var(--agent, var(--phosphor)) 70%, transparent); outline-offset: 2px; }
|
|
1803
|
+
.choice-key { display: grid; place-items: center; width: 24px; height: 24px; margin-top: -1px; border: 1px solid var(--line-2); border-radius: 7px; font-family: var(--mono); font-size: 10.5px; font-weight: 600; color: var(--text-3); transition: color .15s, border-color .15s, background .15s; }
|
|
1804
|
+
.choice-option:hover .choice-key { color: var(--text); }
|
|
1805
|
+
.choice-text { min-width: 0; overflow-wrap: anywhere; text-wrap: pretty; }
|
|
1806
|
+
.choice-tag { align-self: center; padding: 4px 9px; border: 1px solid color-mix(in srgb, var(--agent, var(--phosphor)) 45%, transparent); border-radius: 999px; color: var(--agent, var(--phosphor)); font-family: var(--mono); font-size: 8.5px; font-weight: 600; letter-spacing: .16em; white-space: nowrap; }
|
|
1807
|
+
.choice-option.recommended { border-color: color-mix(in srgb, var(--agent, var(--phosphor)) 40%, var(--line-2)); background: color-mix(in srgb, var(--agent, var(--phosphor)) 6%, var(--panel)); }
|
|
1808
|
+
.choice-option.recommended .choice-key { border-color: color-mix(in srgb, var(--agent, var(--phosphor)) 50%, transparent); color: var(--agent, var(--phosphor)); }
|
|
1809
|
+
.choice-option.picked { border-color: color-mix(in srgb, var(--agent, var(--phosphor)) 70%, transparent); background: color-mix(in srgb, var(--agent, var(--phosphor)) 10%, var(--panel)); }
|
|
1810
|
+
.choice-option.picked .choice-key { background: var(--agent, var(--phosphor)); border-color: transparent; color: var(--bg); }
|
|
1811
|
+
.choice-foot { margin: 14px 2px 0; font-family: var(--mono); font-size: 9.5px; letter-spacing: .08em; color: var(--text-3); }
|
|
1812
|
+
@media (max-width: 560px) {
|
|
1813
|
+
.choice { margin: 12px 0 4px; padding: 14px 12px 12px; }
|
|
1814
|
+
.choice-option { grid-template-columns: 24px minmax(0, 1fr); gap: 12px; }
|
|
1815
|
+
.choice-tag { grid-column: 2; justify-self: start; }
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/* A command an agent asks the human to run: a .choice whose options are command lines. The line
|
|
1819
|
+
is code, read before it is pressed; the tag says what pressing does, then what happened. */
|
|
1820
|
+
.run-request .choice-text { font-family: var(--mono); font-size: 12px; }
|
|
1821
|
+
.run-request .choice-key { font-family: var(--mono); }
|
|
1822
|
+
.run-request .choice-tag { background: transparent; border: 1px solid var(--agent, var(--phosphor)); color: var(--agent, var(--phosphor)); }
|
|
1823
|
+
.run-request .run-line.running { cursor: progress; border-color: var(--agent, var(--phosphor)); }
|
|
1824
|
+
.run-request .run-line.running .choice-tag { animation: klaxon 1.2s ease-in-out infinite; }
|
|
1825
|
+
@media (prefers-reduced-motion: reduce) { .run-request .run-line.running .choice-tag { animation: none; } }
|
|
1826
|
+
.run-request .run-line.ran .choice-tag { background: var(--agent, var(--phosphor)); color: var(--bg); }
|
|
1827
|
+
.run-request .run-line.failed .choice-tag { border-color: var(--crit); color: var(--crit); }
|
|
1828
|
+
.run-request .run-line.refused { cursor: not-allowed; opacity: .6; }
|
|
1829
|
+
.run-request .run-line.refused .choice-tag { border-color: var(--text-3); color: var(--text-3); }
|
|
1830
|
+
.run-request .run-why { margin: -2px 2px 4px 34px; font-size: 11.5px; line-height: 1.45; color: var(--text-3); }
|
|
1744
1831
|
|
|
1745
1832
|
/* What the room still has no answer to. Quieter than the free-turn offer above it: that one is
|
|
1746
1833
|
about to save money, this one is only wondering. */
|
|
@@ -1806,16 +1893,30 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1806
1893
|
.tree { position: relative; grid-row: 3; justify-self: end; align-self: stretch; z-index: 5; width: clamp(240px, calc((100% - var(--column)) / 2 - 36px), 420px); margin: 18px 20px 10px 0; min-height: 0; display: grid; grid-template-rows: auto minmax(0, 1fr); border: 1px solid var(--line-2); border-radius: 18px; background: var(--panel); box-shadow: 0 10px 30px rgba(0, 0, 0, .06); overflow: hidden; animation: tree-in .22s ease; }
|
|
1807
1894
|
/* The finder's own row of tools: a search, and what is waiting to be pasted. */
|
|
1808
1895
|
.tree { grid-template-rows: auto auto auto minmax(0, 1fr); grid-template-columns: minmax(0, 1fr); }
|
|
1896
|
+
/* The files panel reads paths and asks for names; at its old 240px floor neither fit. */
|
|
1897
|
+
#tree.tree { width: clamp(300px, calc((100% - var(--column)) / 2 - 36px), 460px); }
|
|
1809
1898
|
.tree-search { padding: 8px 12px 6px; }
|
|
1810
1899
|
.tree-search input { width: 100%; border: 1px solid var(--line-2); border-radius: 8px; background: color-mix(in srgb, var(--text-3) 6%, transparent); color: var(--text); font-family: var(--mono); font-size: 11px; padding: 6px 10px; outline: 0; }
|
|
1811
1900
|
.tree-search input:focus { border-color: var(--phosphor); }
|
|
1812
|
-
.tree-clip { display: flex; min-width: 0; align-items: center; gap: 8px; margin: 0 12px 6px; padding:
|
|
1901
|
+
.tree-clip { display: flex; flex-wrap: wrap; min-width: 0; align-items: center; gap: 6px 8px; margin: 0 12px 6px; padding: 8px 10px; border: 1px dashed var(--phosphor); border-radius: 8px; font-family: var(--mono); font-size: 11px; color: var(--text-2); }
|
|
1813
1902
|
.tree-clip .what { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1814
|
-
.tree-clip
|
|
1815
|
-
.tree-
|
|
1816
|
-
.tree-
|
|
1817
|
-
|
|
1818
|
-
|
|
1903
|
+
.tree-clip .how { flex: 1 1 100%; font-size: 10px; line-height: 1.45; color: var(--text-3); }
|
|
1904
|
+
.tree-clip button { border: 0; border-radius: 6px; background: transparent; color: var(--phosphor); font: inherit; font-size: 11px; letter-spacing: .08em; padding: 4px 8px; cursor: pointer; }
|
|
1905
|
+
.tree-clip button:hover { background: color-mix(in srgb, var(--phosphor) 12%, transparent); }
|
|
1906
|
+
/* What is being asked is its own line, and the box has the full width under it: a placeholder in
|
|
1907
|
+
a strip three words wide was cut off before anyone could read it. */
|
|
1908
|
+
.tree-ask { display: grid; gap: 6px; flex: 1 1 100%; min-width: 0; }
|
|
1909
|
+
.tree-ask .ask-label { font-size: 11px; letter-spacing: .1em; color: var(--text); }
|
|
1910
|
+
.tree-ask .ask-label .where { margin-left: 6px; letter-spacing: 0; color: var(--text-3); overflow-wrap: anywhere; }
|
|
1911
|
+
.tree-ask .ask-hint { font-size: 10.5px; line-height: 1.45; color: var(--text-2); overflow-wrap: anywhere; }
|
|
1912
|
+
.tree-ask input { width: 100%; min-width: 0; border: 1px solid var(--line-2); border-radius: 6px; background: color-mix(in srgb, var(--text-3) 6%, transparent); color: var(--text); font: inherit; font-size: 13px; padding: 6px 8px; outline: none; }
|
|
1913
|
+
.tree-ask input:focus { border-color: var(--phosphor); }
|
|
1914
|
+
.tree-ask .ask-actions { display: flex; justify-content: flex-end; gap: 4px; }
|
|
1915
|
+
.tree-ask .go { color: var(--bg); background: var(--phosphor); }
|
|
1916
|
+
.tree-ask .go:hover { background: var(--phosphor); filter: brightness(1.08); }
|
|
1917
|
+
.tree .node.fresh { animation: tree-fresh 2.4s ease-out; }
|
|
1918
|
+
@keyframes tree-fresh { 0%, 35% { background: color-mix(in srgb, var(--phosphor) 22%, transparent); } 100% { background: transparent; } }
|
|
1919
|
+
@media (prefers-reduced-motion: reduce) { .tree .node.fresh { animation: none; background: color-mix(in srgb, var(--phosphor) 14%, transparent); } }
|
|
1819
1920
|
.tree-menu { position: absolute; z-index: 20; min-width: 170px; border: 1px solid var(--line-2); border-radius: 10px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.35); padding: 5px; display: grid; gap: 1px; }
|
|
1820
1921
|
.tree-menu button { border: 0; background: transparent; color: var(--text); font: inherit; font-size: 12px; text-align: left; padding: 7px 10px; border-radius: 7px; cursor: pointer; }
|
|
1821
1922
|
.tree-menu button:hover { background: color-mix(in srgb, var(--phosphor) 12%, transparent); }
|
|
@@ -1891,8 +1992,13 @@ dialog.viewer::backdrop { background: rgba(0,0,0,.7); backdrop-filter: blur(4px)
|
|
|
1891
1992
|
.command-card .head { display: flex; gap: 10px; align-items: baseline; padding: 8px 12px; font-family: var(--mono); font-size: 10.5px; letter-spacing: .14em; color: var(--text-2); border-bottom: 1px solid var(--line-2); }
|
|
1892
1993
|
.command-card .head b { color: var(--phosphor); font-weight: 600; }
|
|
1893
1994
|
.command-card .head .args { color: var(--text-3); letter-spacing: .04em; }
|
|
1995
|
+
.command-card .head > span { min-width: 0; overflow-wrap: anywhere; }
|
|
1894
1996
|
.command-card pre { margin: 0; padding: 10px 12px; max-height: 320px; overflow: auto; font-family: var(--mono); font-size: 12px; line-height: 1.55; color: var(--text); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1895
1997
|
.command-card pre .h { color: var(--phosphor); font-weight: 600; }
|
|
1998
|
+
.command-card .undo { margin: 0 12px 10px; border: 1px solid var(--terror); background: color-mix(in srgb, var(--terror) 12%, transparent); color: var(--terror); font: inherit; font-family: var(--mono); font-size: 10px; letter-spacing: .16em; padding: 7px 12px; border-radius: 8px; cursor: pointer; }
|
|
1999
|
+
.command-card .undo:hover { background: color-mix(in srgb, var(--terror) 22%, transparent); }
|
|
2000
|
+
.command-card .undo:disabled { opacity: .5; cursor: default; }
|
|
2001
|
+
.command-card .outcome { display: inline-block; margin: 0 12px 10px; font-family: var(--mono); font-size: 10px; letter-spacing: .16em; color: var(--phosphor); }
|
|
1896
2002
|
|
|
1897
2003
|
.module-card select { border: 1px solid var(--ph-dim); background: #000; color: var(--ph); font: inherit; font-size: 10.5px; letter-spacing: .06em; padding: 6px 8px; }
|
|
1898
2004
|
|
|
@@ -2449,3 +2555,4 @@ dialog.tour::backdrop { background: transparent; }
|
|
|
2449
2555
|
:root[data-scheme="light"] .slash-menu .item.starter:has(.shape.sends) { --shape: #1f6f9e; }
|
|
2450
2556
|
:root[data-scheme="light"] .slash-menu .item.starter:has(.shape.card) { --shape: #9a6400; }
|
|
2451
2557
|
@media (max-width: 560px) { .slash-menu.with-starters { grid-template-columns: minmax(0, 1fr); } }
|
|
2558
|
+
.system.module-proposed .replaces { display: block; margin-top: 4px; color: var(--warn, var(--text-2)); }
|
package/src/adapters/claude.mjs
CHANGED
|
@@ -25,7 +25,7 @@ export const claudeMcpTools = (server) => (server.tools?.length ? server.tools.m
|
|
|
25
25
|
// symlink, in which case the resolved one travels too — see the note in createLease.
|
|
26
26
|
export const leaseRoots = (lease) => [...new Set([lease?.outDir, lease?.realOutDir].filter(Boolean))];
|
|
27
27
|
|
|
28
|
-
export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [] }) {
|
|
28
|
+
export function buildClaudeArgs({ prompt, model = null, effort = null, attachmentsDir = null, lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [] }) {
|
|
29
29
|
const tools = claudeTools({ lease, scopes });
|
|
30
30
|
const mcpTools = [
|
|
31
31
|
...(memoryServer ? memoryServer.tools.map((tool) => `mcp__${memoryServer.name}__${tool}`) : []),
|
|
@@ -50,6 +50,7 @@ export function buildClaudeArgs({ prompt, model = null, attachmentsDir = null, l
|
|
|
50
50
|
return [
|
|
51
51
|
'-p',
|
|
52
52
|
...(model ? ['--model', model] : []),
|
|
53
|
+
...(effort ? ['--effort', effort] : []),
|
|
53
54
|
// Attachments live outside the project; Read needs the folder allowed.
|
|
54
55
|
...(attachmentsDir ? ['--add-dir', attachmentsDir] : []),
|
|
55
56
|
'--output-format', 'json',
|
|
@@ -105,11 +106,11 @@ export function parseClaudeOutput(output) {
|
|
|
105
106
|
}
|
|
106
107
|
}
|
|
107
108
|
|
|
108
|
-
export function invokeClaude({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
109
|
+
export function invokeClaude({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, effort = null, attachments = [], lease = null, scopes = null, imageStudio = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
109
110
|
return runReadonlyProcess({
|
|
110
111
|
onProgress: onProgress ? (out) => onProgress({ chars: (() => { try { return String(JSON.parse(out)?.result ?? '').length; } catch { return 0; } })() }) : null,
|
|
111
112
|
executable,
|
|
112
|
-
args: buildClaudeArgs({ prompt, model, attachmentsDir: attachments[0]?.dir ?? null, lease, scopes, imageStudio, memoryServer, mcpServers }),
|
|
113
|
+
args: buildClaudeArgs({ prompt, model, effort, attachmentsDir: attachments[0]?.dir ?? null, lease, scopes, imageStudio, memoryServer, mcpServers }),
|
|
113
114
|
cwd: projectRoot,
|
|
114
115
|
env: process.env,
|
|
115
116
|
timeoutMs,
|
package/src/adapters/codex.mjs
CHANGED
|
@@ -24,7 +24,7 @@ export function codexMcpOverrides(servers) {
|
|
|
24
24
|
});
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function buildCodexArgs({ projectRoot, prompt, model = null, attachments = [], lease = null, scopes = null, memoryServer = null, mcpServers = [] }) {
|
|
27
|
+
export function buildCodexArgs({ projectRoot, prompt, model = null, effort = null, attachments = [], lease = null, scopes = null, memoryServer = null, mcpServers = [] }) {
|
|
28
28
|
const images = attachments.filter((file) => /^image\//.test(file.contentType ?? ''));
|
|
29
29
|
return [
|
|
30
30
|
// AIRLOCK (#4): commands with network, so pushes and deploys can leave; otherwise the sandbox.
|
|
@@ -35,6 +35,7 @@ export function buildCodexArgs({ projectRoot, prompt, model = null, attachments
|
|
|
35
35
|
'-C', lease ? lease.outDir : projectRoot,
|
|
36
36
|
...codexMcpOverrides([memoryServer, ...mcpServers]),
|
|
37
37
|
// Image generation is a Codex feature; the human's scope decides per turn.
|
|
38
|
+
...(effort ? ['-c', `model_reasoning_effort=${tomlValue(effort)}`] : []),
|
|
38
39
|
...(lease && (scopes?.imageGen === false || lease.scopes?.imageGen === false) ? ['-c', 'features.image_generation=false'] : []),
|
|
39
40
|
'exec',
|
|
40
41
|
// Codex refuses to run outside a git repository it has not been told to trust, and a new
|
|
@@ -78,11 +79,11 @@ export function parseCodexOutput(output) {
|
|
|
78
79
|
return { text: text.trim(), usage, ...(error ? { error } : {}) };
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
export function invokeCodex({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, attachments = [], lease = null, scopes = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
82
|
+
export function invokeCodex({ executable, projectRoot, prompt, timeoutMs = 120000, signal, model = null, effort = null, attachments = [], lease = null, scopes = null, memoryServer = null, mcpServers = [], onProgress = null }) {
|
|
82
83
|
return runReadonlyProcess({
|
|
83
84
|
onProgress: onProgress ? (out) => onProgress({ chars: parseCodexOutput(out).text.length }) : null,
|
|
84
85
|
executable,
|
|
85
|
-
args: buildCodexArgs({ projectRoot, prompt, model, attachments, lease, scopes, memoryServer, mcpServers }),
|
|
86
|
+
args: buildCodexArgs({ projectRoot, prompt, model, effort, attachments, lease, scopes, memoryServer, mcpServers }),
|
|
86
87
|
cwd: lease ? lease.outDir : projectRoot,
|
|
87
88
|
env: process.env,
|
|
88
89
|
timeoutMs,
|
package/src/choices.mjs
CHANGED
|
@@ -12,18 +12,36 @@
|
|
|
12
12
|
//
|
|
13
13
|
// ```pulse-ask
|
|
14
14
|
// Where should the exported dataset live?
|
|
15
|
+
// - In the project, so it travels with the repository (recommended)
|
|
15
16
|
// - Beside the room log, with the rest of the archive
|
|
16
|
-
// - In the project, so it travels with the repository
|
|
17
17
|
// - Somewhere the human names each time
|
|
18
18
|
// ```
|
|
19
19
|
//
|
|
20
20
|
// Two options are a yes/no, which a sentence already handles; the block earns its place from
|
|
21
21
|
// three. Above six it is a list, not a choice, and the room says so by refusing it.
|
|
22
|
+
//
|
|
23
|
+
// One option may carry the agent's own pick: "(recommended)" at the end of the line, in either
|
|
24
|
+
// language, or a ★ in front. An agent that asks without saying what it would do has handed the
|
|
25
|
+
// human the whole decision and kept the half it was asked for, so the briefing requires it; the
|
|
26
|
+
// parser only reads it, and a choice that forgot still reaches the human.
|
|
22
27
|
|
|
23
28
|
export const MIN_OPTIONS = 3;
|
|
24
29
|
export const MAX_OPTIONS = 6;
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
// Long enough for the option an agent actually writes — the reported one was cut at 120 in the
|
|
31
|
+
// middle of a word, and the half that was missing was the half that made it an option. Past
|
|
32
|
+
// this it is a paragraph, and it is cut at a word with an ellipsis rather than mid-letter.
|
|
33
|
+
const MAX_QUESTION = 300;
|
|
34
|
+
const MAX_OPTION = 320;
|
|
35
|
+
|
|
36
|
+
const RECOMMENDED = /\s*[(\[]\s*(?:recommended|recomendad[ao]|sugerid[ao])\s*[)\]]\s*$/i;
|
|
37
|
+
const STAR = /^[★☆⭐]\s*/;
|
|
38
|
+
|
|
39
|
+
function clip(text, max) {
|
|
40
|
+
if (text.length <= max) return text;
|
|
41
|
+
const cut = text.slice(0, max - 1);
|
|
42
|
+
const space = cut.lastIndexOf(' ');
|
|
43
|
+
return `${(space > max * 0.6 ? cut.slice(0, space) : cut).replace(/[\s,;:.-]+$/, '')}…`;
|
|
44
|
+
}
|
|
27
45
|
|
|
28
46
|
// The last ```pulse-ask block of a reply, if it closes the reply. Returns null for anything
|
|
29
47
|
// else: a block quoted mid-answer is an example, and an example is not a question.
|
|
@@ -36,22 +54,28 @@ export function parseChoice(text) {
|
|
|
36
54
|
|
|
37
55
|
const lines = last[1].split('\n').map((line) => line.trim()).filter(Boolean);
|
|
38
56
|
if (!lines.length) return null;
|
|
39
|
-
const question = lines[0].startsWith('-') ? null : lines[0]
|
|
57
|
+
const question = lines[0].startsWith('-') ? null : clip(lines[0], MAX_QUESTION);
|
|
40
58
|
const options = [];
|
|
41
59
|
const seen = new Set();
|
|
60
|
+
let recommended = null;
|
|
42
61
|
for (const line of lines.slice(question ? 1 : 0)) {
|
|
43
62
|
const match = line.match(/^[-*•]\s+(.+)$/);
|
|
44
63
|
if (!match) continue;
|
|
45
|
-
|
|
64
|
+
let label = match[1].trim();
|
|
65
|
+
const marked = RECOMMENDED.test(label) || STAR.test(label);
|
|
66
|
+
label = clip(label.replace(RECOMMENDED, '').replace(STAR, '').trim(), MAX_OPTION);
|
|
46
67
|
// The same option twice is a mistake, and a choice that offers it twice reads as rigged.
|
|
47
68
|
const key = label.toLowerCase();
|
|
48
69
|
if (!label || seen.has(key)) continue;
|
|
49
70
|
seen.add(key);
|
|
71
|
+
// Only the first mark counts: two recommendations are no recommendation, but dropping both
|
|
72
|
+
// would punish the agent for the half it got right.
|
|
73
|
+
if (marked && recommended === null) recommended = options.length;
|
|
50
74
|
options.push(label);
|
|
51
75
|
}
|
|
52
76
|
if (!question) return null;
|
|
53
77
|
if (options.length < MIN_OPTIONS || options.length > MAX_OPTIONS) return null;
|
|
54
|
-
return { question, options };
|
|
78
|
+
return { question, options, recommended };
|
|
55
79
|
}
|
|
56
80
|
|
|
57
81
|
// What the reply reads as once the block has been lifted out of it: the room shows the question
|
package/src/file-ops.mjs
CHANGED
|
@@ -43,3 +43,22 @@ export function planFileOp({ operation, from, to }) {
|
|
|
43
43
|
if (operation === 'move' && `${target}/`.startsWith(`${source}/`)) return { ok: false, error: 'A folder cannot be moved inside itself.' };
|
|
44
44
|
return { ok: true, operation, from: source, to: target, guarded: needsDesignation(source, target) };
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
// The name a copy takes when its own is already used where it lands: "notes copy.md", then
|
|
48
|
+
// "notes copy 2.md", the way a file manager does it. `word` is the room's word for "copy", and
|
|
49
|
+
// `taken` answers whether a project-relative path already holds something.
|
|
50
|
+
export async function freeCopyPath(to, { taken, word = 'copy', limit = 100 }) {
|
|
51
|
+
const path = String(to ?? '').replace(/^\/+/, '').trim();
|
|
52
|
+
if (!path || !(await taken(path))) return path;
|
|
53
|
+
const slash = path.lastIndexOf('/');
|
|
54
|
+
const dir = slash >= 0 ? path.slice(0, slash + 1) : '';
|
|
55
|
+
const name = path.slice(slash + 1);
|
|
56
|
+
// A leading dot is part of the name (".env"), not an extension.
|
|
57
|
+
const dot = name.lastIndexOf('.');
|
|
58
|
+
const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, ''];
|
|
59
|
+
for (let n = 1; n <= limit; n += 1) {
|
|
60
|
+
const candidate = `${dir}${stem} ${word}${n > 1 ? ` ${n}` : ''}${ext}`;
|
|
61
|
+
if (!(await taken(candidate))) return candidate;
|
|
62
|
+
}
|
|
63
|
+
return path;
|
|
64
|
+
}
|
package/src/models.mjs
CHANGED
|
@@ -26,6 +26,25 @@ export const KNOWN_MODELS = {
|
|
|
26
26
|
},
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
// How hard the model thinks before it answers, for the CLIs that take it per run. Claude Code
|
|
30
|
+
// reads --effort; Codex reads model_reasoning_effort as a config override. Gemini and OpenCode
|
|
31
|
+
// have no per-run knob MADRE can name truthfully (OpenCode's --variant means something different
|
|
32
|
+
// for each provider), so they get no chooser at all rather than one that guesses.
|
|
33
|
+
//
|
|
34
|
+
// Ordered lightest first: the composer draws them as a ladder, and the order is the meaning.
|
|
35
|
+
export const EFFORTS = {
|
|
36
|
+
claude: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
37
|
+
codex: ['minimal', 'low', 'medium', 'high', 'xhigh'],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function isValidEffort(agentId, effort) {
|
|
41
|
+
return typeof effort === 'string' && (EFFORTS[agentId] ?? []).includes(effort);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function parseCodexDefaultEffort(toml) {
|
|
45
|
+
return String(toml ?? '').match(/^\s*model_reasoning_effort\s*=\s*"([^"]+)"/m)?.[1] ?? null;
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
export function parseCodexModelCache(json) {
|
|
30
49
|
try {
|
|
31
50
|
const data = JSON.parse(json);
|
|
@@ -58,9 +77,12 @@ export async function discoverModels({ agents = [], config = {}, home = homedir(
|
|
|
58
77
|
const known = KNOWN_MODELS[agent.id] ?? { defaults: [], note: '' };
|
|
59
78
|
let discovered = [];
|
|
60
79
|
let cliDefault = null;
|
|
80
|
+
let effortDefault = null;
|
|
61
81
|
if (agent.id === 'codex') {
|
|
62
82
|
discovered = parseCodexModelCache(await readFile(join(home, '.codex', 'models_cache.json'), 'utf8').catch(() => ''));
|
|
63
|
-
|
|
83
|
+
const toml = await readFile(join(home, '.codex', 'config.toml'), 'utf8').catch(() => '');
|
|
84
|
+
cliDefault = parseCodexDefaultModel(toml);
|
|
85
|
+
effortDefault = parseCodexDefaultEffort(toml);
|
|
64
86
|
}
|
|
65
87
|
if (agent.id === 'opencode' && agent.detected) {
|
|
66
88
|
discovered = await listOpenCode(agent).catch(() => []);
|
|
@@ -71,6 +93,8 @@ export async function discoverModels({ agents = [], config = {}, home = homedir(
|
|
|
71
93
|
models: unique([...custom, ...discovered, ...known.defaults]),
|
|
72
94
|
default: cliDefault,
|
|
73
95
|
note: known.note,
|
|
96
|
+
efforts: EFFORTS[agent.id] ?? [],
|
|
97
|
+
effortDefault: isValidEffort(agent.id, effortDefault) ? effortDefault : null,
|
|
74
98
|
source: discovered.length ? 'discovered' : 'known',
|
|
75
99
|
};
|
|
76
100
|
}
|
package/src/modules/index.mjs
CHANGED
|
@@ -15,6 +15,7 @@ import imageStudio from './image-studio.mjs';
|
|
|
15
15
|
import gitPulse from './git-pulse.mjs';
|
|
16
16
|
import ash from './ash.mjs';
|
|
17
17
|
import choices from './choices.mjs';
|
|
18
|
+
import runs from './runs.mjs';
|
|
18
19
|
import ripley from './ripley.mjs';
|
|
19
20
|
import ollama from './ollama.mjs';
|
|
20
21
|
import playwright from './playwright.mjs';
|
|
@@ -27,7 +28,7 @@ import { defineModule, matchRoute } from './sdk.mjs';
|
|
|
27
28
|
// so a module of somebody's own cannot take one and collide with it the day it comes out.
|
|
28
29
|
export const LAB_MODULES = [correo];
|
|
29
30
|
const labsAsked = new Set(String(process.env.PULSE_LABS ?? '').split(',').map((id) => id.trim()).filter(Boolean));
|
|
30
|
-
export const MODULES = [ahp, imageStudio, gitPulse, ash, choices, ripley, ollama, playwright, ...LAB_MODULES.filter((module) => labsAsked.has(module.id))];
|
|
31
|
+
export const MODULES = [ahp, imageStudio, gitPulse, ash, choices, runs, ripley, ollama, playwright, ...LAB_MODULES.filter((module) => labsAsked.has(module.id))];
|
|
31
32
|
export const BUILTIN_IDS = new Set([...MODULES, ...LAB_MODULES].map((module) => module.id));
|
|
32
33
|
export const loadFailures = []; // { file, error } for MODULES to show
|
|
33
34
|
export const moduleFolders = ({ stateRoot, projectRoot }) => ({ user: join(stateRoot, 'modules'), project: join(projectRoot, '.madre', 'modules') });
|