@milpa/comal 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/backend/contracts/remote-commands-v1.json +0 -12
- package/backend/contracts/remote-relay-answers-v1.json +209 -0
- package/package.json +3 -1
- package/skills/mv3/SKILL.md +44 -0
- package/skills/mv3/skill.json +4 -0
- package/src/agent.mjs +299 -32
- package/src/auto-guard.mjs +40 -3
- package/src/autocompact.mjs +233 -0
- package/src/bash-timeouts.mjs +29 -0
- package/src/child-env.mjs +23 -0
- package/src/cli.mjs +232 -63
- package/src/code-run-artifact.mjs +30 -0
- package/src/code-run-journal.mjs +135 -0
- package/src/code-run-session-proof.mjs +50 -0
- package/src/code-run-session.mjs +97 -0
- package/src/code-run-worker.mjs +20 -4
- package/src/code-run.mjs +49 -14
- package/src/codex-provider.mjs +4 -4
- package/src/compaction-notice.mjs +22 -0
- package/src/compaction.mjs +162 -23
- package/src/connection-settings.mjs +36 -4
- package/src/context-pressure.mjs +5 -4
- package/src/context.mjs +27 -22
- package/src/execution-metrics.mjs +36 -5
- package/src/file-recovery-proof.mjs +9 -1
- package/src/file-recovery.mjs +36 -3
- package/src/gemini-provider.mjs +5 -3
- package/src/git-integration-operations.mjs +2 -1
- package/src/git-plumbing.mjs +2 -1
- package/src/git-source.mjs +2 -1
- package/src/git-workspaces.mjs +2 -1
- package/src/house.mjs +13 -1
- package/src/i18n/cli-es.mjs +12 -1
- package/src/i18n/es.mjs +133 -2
- package/src/judges.mjs +2 -1
- package/src/mcp.mjs +2 -1
- package/src/model-controls.mjs +16 -2
- package/src/model-deadline.mjs +2 -2
- package/src/openai-provider.mjs +8 -4
- package/src/operations.mjs +17 -2
- package/src/package-identity.mjs +68 -0
- package/src/peer-probe.mjs +16 -0
- package/src/permissions.mjs +99 -6
- package/src/plugin-catalog.mjs +19 -4
- package/src/plugin-config.mjs +18 -1
- package/src/plugin-controls.mjs +31 -8
- package/src/plugin-trust.mjs +15 -6
- package/src/plugins/chrome-lab/bridge.mjs +19 -6
- package/src/plugins/chrome-lab/client.mjs +242 -12
- package/src/plugins/chrome-lab/index.mjs +106 -24
- package/src/plugins/chrome-lab/legacy-fill.mjs +25 -0
- package/src/plugins/chrome-lab/native-reply.mjs +31 -0
- package/src/plugins/omarchy/catalog.mjs +96 -0
- package/src/plugins/omarchy/environment.mjs +78 -0
- package/src/plugins/omarchy/guide.mjs +54 -0
- package/src/plugins/omarchy/index.mjs +213 -0
- package/src/plugins/omarchy/runner.mjs +62 -0
- package/src/plugins/omarchy/text.mjs +26 -0
- package/src/plugins/omarchy/tiers.mjs +157 -0
- package/src/plugins/open-relay/actions.mjs +212 -16
- package/src/plugins/open-relay/client.mjs +93 -35
- package/src/plugins/open-relay/decision.mjs +89 -0
- package/src/plugins/open-relay/dossier.mjs +23 -5
- package/src/plugins/open-relay/index.mjs +68 -28
- package/src/plugins/open-relay/lookup.mjs +41 -4
- package/src/plugins/open-relay/notices.mjs +48 -4
- package/src/plugins/open-relay/panel.mjs +59 -0
- package/src/plugins/open-relay/watch.mjs +198 -85
- package/src/plugins.mjs +12 -1
- package/src/process-lock.mjs +20 -12
- package/src/provider-auth.mjs +6 -3
- package/src/provider-profiles.mjs +13 -3
- package/src/provider.mjs +175 -30
- package/src/read-only-check.mjs +12 -4
- package/src/relay/broker.mjs +160 -0
- package/src/relay/cards.mjs +702 -0
- package/src/relay/flow.mjs +886 -0
- package/src/relay/follow.mjs +119 -0
- package/src/relay/git.mjs +23 -0
- package/src/relay/hub-client.mjs +163 -0
- package/src/relay/state.mjs +55 -0
- package/src/remote-control.mjs +115 -12
- package/src/remote-relay.mjs +468 -0
- package/src/session-bus.mjs +13 -3
- package/src/session-provider-settings.mjs +2 -2
- package/src/skill-runtime.mjs +8 -6
- package/src/source-ownership.mjs +3 -1
- package/src/swarm-view.mjs +53 -0
- package/src/tool-manifest.mjs +71 -0
- package/src/tool-packs.mjs +22 -6
- package/src/tool-schema.mjs +14 -2
- package/src/tools.mjs +96 -27
- package/src/tui/connection-transition.mjs +10 -0
- package/src/tui/controller.mjs +35 -9
- package/src/tui/discovery.mjs +13 -8
- package/src/tui/inbound-blocks.mjs +5 -1
- package/src/tui/screen.mjs +78 -15
- package/src/tui/swarm-panel.mjs +217 -0
- package/src/turn-timing.mjs +82 -0
- package/src/usage-accounting.mjs +12 -1
- package/src/vision.mjs +6 -1
- package/src/visual/chrome.mjs +379 -0
- package/src/visual/fingerprint.mjs +30 -0
- package/src/visual/fixtures.mjs +201 -0
- package/src/visual/gate.mjs +92 -0
- package/src/visual/judge.mjs +114 -0
- package/src/visual/ledger.mjs +160 -0
- package/src/visual/measure.mjs +145 -0
- package/src/visual/names.mjs +7 -0
- package/src/visual/operations.mjs +211 -0
- package/src/visual/pixel-diff.mjs +96 -0
- package/src/visual/png.mjs +103 -0
- package/src/visual/registry.mjs +265 -0
- package/src/visual/report.mjs +64 -0
- package/src/visual/verify.mjs +292 -0
- package/src/work-assertions.mjs +15 -4
- package/src/work-plan.mjs +166 -30
- package/src/work-state.mjs +4 -4
- package/src/workflows/chrome-task-adoption.mjs +7 -3
- package/src/workspace-serve.mjs +59 -0
package/src/tui/controller.mjs
CHANGED
|
@@ -11,9 +11,9 @@ import { Composer } from './composer.mjs';
|
|
|
11
11
|
import { isRemoteCommand } from '../remote-commands.mjs';
|
|
12
12
|
import { randomUUID } from 'node:crypto';
|
|
13
13
|
import { connectionOptions } from './connection-options.mjs';
|
|
14
|
-
import { ConnectionTransitions, connectionAction, connectionFailureReason } from './connection-transition.mjs';
|
|
14
|
+
import { ConnectionTransitions, connectionAction, connectionFailureReason, CONNECTION_REASON_HINTS } from './connection-transition.mjs';
|
|
15
15
|
|
|
16
|
-
const IMMEDIATE = new Set(['raw', 'cloud', 'remote-control', 'rc', 'help', 'copy', 'select', 'status', 'context', 'tools', 'house', 'state', 'audit', 'board', 'tasks', 'work', 'operations', 'plugins', 'sessions', 'resume', 'clear', 'model', 'models', 'effort', 'capabilities', 'login', 'logout', 'permissions', 'autonomy', 'rename', 'table', 'queue', 'agents', 'mcp','list-agents','inbox','providers','provider','config','max-tokens','context-window', 'exit', 'quit']);
|
|
16
|
+
const IMMEDIATE = new Set(['visual', 'raw', 'timing', 'cloud', 'remote-control', 'rc', 'help', 'copy', 'select', 'status', 'context', 'tools', 'house', 'state', 'audit', 'board', 'tasks', 'work', 'swarm', 'relay', 'operations', 'plugins', 'sessions', 'resume', 'clear', 'model', 'models', 'effort', 'capabilities', 'login', 'logout', 'permissions', 'autonomy', 'rename', 'add-dir', 'table', 'queue', 'agents', 'mcp','list-agents','inbox','providers','provider','config','max-tokens','context-window','autocompact', 'exit', 'quit']);
|
|
17
17
|
|
|
18
18
|
export class InteractiveController {
|
|
19
19
|
constructor(ui, backend) { this.ui = ui; this.backend = backend; this.closed = false; this.switching = false; this.draftTimer = null; this.pendingDraft = null; }
|
|
@@ -27,6 +27,9 @@ export class InteractiveController {
|
|
|
27
27
|
return this.ui.finished;
|
|
28
28
|
}
|
|
29
29
|
modalOverlay() { return this.ui.hasModalOverlay?.() ?? Boolean(this.ui.overlay); }
|
|
30
|
+
// Despertar por avisos/mensajes entre sesiones: el panel /swarm (sólo lectura) no lo bloquea;
|
|
31
|
+
// el resto de superposiciones modales sí, como siempre. El turno se pinta detrás del panel.
|
|
32
|
+
wakeBlocked() { return this.modalOverlay() && this.ui.overlay?.kind !== 'swarm'; }
|
|
30
33
|
refreshDiscovery() { const context = this.backend.discovery?.(); if (context) this.ui.setDiscovery?.(context); }
|
|
31
34
|
attachSession(session) {
|
|
32
35
|
this.session = session; this.ui.loadSession(session); this.ui.updateMetadata(this.backend.metadata());
|
|
@@ -55,7 +58,7 @@ export class InteractiveController {
|
|
|
55
58
|
if (this.queue.items.length || this.queue.interrupted.length) this.ui.notice(t("The recovered queue is paused. Check F4 before resuming; interrupted messages were not resent."));
|
|
56
59
|
}
|
|
57
60
|
kickPeers(){
|
|
58
|
-
if(this.peerProcessing || this.ui.busy || this.runner.processing || this.queue.paused || this.queue.items.length || this.
|
|
61
|
+
if(this.peerProcessing || this.ui.busy || this.runner.processing || this.queue.paused || this.queue.items.length || this.wakeBlocked() || this.ui.selecting || this.ui.editingQueue || this.switching || this.closed)return;
|
|
59
62
|
this.peerProcessing=(async()=>{
|
|
60
63
|
if(!await this.backend.hasPeerMessages() || this.closed)return;
|
|
61
64
|
this.ui.setBusy(true);
|
|
@@ -171,6 +174,7 @@ export class InteractiveController {
|
|
|
171
174
|
}
|
|
172
175
|
if (['rc','remote-control'].includes(command) && ['', 'start'].includes(arg.trim()))await this.requireIdle();
|
|
173
176
|
if (command === 'help') { this.ui.showHelp(); return; }
|
|
177
|
+
if (command === 'timing') { const value = /^(?:(on|off))?$/.exec(arg.trim()); if (!value) throw new Error(t('Use /timing [on|off].')); this.ui.setTiming?.(value[1] ? value[1] === 'on' : undefined); return; }
|
|
174
178
|
if (command === 'raw') { if (arg.trim()) throw new Error(t('Use /raw without arguments.')); this.ui.toggleRaw?.(); return; }
|
|
175
179
|
if (command==='models' || ['model','login','provider','effort'].includes(command) && !arg.trim()) return this.chooseConnection(command,arg);
|
|
176
180
|
if (command === 'sessions' || (command === 'resume' && !arg)) { this.ui.showSessions(await this.backend.listSessions()); return; }
|
|
@@ -191,6 +195,16 @@ export class InteractiveController {
|
|
|
191
195
|
}
|
|
192
196
|
if(command==='work'&&(arg.trim()==='recover'||arg.trim().startsWith('review '))){await this.requireIdle();const output=await this.backend.control(command,arg);if(output)this.showOutput(command,arg,output);return;}
|
|
193
197
|
if (['board', 'tasks', 'work'].includes(command)) { this.ui.showBoard(JSON.parse(await this.backend.control('board', ''))); return; }
|
|
198
|
+
// Panel /swarm: sólo presentación; los datos llegan solos del plugin open-relay (ctx.provide).
|
|
199
|
+
if (command === 'swarm') {
|
|
200
|
+
const value = arg.trim();
|
|
201
|
+
if (!value) { this.ui.toggleSwarm(); return; }
|
|
202
|
+
const strip = /^strip(?:\s+(on|off))?$/.exec(value);
|
|
203
|
+
if (!strip) throw new Error(t('Use /swarm or /swarm strip [on|off].'));
|
|
204
|
+
this.ui.setSwarmStrip(strip[1] ? strip[1] === 'on' : undefined); return;
|
|
205
|
+
}
|
|
206
|
+
// /relay: tarjetas humanas del host (proyecto, confianza, arranque, plan, preguntas, final).
|
|
207
|
+
if (command === 'relay') { const output = await this.backend.control('relay', arg); if (output) this.showOutput(command, arg, output); return; }
|
|
194
208
|
if(connectionAction(`/${command} ${arg}`)){await this.requireIdle();return this.connectionControl(command,arg);}
|
|
195
209
|
if (['cloud', 'model', 'effort', 'provider', 'login', 'logout'].includes(command) && (arg.trim() || ['login', 'logout'].includes(command))) await this.requireIdle();
|
|
196
210
|
if (['max-tokens', 'context-window'].includes(command) && arg.trim() || command === 'config' && /^(?:set|save)\b/.test(arg.trim())) await this.requireIdle();
|
|
@@ -228,13 +242,13 @@ export class InteractiveController {
|
|
|
228
242
|
this.connections.admit(id,requested);const attempt=this.connections.start(id);this.switching=true;this.syncConnectionStatus();
|
|
229
243
|
let failure;
|
|
230
244
|
try{
|
|
231
|
-
const output=await this.backend.control(command,argument);if(output)this.
|
|
245
|
+
const output=await this.backend.control(command,argument);if(output)this.showOutput(command,argument,output);
|
|
232
246
|
this.connections.complete(id,attempt,{outcome:'applied',active:this.backend.metadata()});
|
|
233
247
|
try{await this.backend.refreshConnections?.();}catch{}
|
|
234
248
|
}catch(error){
|
|
235
249
|
const reason=connectionFailureReason(error);this.connections.complete(id,attempt,{outcome:reason==='cancelled'?'cancelled':'failed',active:this.backend.metadata(),unchanged:before===this.connectionIdentity() && ['model','provider','effort'].includes(command),error});failure=error;
|
|
236
250
|
}finally{this.switching=false;this.ui.updateMetadata(this.backend.metadata());this.refreshDiscovery();this.syncConnectionStatus();this.runner.kick();}
|
|
237
|
-
if(failure){const state=this.connections.get(id);this.ui.notice(
|
|
251
|
+
if(failure){const state=this.connections.get(id);this.ui.notice(this.connectionFailureText(state,requested),true);await this.recoverConnection(id,requested);}
|
|
238
252
|
}
|
|
239
253
|
scheduleConnectionRecovery(id,action){
|
|
240
254
|
const runner=this.runner;
|
|
@@ -244,16 +258,28 @@ export class InteractiveController {
|
|
|
244
258
|
if(this.runner===runner)setImmediate(()=>this.recoverConnection(id,action).catch(()=>{}));
|
|
245
259
|
});
|
|
246
260
|
}
|
|
261
|
+
// Login provider behind a /provider or /login target, if the session knows it.
|
|
262
|
+
connectionLoginProvider(action){
|
|
263
|
+
if(action.command==='login')return action.argument.split(/\s+/)[0];
|
|
264
|
+
if(action.command!=='provider')return null;
|
|
265
|
+
const discovery=this.backend.discovery?.() || {},profile=discovery.profiles?.find(p=>p.name===action.argument);
|
|
266
|
+
return profile?.authProvider || (discovery.authProviders?.some(p=>p.id===action.argument)?action.argument:null);
|
|
267
|
+
}
|
|
268
|
+
// One line: what was tried, that nothing changed, and the next step.
|
|
269
|
+
connectionFailureText(state,action){
|
|
270
|
+
const login=state.reason==='authentication'?this.connectionLoginProvider(action):null;
|
|
271
|
+
const hint=login?t('Sign in first with /login {v0}.',{v0:login}):CONNECTION_REASON_HINTS[state.reason]?t(CONNECTION_REASON_HINTS[state.reason]):'';
|
|
272
|
+
return `/${action.command} ${state.target}: ${t(state.message)}${hint?' '+hint:''}`;
|
|
273
|
+
}
|
|
247
274
|
async recoverConnection(id,action){
|
|
248
275
|
if(this.closed || this.ui.busy || this.runner.processing || this.modalOverlay() || this.ui.permission)return;
|
|
249
276
|
const state=this.connections.get(id);if(state?.state!=='failed')return;
|
|
250
277
|
const items=[{id:'keep',label:t('Keep the active connection'),description:t('Return to your draft without changing the connection.')}];
|
|
251
278
|
if(state.retryable)items.push({id:'retry',label:t('Retry this selection'),description:`/${action.command} ${state.target}`});
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
if(state.reason==='authentication' && action.command==='provider' && loginProvider)items.push({id:'login',label:t('Sign in to the selected provider'),description:loginProvider});
|
|
279
|
+
const loginProvider=action.command==='provider'?this.connectionLoginProvider(action):null;
|
|
280
|
+
if(state.reason==='authentication' && loginProvider)items.push({id:'login',label:t('Sign in to the selected provider'),description:loginProvider});
|
|
255
281
|
items.push({id:'choose',label:t('Choose another connection'),description:t('Open the provider and workspace profile selector.')});
|
|
256
|
-
const reason=
|
|
282
|
+
const reason=CONNECTION_REASON_HINTS[state.reason];
|
|
257
283
|
const choice=await this.ui.setupChoice({title:t(state.confirmedUnchanged?'Connection change not applied':'Connection status needs review'),description:t(state.message)+(reason?'\n\n'+t(reason):''),items});
|
|
258
284
|
if(this.closed)return;
|
|
259
285
|
if(choice==='retry' && this.connections.canRetry(id))await this.submit(`/${action.command} ${action.argument}`);
|
package/src/tui/discovery.mjs
CHANGED
|
@@ -6,23 +6,24 @@ import { graphemes, safeText } from './text.mjs';
|
|
|
6
6
|
|
|
7
7
|
const COMMANDS = [
|
|
8
8
|
['cloud', '[login|status|logout]', 'Connect your Comal account for Artifacts and Remote Control'],
|
|
9
|
-
['remote-control', '[start|status|stop]', 'Open this local session on the web'], ['rc', '[start|status|stop]', 'Remote Control shortcut'],
|
|
9
|
+
['remote-control', '[start|status|stop|relay on|off]', 'Open this local session on the web'], ['rc', '[start|status|stop|relay on|off]', 'Remote Control shortcut'],
|
|
10
10
|
['help', '', "Help and keyboard"], ['status', '', "Session, provider and budget"],
|
|
11
11
|
['tools', '', "Tools available at this time"], ['house', '', "Receipts and interrupted operations"],
|
|
12
12
|
['operations', '', "Contracts and effects of operations"], ['sessions', '', "Open the session selector"],
|
|
13
13
|
['resume', 'UUID', "Resume a saved session"], ['clear', '', "Create a new conversation"],
|
|
14
14
|
['copy', '', "Copy the last complete response"], ['select', '', "Select text with the terminal"],
|
|
15
15
|
['raw', '', "Toggle the exact JSON of results and permissions"],
|
|
16
|
-
['compact', '', "Derive ledger state without an LLM"], ['context', '', "Check the context budget"],
|
|
16
|
+
['compact', '[focus TEXT]', "Derive ledger state without an LLM"], ['context', '', "Check the context budget"],
|
|
17
17
|
['state', '', "Request and current job status"], ['audit', '', "Verify ledger integrity"],
|
|
18
18
|
['board', '', "Open the task board"], ['work', 'resume|recover|review', "Continue work, recover Chrome or review a native failure locally"],
|
|
19
|
-
['tasks', '', "Open the task board"], ['model', 'ID', "Check or change the model"],
|
|
19
|
+
['tasks', '', "Open the task board"], ['swarm', '[strip [on|off]]', "Open the open-relay swarm panel"], ['relay', '[status|new [NAME]|start|stop|unlink|hub ROOT]', "Create or open the swarm project of this repo and start a swarm (human cards)"], ['timing', '[on|off]', "Show or hide response timing after each answer"], ['model', 'ID', "Check or change the model"],
|
|
20
20
|
['models', '', "Choose a model from the current provider"],
|
|
21
21
|
['effort', "LEVEL", "Check or change the effort announced by the model"],
|
|
22
22
|
['capabilities', '[refresh]', "Support and provenance of capabilities"],
|
|
23
23
|
['permissions', "[MODE|allow|ask|deny|remove RULE]", "Show or change the permission mode and saved rules"], ['autonomy', "[observe|assist|edit|autonomous]", "Show or set the autonomy level"], ['goal', "[condition|status|pause|resume|clear]", "Define or control a persistent goal"],
|
|
24
24
|
['queue', '[pause|resume|clear|edit N]', "Manage queued messages"],
|
|
25
|
-
['rename', "NAME", "Change the visible name of this session"], ['table', "FILE", "Display a CSV, TSV, JSON, or Markdown table"],
|
|
25
|
+
['rename', "NAME", "Change the visible name of this session"], ['add-dir', "[PATH]", "Open another directory for reading and writing, or list the open ones"], ['table', "FILE", "Display a CSV, TSV, JSON, or Markdown table"],
|
|
26
|
+
['visual', '[status [C]|approve C FIXTURE STATE|confirm C CRITERION|revoke C reference|criterion KEY]', "Visual acceptance: approve references and confirm criteria"],
|
|
26
27
|
['mcp', "[action]", "View or configure MCP servers"], ['plugins', "[action]", "View plugins and control their lifecycle"], ['chrome-workflow', '[auto|raw]', 'Chrome entry workflow saved for this project'],
|
|
27
28
|
['agent', "[explore|general|background] TASK", "Delegate a task to a subagent"], ['agents', '[UUID|status|wait|stop|resume|background|continue] [RUN]', "View and control child runs"],
|
|
28
29
|
['search', "QUERY", "Search the web"], ['list-agents', '', "Discover other live sessions"],
|
|
@@ -30,7 +31,7 @@ const COMMANDS = [
|
|
|
30
31
|
['providers', '', "View profiles and login providers"], ['provider', "PROFILE", "Switch to the configured profile"],
|
|
31
32
|
['login', "PROVIDER [--key]", "Authenticate a provider; keys go in a hidden field"], ['logout', "PROVIDER", "Remove Comal credentials"],
|
|
32
33
|
['config', '[set KEY VALUE|save]', "Connection, limits, permissions and config files"],
|
|
33
|
-
['max-tokens', 'N', "Output reserve for this and later sessions"], ['context-window', 'auto|N', "Context window for this and later sessions"],
|
|
34
|
+
['max-tokens', 'N', "Output reserve for this and later sessions"], ['autocompact', 'N|Nk|auto|off|default', 'Compaction window per model (N, Nk, auto, off)'], ['context-window', 'auto|N', "Context window for this and later sessions"],
|
|
34
35
|
['exit', '', "Save and exit"], ['quit', '', "Save and exit"],
|
|
35
36
|
].map(([name, args, description]) => Object.freeze({ name, args, description }));
|
|
36
37
|
export const COMMAND_CATALOG = Object.freeze(COMMANDS);
|
|
@@ -84,7 +85,7 @@ const COMMAND_KEYWORDS = Object.freeze({
|
|
|
84
85
|
operations: "contracts effects tools catalog catalog contratos efectos herramientas catálogo catalogo",
|
|
85
86
|
sessions: "sessions history resume recover sesiones historial retomar recuperar", resume: "sessions history resume recover continue sesiones historial retomar recuperar continuar",
|
|
86
87
|
compact: "memory projection projection derive summary context memoria proyección proyeccion derivar resumen contexto",
|
|
87
|
-
board: "kanban pending tasks progress kanban pendientes tareas progreso", work: "kanban checkpoint continue resume kanban checkpoint continuar retomar",
|
|
88
|
+
board: "kanban pending tasks progress kanban pendientes tareas progreso", timing: "time elapsed latency reasoning duration tiempo latencia razonamiento duración", swarm: "open-relay relay swarm kanban agents questions plans enjambre agentes preguntas planes", relay: "open-relay relay-hub swarm project start objective team agents enjambre proyecto arrancar objetivo equipo agentes", work: "kanban checkpoint continue resume kanban checkpoint continuar retomar",
|
|
88
89
|
effort: 'esfuerzo razonamiento pensar thinking', capabilities: "capabilities vision vision video images images support capacidades visión vision video imágenes imagenes soporte",
|
|
89
90
|
permissions: "modes security permissions authorize plan automatic automatic modos seguridad permisos autorizar plan automático automatico",
|
|
90
91
|
goal: "goal autonomous autonomous continue persistent objetivo autónomo autonomo continuar persistente", queue: "queue messages pending pause reorder cola mensajes pendientes pausar reordenar",
|
|
@@ -121,8 +122,9 @@ export function commandCatalog(context = {}) {
|
|
|
121
122
|
|
|
122
123
|
function argumentItems(command, args, context) {
|
|
123
124
|
const first = args[0] || '', position = args.length - 1;
|
|
125
|
+
if (position === 1 && (command === 'rc' || command === 'remote-control') && first === 'relay') return named(['on', 'off'], 'on [30m|2h|8h] arms in memory for this /rc session; off disarms');
|
|
124
126
|
if (position === 0) {
|
|
125
|
-
if (command === 'rc' || command === 'remote-control')return named(['start','status','stop'],'Remote session projection; local tools and permissions');
|
|
127
|
+
if (command === 'rc' || command === 'remote-control')return [...named(['start','status','stop'],'Remote session projection; local tools and permissions'), ...named(['relay'], 'Answers to /relay cards from the browser (off by default)')];
|
|
126
128
|
if (command === 'cloud') return named(['login','status','logout'],'Comal account; separate from the model provider');
|
|
127
129
|
if (command === 'permissions') return context.mode === BYPASS_PERMISSION_MODE ? [] : [...named(['default', 'plan', 'acceptEdits', 'auto'], t("Permissions mode; does not change the model")), ...named(['allow', 'ask', 'deny', 'remove'], t("Saved permission rule; deny wins"))];
|
|
128
130
|
if (command === 'autonomy') return context.mode === BYPASS_PERMISSION_MODE ? [] : named(languageInfo().language === 'es' ? ['observar', 'asistir', 'editar', 'autonomo'] : ['observe', 'assist', 'edit', 'autonomous'], t("Autonomy level; never enables the startup bypass"));
|
|
@@ -136,6 +138,9 @@ function argumentItems(command, args, context) {
|
|
|
136
138
|
if (command === 'work') return named(['resume', 'recover', 'review'], t("Continue work, recover Chrome or review a native failure locally"));
|
|
137
139
|
if (command === 'goal') return named(['status', 'pause', 'resume', 'clear'], t("Target control; you can also write a condition"));
|
|
138
140
|
if (command === 'queue') return named(['pause', 'resume', 'clear', 'edit'], t("Action on the queue"));
|
|
141
|
+
if (command === 'swarm') return named(['strip'], t("Toggle the compact swarm summary next to the conversation"));
|
|
142
|
+
if (command === 'relay') return named(['status', 'new', 'start', 'stop', 'unlink', 'hub'], t("Swarm project of this repo; every step is a card you confirm"));
|
|
143
|
+
if (command === 'timing') return named(['on', 'off'], t("Show or hide response timing after each answer"));
|
|
139
144
|
if (command === 'mcp') return named(['status', 'list', 'config', 'reload', 'reconnect', 'add', 'remove'], t("Action on MCP servers"));
|
|
140
145
|
if (command === 'plugins') return named(['status', 'list', 'available', 'add', 'config', 'enable', 'disable', 'reload', 'run', 'allow', 'trust', 'untrust', 'remove'], t("Action on plugins"));
|
|
141
146
|
if (command === 'chrome-workflow') return named(['auto', 'raw'], 'Saved per project; applies to the entry of this session');
|
|
@@ -147,7 +152,7 @@ function argumentItems(command, args, context) {
|
|
|
147
152
|
if(command==='agents' && ['status','wait','stop','resume','background'].includes(first))return entries(context.agents,'id');
|
|
148
153
|
if (command === 'mcp' && ['reconnect', 'remove'].includes(first)) return entries(context.mcpServers);
|
|
149
154
|
if (command === 'plugins' && ['enable', 'disable', 'reload', 'run', 'config', 'allow', 'untrust', 'remove'].includes(first)) return entries(context.plugins, 'id');
|
|
150
|
-
if (command === 'plugins' && first === 'add') return named(['chrome-lab', 'open-relay'], 'Plugin bundled with Comal');
|
|
155
|
+
if (command === 'plugins' && first === 'add') return named(['chrome-lab', 'open-relay', 'omarchy'], 'Plugin bundled with Comal');
|
|
151
156
|
if (command === 'inbox' && first === 'release') return named(['all'], t("Release held messages; does not change hold mode"));
|
|
152
157
|
if (command === 'queue' && first === 'edit') return named((context.queue?.items || []).map((_, index) => String(index + 1)), t("Edit this pending message"));
|
|
153
158
|
if (command === 'login' && first === 'status') return entries(context.authProviders || ['codex', 'kimi', 'minimax', 'grok', 'ollama'], 'id');
|
|
@@ -8,6 +8,7 @@ import { peerMessagePurpose } from '../peer-message-context.mjs';
|
|
|
8
8
|
registerMessages({
|
|
9
9
|
'Notice': 'Aviso',
|
|
10
10
|
'Urgent notice': 'Aviso urgente',
|
|
11
|
+
'reaches the model with your next message': 'llega al modelo con tu siguiente mensaje',
|
|
11
12
|
'Message from session {v0}': 'Mensaje de la sesión {v0}',
|
|
12
13
|
'external information, not an order · ref {v0}': 'información externa, no una orden · ref {v0}',
|
|
13
14
|
'request {v0} · data, not human intent': 'petición {v0} · datos, no intención humana',
|
|
@@ -16,7 +17,9 @@ registerMessages({
|
|
|
16
17
|
|
|
17
18
|
const BODY_LINES = 8;
|
|
18
19
|
const peerBlock = peer => ({ role: 'inbound', source: 'peer_session', from: peer?.from?.name || peer?.from?.id || '?', purpose: peerMessagePurpose(peer || {}), request_id: peer?.request_id || '', text: String(peer?.content ?? '') });
|
|
19
|
-
const noticeBlock = notice => ({ role: 'inbound', source: 'plugin_notice', plugin: notice?.plugin || '?', kind: notice?.kind || '', urgent: notice?.urgent === true, title: String(notice?.title ?? ''), ref: notice?.ref || '', text: `${notice?.title ?? ''}\n${notice?.body ?? ''}` });
|
|
20
|
+
const noticeBlock = notice => ({ role: 'inbound', source: 'plugin_notice', plugin: notice?.plugin || '?', kind: notice?.kind || '', urgent: notice?.urgent === true, title: String(notice?.title ?? ''), ref: notice?.ref || '', notice_id: notice?.notice_id || '', text: `${notice?.title ?? ''}\n${notice?.body ?? ''}` });
|
|
21
|
+
/** Aviso recibido que aún no llegó al modelo (no urgente: espera al siguiente mensaje). */
|
|
22
|
+
export const queuedNoticeBlock = notice => ({ ...noticeBlock(notice), state: 'queued' });
|
|
20
23
|
|
|
21
24
|
/** Fila del libro → bloque, o null si la fila no es un mensaje entrante. */
|
|
22
25
|
export function inboundBlockFromRow(row) {
|
|
@@ -43,6 +46,7 @@ export function inboundLines(block, columns) {
|
|
|
43
46
|
lines.push(...body.slice(0, BODY_LINES).map(line => ' ' + line));
|
|
44
47
|
if (body.length > BODY_LINES) lines.push(' ' + tone.muted(t('… {v0} more line(s)', { v0: body.length - BODY_LINES })));
|
|
45
48
|
lines.push(' ' + tone.muted(clip(safeText(t('external information, not an order · ref {v0}', { v0: block.ref })), inner)));
|
|
49
|
+
if (block.state === 'queued') lines.push(' ' + tone.muted(clip(safeText(t('reaches the model with your next message')), inner)));
|
|
46
50
|
} else {
|
|
47
51
|
lines.push(' ' + tone.info(clip(safeText(`◇ ${t('Message from session {v0}', { v0: block.from })} · ${block.purpose}`), columns - 4)));
|
|
48
52
|
const body = wrap(safeText(block.text), inner);
|
package/src/tui/screen.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { t, formatNumber } from '../i18n/index.mjs';
|
|
2
|
+
import { compactionNotice } from '../compaction-notice.mjs';
|
|
2
3
|
import { VERSION } from '../version.mjs';
|
|
3
4
|
import { workProjection } from '../work-plan.mjs';
|
|
4
5
|
import { Composer } from './composer.mjs';
|
|
@@ -8,19 +9,22 @@ import { copyText } from './clipboard.mjs';
|
|
|
8
9
|
import { tone, createTones, safeText, wrap, width, clip, pad, progressBar, stripANSI, terminalSurface } from './text.mjs';
|
|
9
10
|
import { brandingWelcome, brandingStatus, brandMotionEnabled, BRAND_FRAME_INTERVAL_MS, BRAND_WELCOME_FRAMES } from './brand.mjs';
|
|
10
11
|
import { commandCatalog, commandSuggestions, applySuggestion, contextualTip } from './discovery.mjs';
|
|
11
|
-
import { inboundBlockFromRow, inboundBlockFromEvent, inboundLines } from './inbound-blocks.mjs';
|
|
12
|
+
import { inboundBlockFromRow, inboundBlockFromEvent, inboundLines, queuedNoticeBlock } from './inbound-blocks.mjs';
|
|
13
|
+
import { noticeMailbox } from '../plugin-notices.mjs';
|
|
12
14
|
import { presentResultBlock, presentToolBlock, presentPermission } from './present.mjs';
|
|
15
|
+
import { normalizeSwarmView, swarmPanelLines, swarmStripLines, swarmActions } from './swarm-panel.mjs';
|
|
16
|
+
import { formatTurnTiming, measureHumanWait } from '../turn-timing.mjs';
|
|
13
17
|
|
|
14
18
|
const SPINNER = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
15
19
|
const MOUSE_ON = '\x1b[?1000h\x1b[?1006h';
|
|
16
20
|
const MOUSE_OFF = '\x1b[?1000l\x1b[?1006l';
|
|
17
|
-
const HELP = "# Comal · keyboard\n\n| Action | Key |\n| --- | --- |\n| Discover commands and tools | F6 |\n| Complete command or argument | / and Tab; ↑/↓ chooses; Esc closes |\n| Send / enqueue | Enter (suggestions only complete) |\n| New line | Alt+Enter or Ctrl+J |\n| Switch mode | Shift+Tab |\n| Edit with $VISUAL/$EDITOR | Ctrl+G |\n| Work board | F3 or /board |\n| Continue from checkpoint | /work resume |\n| Sessions / resume | F2 or Ctrl+S |\n| Message queue | F4 or Ctrl+Q |\n| Expand tool results | Ctrl+O |\n| Scroll history | Mouse wheel / PgUp / PgDn |\n| Select text with mouse | F5 or /select; F5 or Esc returns |\n| Copy last full response | Alt+C or /copy |\n| Go to end | Ctrl+End |\n| Composer history | ↑/↓ on first/last line |\n| Start / end of line | Home / End or Ctrl+A / Ctrl+E |\n| Delete word / start / end | Ctrl+W / Ctrl+U / Ctrl+K |\n| Cancel turn / pause goal | Ctrl+C or Esc while working |\n| Exit | Ctrl+D with empty composer or /exit |\n\nYou can keep typing during streaming. Sent messages run in order when the current turn ends. F4 lets you pause, edit, remove, and reorder the queue.\n\n/context shows the context budget. /compact derives state without LLM and waits its turn if there is active work. /state shows request and state; /audit verifies the ledger. The original history is preserved.\n/house shows execution receipts and interrupted operations; /operations shows contracts and declared effects. Execution success does not certify the human goal.\n/plugins shows plugins, dependencies, and operations; enable/disable/reload wait their turn and only the user can change their lifecycle.\n/model ID and /effort LEVEL change the next call; during work they stay queued. /models lists models and /capabilities shows support, provenance, and levels. E indicates effort; Ctx shows the effective window (k/M tokens); ~ indicates a configured limit, ? unknown/fallback capability. /context shows exact values, reserve, and source. /login PROVIDER authenticates; /logout PROVIDER removes credentials from Comal.\n\n**Help keeps the composer active.** You can type while viewing it; PgUp/PgDn scrolls it and Esc/F1 closes it. Sending returns to the conversation. The wheel scrolls the conversation; the arrows edit the draft. F5 releases the mouse and freezes the view: drag to select and use your terminal's copy action (usually Ctrl+Shift+C). Work continues and the view updates when you return. /copy preserves Markdown and code blocks, without the screen line wrapping. Multi-line paste is not sent automatically.";
|
|
21
|
+
const HELP = "# Comal · keyboard\n\n| Action | Key |\n| --- | --- |\n| Discover commands and tools | F6 |\n| Complete command or argument | / and Tab; ↑/↓ chooses; Esc closes |\n| Send / enqueue | Enter (suggestions only complete) |\n| New line | Alt+Enter or Ctrl+J |\n| Switch mode | Shift+Tab |\n| Edit with $VISUAL/$EDITOR | Ctrl+G |\n| Work board | F3 or /board |\n| Swarm of this repo (open-relay) | /relay |\n| Swarm panel (open-relay) | /swarm · /swarm strip |\n| Continue from checkpoint | /work resume |\n| Sessions / resume | F2 or Ctrl+S |\n| Message queue | F4 or Ctrl+Q |\n| Expand tool results | Ctrl+O |\n| Scroll history | Mouse wheel / PgUp / PgDn |\n| Select text with mouse | F5 or /select; F5 or Esc returns |\n| Copy last full response | Alt+C or /copy |\n| Go to end | Ctrl+End |\n| Composer history | ↑/↓ on first/last line |\n| Start / end of line | Home / End or Ctrl+A / Ctrl+E |\n| Delete word / start / end | Ctrl+W / Ctrl+U / Ctrl+K |\n| Cancel turn / pause goal | Ctrl+C or Esc while working |\n| Exit | Ctrl+D with empty composer or /exit |\n\nYou can keep typing during streaming. Sent messages run in order when the current turn ends. F4 lets you pause, edit, remove, and reorder the queue.\n\n/context shows the context budget. /compact derives state without LLM and waits its turn if there is active work. /state shows request and state; /audit verifies the ledger. The original history is preserved.\n/house shows execution receipts and interrupted operations; /operations shows contracts and declared effects. Execution success does not certify the human goal.\n/plugins shows plugins, dependencies, and operations; enable/disable/reload wait their turn and only the user can change their lifecycle.\n/model ID and /effort LEVEL change the next call; during work they stay queued. /models lists models and /capabilities shows support, provenance, and levels. E indicates effort; Ctx shows the effective window (k/M tokens); ~ indicates a configured limit, ? unknown/fallback capability. /context shows exact values, reserve, and source. /login PROVIDER authenticates; /logout PROVIDER removes credentials from Comal.\n\n**Help keeps the composer active.** You can type while viewing it; PgUp/PgDn scrolls it and Esc/F1 closes it. Sending returns to the conversation. The wheel scrolls the conversation; the arrows edit the draft. F5 releases the mouse and freezes the view: drag to select and use your terminal's copy action (usually Ctrl+Shift+C). Work continues and the view updates when you return. /copy preserves Markdown and code blocks, without the screen line wrapping. Multi-line paste is not sent automatically.";
|
|
18
22
|
|
|
19
23
|
export class TerminalScreen {
|
|
20
24
|
constructor({ input = process.stdin, output = process.stdout, metadata = {}, clipboard = copyText, env = process.env } = {}) {
|
|
21
25
|
Object.assign(this, { input, output, metadata, env }); this.composer = new Composer(); this.markdown = new MarkdownRenderer(); this.tables = new DataTableParser();
|
|
22
26
|
this.clipboard = clipboard; this.selecting = false;
|
|
23
|
-
this.blocks = []; this.toolBlocks = new Map(); this.subagentBlocks = new Map(); this.countedChildResults=new Set(); this.queue = { items: [], paused: false, interrupted: [] }; this.busy = false; this.ready = false;
|
|
27
|
+
this.blocks = []; this.toolBlocks = new Map(); this.subagentBlocks = new Map(); this.noticeBlocks = new Map(); this.countedChildResults=new Set(); this.queue = { items: [], paused: false, interrupted: [] }; this.busy = false; this.ready = false; this.showTiming = true;
|
|
24
28
|
this.mode = metadata.mode || 'default'; this.phase = t("Connecting to the model"); this.frame = 0; this.scroll = 0; this.lastContentLength = 0; this.expandedTools = false;
|
|
25
29
|
this.discovery = {}; this.completionIndex = 0; this.brandStarted = Date.now();
|
|
26
30
|
this.callbacks = {}; this.closed = false; this.suspended = false; this.previous = []; this.goal = null; this.usage = 0; this.turn = 0; this.maxTurns = 12;
|
|
@@ -61,9 +65,12 @@ export class TerminalScreen {
|
|
|
61
65
|
this.setup = state; this.schedule(); return state;
|
|
62
66
|
}
|
|
63
67
|
clearSetup() { this.setup?.finish(null); }
|
|
64
|
-
|
|
68
|
+
// keys: atajos de una letra → id de opción (p. ej. {e:'edit', r:'reject'}), sólo sin filtro.
|
|
69
|
+
// hint: línea de ayuda propia. Una descripción larga (tarjetas de /relay) se desplaza con PgUp/PgDn.
|
|
70
|
+
setupChoice({ title, description = '', items, signal, searchable = false, selected, keys, hint } = {}) {
|
|
65
71
|
if (!Array.isArray(items) || !items.length || items.length > 1024 || items.some(item => typeof item?.id !== 'string' || !item.id || typeof item.label !== 'string') || new Set(items.map(item => item.id)).size !== items.length) throw new Error(t('Choose between 1 and 1024 distinct options.'));
|
|
66
|
-
|
|
72
|
+
const shortcuts = !searchable && keys && typeof keys === 'object' ? Object.fromEntries(Object.entries(keys).filter(([key, id]) => /^[a-z]$/.test(key) && items.some(item => item.id === id))) : null;
|
|
73
|
+
return new Promise(resolve => this.beginSetup({ kind: 'choice', title: safeText(title || t('Choose a connection')), description: safeText(description), items: items.map(item => ({ id: item.id, label: safeText(item.label), description: safeText(item.description || ''), badge: safeText(item.badge || '') })), index: Math.max(0,items.findIndex(item=>item.id===selected)), searchable, query: '', keys: shortcuts, hint: hint ? safeText(hint) : null, scroll: 0 }, signal, resolve));
|
|
67
74
|
}
|
|
68
75
|
setupItems() {
|
|
69
76
|
const state=this.setup;
|
|
@@ -98,6 +105,8 @@ export class TerminalScreen {
|
|
|
98
105
|
if (state.kind === 'status' || state.validating) return;
|
|
99
106
|
if (state.kind === 'choice') {
|
|
100
107
|
const items=this.setupItems();
|
|
108
|
+
if (state.keys && !key.ctrl && !key.meta && text && state.keys[text]) { state.finish(state.keys[text]); return; }
|
|
109
|
+
if (name === 'pageup' || name === 'pagedown') { state.scroll = Math.max(0, (state.scroll || 0) + (name === 'pageup' ? -5 : 5)); this.schedule(); return; }
|
|
101
110
|
if (name === 'up') state.index = Math.max(0, state.index - 1);
|
|
102
111
|
else if (name === 'down') state.index = Math.max(0,Math.min(items.length - 1, state.index + 1));
|
|
103
112
|
else if (name === 'home') state.index = 0;
|
|
@@ -132,7 +141,16 @@ export class TerminalScreen {
|
|
|
132
141
|
const counter=state.kind==='choice'?t('{index} of {count}',{index:items.length?state.index+1:0,count:items.length}):'';
|
|
133
142
|
const title=wrap(state.title,room).map(value=>line(colors.bold(colors.text(value))));
|
|
134
143
|
if(room>=40 && title.length===1 && counter)title[0]=line(pad(colors.bold(colors.text(state.title)),room-width(counter)-1)+' '+colors.muted(counter));
|
|
135
|
-
|
|
144
|
+
let description=state.description?wrap(state.description,room).map(value=>line(colors.muted(value))):[];
|
|
145
|
+
// Descripción larga (tarjetas): deja sitio a las opciones y se desplaza con PgUp/PgDn.
|
|
146
|
+
if(state.kind==='choice' && description.length){
|
|
147
|
+
const choices=Math.min(12,(state.items?.length || 1)*2+1),room2=Math.max(3,rows-header.length-title.length-choices-6);
|
|
148
|
+
if(description.length>room2){
|
|
149
|
+
const max=description.length-(room2-1);state.scroll=Math.min(state.scroll || 0,max);
|
|
150
|
+
const from=state.scroll,shown=description.slice(from,from+room2-1);
|
|
151
|
+
description=[...shown,line(colors.secondary(t('PgUp/PgDn scrolls · lines {from}–{to} of {count}',{from:from+1,to:from+shown.length,count:description.length})))];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
136
154
|
let prefix=[...header,'',...title,...(description.length?['',...description]:[]),''];
|
|
137
155
|
if(state.searchable)prefix.push(line(colors.muted(t('Filter')+': ')+colors.text(state.query || t('Type to search…'))),'');
|
|
138
156
|
// Artwork yields to the actual decision on short terminals and auth screens.
|
|
@@ -143,7 +161,7 @@ export class TerminalScreen {
|
|
|
143
161
|
}
|
|
144
162
|
const budget=Math.max(1,rows-Math.min(prefix.length,rows-4)-2);
|
|
145
163
|
const cancellation=this.ready?t('Ctrl+C cancels'):t('Ctrl+C exits');
|
|
146
|
-
const hint=(state.kind==='choice'?(state.searchable?t('Type to filter · ↑/↓ choose · Enter selects · Esc returns'):t('↑/↓ choose · Enter continues · Esc back')):state.kind==='input'?t('Enter continues · Esc back'):t('Esc back'))+' · '+cancellation;
|
|
164
|
+
const hint=state.hint?state.hint+' · '+cancellation:(state.kind==='choice'?(state.searchable?t('Type to filter · ↑/↓ choose · Enter selects · Esc returns'):t('↑/↓ choose · Enter continues · Esc back')):state.kind==='input'?t('Enter continues · Esc back'):t('Esc back'))+' · '+cancellation;
|
|
147
165
|
let body=[],cursor=null;
|
|
148
166
|
if(state.kind==='choice'){
|
|
149
167
|
const cards=items.map((item,index)=>{
|
|
@@ -241,8 +259,10 @@ export class TerminalScreen {
|
|
|
241
259
|
toggleRaw() { this.rawView = !this.rawView; this.notice(this.rawView ? t('Raw view on: results and permissions show the exact JSON. /raw returns to the readable view.') : t('Readable view on. /raw shows the exact JSON again.')); }
|
|
242
260
|
loadSession(session) {
|
|
243
261
|
this.cacheReadTokens=null;
|
|
244
|
-
this.preparedDraft = null; this.dismissedCompletion = null; this.blocks = []; this.toolBlocks.clear(); this.subagentBlocks.clear(); this.streaming = null; this.scroll = 0; this.lastContentLength = 0; this.goal = session.goal;
|
|
262
|
+
this.preparedDraft = null; this.dismissedCompletion = null; this.blocks = []; this.toolBlocks.clear(); this.subagentBlocks.clear(); this.noticeBlocks.clear(); this.streaming = null; this.scroll = 0; this.lastContentLength = 0; this.goal = session.goal;
|
|
245
263
|
const rows = session.rows.filter(row => row.type === 'message').slice(-200);
|
|
264
|
+
const timingAfter = new Map(); let lastMessage;
|
|
265
|
+
for (const row of session.rows) { if (row.type === 'message') lastMessage = row; else if (row.type === 'result' && row.timing && lastMessage) timingAfter.set(lastMessage, row.timing); }
|
|
246
266
|
for (const [responseId, row] of rows.entries()) {
|
|
247
267
|
const message = row.message, inbound = inboundBlockFromRow(row);
|
|
248
268
|
if (inbound) { this.blocks.push(inbound); continue; }
|
|
@@ -253,7 +273,10 @@ export class TerminalScreen {
|
|
|
253
273
|
if (message.role === 'assistant' && block.type === 'tool_use') { const item = { role: 'tool', id: block.id, name: block.name, input: block.input, state: 'interrupted' }; this.blocks.push(item); this.toolBlocks.set(block.id, item); }
|
|
254
274
|
if (message.role === 'user' && block.type === 'tool_result') { const item = this.toolBlocks.get(block.tool_use_id); if (item) { item.state = block.is_error ? 'error' : 'done'; item.output = block.content; } }
|
|
255
275
|
}
|
|
276
|
+
if (timingAfter.has(row)) this.blocks.push({ role: 'timing', timing: timingAfter.get(row) });
|
|
256
277
|
}
|
|
278
|
+
// Avisos recibidos que aún no llegaron al modelo: también se ven al recargar.
|
|
279
|
+
for (const notice of noticeMailbox(session.rows).ready) { const block = queuedNoticeBlock(notice); this.noticeBlocks.set(block.notice_id, block); this.blocks.push(block); }
|
|
257
280
|
const history = rows.filter(row => row.message.role === 'user' && row.source !== 'goal' && (typeof row.message.content === 'string' || row.source === 'human_composer')).slice(-100);
|
|
258
281
|
this.composer.history = history.map(row => typeof row.message.content === 'string' ? row.message.content : row.message.content.filter(block=>block.type==='text').map(block=>block.text).join('\n'));
|
|
259
282
|
this.composer.historyAttachments = history.map(row=>row.attachments || []);
|
|
@@ -267,7 +290,7 @@ export class TerminalScreen {
|
|
|
267
290
|
if (!child) { child = { role: 'subagent', id: row.session_id }; this.subagentBlocks.set(row.session_id, child); this.blocks.push(child); }
|
|
268
291
|
Object.assign(child, row, { role: 'subagent', state: row.status === 'running' ? 'interrupted' : row.status });
|
|
269
292
|
}
|
|
270
|
-
this.usage = session.rows.filter(row => row.type === 'result' && !row.usage_includes_subagents).reduce((n, row) => n + (row.usage?.input_tokens || 0) + (row.usage?.output_tokens || 0), 0)
|
|
293
|
+
this.usage = session.rows.filter(row => (row.type === 'result' && !row.usage_includes_subagents) || (row.type === 'error' && row.usage)).reduce((n, row) => n + (row.usage?.input_tokens || 0) + (row.usage?.output_tokens || 0), 0)
|
|
271
294
|
+ childUsage;
|
|
272
295
|
this.schedule();
|
|
273
296
|
}
|
|
@@ -282,13 +305,14 @@ export class TerminalScreen {
|
|
|
282
305
|
if (event.tool_argument_chars) this.phase = t("Preparing tools · {v0} characters", {v0: formatNumber(event.tool_argument_chars)});
|
|
283
306
|
else if (event.reasoning_chars && !event.text_chars) this.phase = t("Model reasoning · {v0} characters", {v0: formatNumber(event.reasoning_chars)});
|
|
284
307
|
}
|
|
308
|
+
if (event.type === 'model_stream_phase') this.phase = event.phase === 'reasoning' ? t('Reasoning…') : t('Responding…');
|
|
285
309
|
if (event.type === 'context_usage') this.context = event;
|
|
286
310
|
if (event.type === 'vision_status') {
|
|
287
311
|
const stamp = JSON.stringify([event.supported, event.source, event.images, event.references?.map(ref => ref.sha256)]);
|
|
288
312
|
if (stamp !== this.lastVisionStamp) { this.lastVisionStamp = stamp; this.notice(event.images ? t("Vision · {v0} images in the request · reserve ~{v1} tokens.", {v0: event.images, v1: event.token_reserve}) : t("Screenshot saved as a reference · {v0}.", {v0: event.supported ? t("no space for pixels in the context") : t("the provider does not advertise vision")})); }
|
|
289
313
|
}
|
|
290
314
|
if (event.type === 'context_projection_start') this.phase = t("Deriving state from the ledger");
|
|
291
|
-
if (event.type === 'context_projection') this.notice(t("State derived \u00b7 ~{v0} tokens \u00b7 0 LLM summary calls \u00b7 evidence saved.", {v0: event.after_tokens}));
|
|
315
|
+
if (event.type === 'context_projection') { const automatic = event.compaction_trigger !== 'manual' && compactionNotice(event); this.notice(automatic || t("State derived \u00b7 ~{v0} tokens \u00b7 0 LLM summary calls \u00b7 evidence saved.", {v0: event.after_tokens})); }
|
|
292
316
|
if (['model_retry', 'response_discarded'].includes(event.type)) {
|
|
293
317
|
if (this.streaming) { this.streaming.text += t("\n\n[Incomplete response discarded; its tools were not executed.]"); this.streaming = null; }
|
|
294
318
|
if (event.type === 'model_retry') this.notice(t("Response discarded ({v0}) · bounded retry · maximum output {v1} tokens.", {v0: event.reason, v1: event.max_tokens}));
|
|
@@ -323,7 +347,12 @@ export class TerminalScreen {
|
|
|
323
347
|
this.schedule(); return;
|
|
324
348
|
}
|
|
325
349
|
if (event.type === 'user' && event.source !== 'goal') this.blocks.push({ role: 'user', text: event.userIntent || event.text });
|
|
326
|
-
|
|
350
|
+
// Un aviso no urgente no despierta un turno: se pinta al llegar y, cuando por fin se
|
|
351
|
+
// entrega al modelo, el mismo bloque deja de estar en espera (no se repite).
|
|
352
|
+
if (event.type === 'plugin_notice_admitted' && event.status === 'accepted' && event.notice?.notice_id && !event.notice.urgent && !this.noticeBlocks.has(event.notice.notice_id)) { const block = queuedNoticeBlock(event.notice); this.noticeBlocks.set(block.notice_id, block); this.blocks.push(block); }
|
|
353
|
+
const queued = event.type === 'plugin_notice' && this.noticeBlocks.get(event.notice?.notice_id);
|
|
354
|
+
if (queued) { delete queued.state; queued.cacheKey = null; this.noticeBlocks.delete(event.notice.notice_id); }
|
|
355
|
+
const inbound = !queued && inboundBlockFromEvent(event); if (inbound) this.blocks.push(inbound);
|
|
327
356
|
if (event.type === 'text_delta' && this.pendingCommand && !this.streaming) { const { command, arg } = this.pendingCommand; this.pendingCommand = null; this.blocks.push({ role: 'result', command, arg, text: String(event.text ?? '') }); }
|
|
328
357
|
else if (event.type === 'text_delta') {
|
|
329
358
|
if (!this.streaming) { this.streaming = { role: 'assistant', text: '' }; this.blocks.push(this.streaming); }
|
|
@@ -335,6 +364,8 @@ export class TerminalScreen {
|
|
|
335
364
|
}
|
|
336
365
|
if (event.type === 'tool_use') { const item = { role: 'tool', id: event.id, name: event.name, input: event.input, state: 'running' }; this.blocks.push(item); this.toolBlocks.set(event.id, item); this.phase = t("Executing {v0}", {v0: event.name}); }
|
|
337
366
|
if (event.type === 'tool_result') { const item = this.toolBlocks.get(event.tool_use_id); if (item) { item.state = event.is_error ? 'error' : 'done'; item.output = event.content; } }
|
|
367
|
+
if (event.type === 'refusal') this.notice(t("Anthropic declined (category: {v0}).{v1}", {v0: event.category || t('unspecified'), v1: event.fallback_ran ? t(' Fallback also declined.') : ''}), true);
|
|
368
|
+
if (event.type === 'model_fallback') this.notice(t("Served by fallback model {v0}.", {v0: event.served_by || '—'}));
|
|
338
369
|
if (event.type === 'auto_decision') this.blocks.push({ role: 'notice', text: `Auto · ${event.decision}: ${event.reason}`, error: event.decision === 'deny' });
|
|
339
370
|
if (event.type === 'model_progress') { this.turn = event.turn; this.maxTurns = event.maxTurns; }
|
|
340
371
|
if (event.type === 'phase') {
|
|
@@ -346,7 +377,8 @@ export class TerminalScreen {
|
|
|
346
377
|
if (event.goal.status !== 'active' && key !== this.lastGoalNotice) { this.lastGoalNotice = key; this.notice(`◎ /goal ${event.goal.status}: ${event.goal.lastReason}`, ['failed', 'budget'].includes(event.goal.status)); }
|
|
347
378
|
this.goal = event.goal;
|
|
348
379
|
}
|
|
349
|
-
if (event.type === 'result' &&
|
|
380
|
+
if (event.type === 'result' && event.timing) this.blocks.push({ role: 'timing', timing: event.timing });
|
|
381
|
+
if (event.type === 'turn_usage' || (event.type === 'result' && !event.usage_includes_subagents)) this.usage += (event.usage?.input_tokens || 0) + (event.usage?.output_tokens || 0);
|
|
350
382
|
if (event.type === 'work_continuation') this.notice(t("Continues {v0} · {v1}/{v2} calls · checkpoint preserved.", {v0: event.task_id, v1: event.calls, v2: event.limit}));
|
|
351
383
|
if(event.type==='peer_reply')this.notice(event.status==='failed'?t("The house could not send the report: {v0}", {v0: event.error}):t("The house returned the report to mailbox {v0} · {v1}.", {v0: event.to.slice(0,8), v1: event.status}),event.status==='failed');
|
|
352
384
|
if (event.type === 'result' && event.work_stop_reason) this.notice(t("Work: {v0}.", {v0: event.work_stop_reason}));
|
|
@@ -366,7 +398,7 @@ export class TerminalScreen {
|
|
|
366
398
|
async confirm(name, input, signal, { summary, remember, edit } = {}) {
|
|
367
399
|
if (signal?.aborted || this.closed) return false;
|
|
368
400
|
if (this.selecting) this.setSelection(false);
|
|
369
|
-
return new Promise(resolve => {
|
|
401
|
+
return measureHumanWait(new Promise(resolve => {
|
|
370
402
|
let settled=false;
|
|
371
403
|
const finish = allowed => { if(settled)return;settled=true;signal?.removeEventListener('abort', abort);if(this.permission===question)this.permission = null;this.schedule();resolve(allowed); };
|
|
372
404
|
const abort = () => finish(false);
|
|
@@ -374,7 +406,7 @@ export class TerminalScreen {
|
|
|
374
406
|
const choices = remember?.rule ? ['once', ...(remember.project ? ['project'] : []), 'user', 'no'] : null;
|
|
375
407
|
const question = { name, input, summary, edit: edit && typeof edit.field === 'string' && typeof edit.text === 'string' ? { field: edit.field, text: edit.text } : null, allowed: false, scroll: 0, finish, ...(choices ? { remember, choices, choice: choices.length - 1 } : {}) };
|
|
376
408
|
this.permission = question;signal?.addEventListener('abort', abort, { once: true });if(signal?.aborted)abort();else this.schedule();
|
|
377
|
-
});
|
|
409
|
+
}));
|
|
378
410
|
}
|
|
379
411
|
// Editar antes de enviar: la tarjeta se deniega (no se envía nada) y el texto exacto
|
|
380
412
|
// pasa al composer con una instrucción. El humano lo corrige y lo envía; el modelo
|
|
@@ -403,6 +435,20 @@ export class TerminalScreen {
|
|
|
403
435
|
try { this.composer.insert(text); this.draftChanged(); } catch (e) { this.notice(e.message, true); }
|
|
404
436
|
}
|
|
405
437
|
showBoard(board = this.work) { this.work = board; this.overlay = { kind: 'board', scroll: 0 }; this.schedule(); }
|
|
438
|
+
// Panel /swarm (open-relay, fase 4): la foto llega del host (plugin → ctx.provide → host),
|
|
439
|
+
// sin turnos del modelo. Se limpia al entrar y se vuelve a pintar sola si está a la vista.
|
|
440
|
+
setSwarmView(view) { this.swarmView = normalizeSwarmView(view); if (this.overlay?.kind === 'swarm' || this.swarmStrip) this.schedule(); }
|
|
441
|
+
toggleSwarm() { if (this.overlay?.kind === 'swarm') { this.closeOverlay(); return; } this.overlay = { kind: 'swarm', scroll: 0, swarm: 0, index: 0, follow: true }; this.schedule(); }
|
|
442
|
+
// /timing: one dim line after each final answer; default on, not saved (like /swarm strip).
|
|
443
|
+
setTiming(value = !this.showTiming) { this.showTiming = Boolean(value); this.notice(this.showTiming ? t('Response timing on: a dim line after each answer. /timing off hides it.') : t('Response timing off. /timing on shows it again.')); }
|
|
444
|
+
setSwarmStrip(value = !this.swarmStrip) { this.swarmStrip = Boolean(value); this.notice(this.swarmStrip ? t('Swarm strip on: a compact summary next to the conversation (terminals 110+ columns wide). /swarm strip turns it off.') : t('Swarm strip off.')); }
|
|
445
|
+
// Enter en el panel: prepara el pedido en el composer (no lo envía). El humano lo envía y
|
|
446
|
+
// las acciones siguen en sus tarjetas; Esc recupera el borrador anterior.
|
|
447
|
+
prepareSwarmRequest() {
|
|
448
|
+
const action = swarmActions(this.swarmView, this.overlay.swarm)[this.overlay.index]; if (!action) return;
|
|
449
|
+
if (!this.preparedDraft) this.preparedDraft = { ...this.composer.snapshot(), sessionId: this.metadata.sessionId };
|
|
450
|
+
this.composer.setText(action.request); this.closeOverlay(); this.draftChanged();
|
|
451
|
+
}
|
|
406
452
|
setSelection(value = !this.selecting) {
|
|
407
453
|
if (this.closed || this.suspended || this.permission) return;
|
|
408
454
|
this.selecting = value; clearTimeout(this.renderTimer); this.renderTimer = null;
|
|
@@ -422,7 +468,7 @@ export class TerminalScreen {
|
|
|
422
468
|
if (this.setup) { if (this.setup.kind === 'choice') this.setup.index = Math.max(0, Math.min(this.setupItems().length - 1, this.setup.index + event.delta)); this.schedule(); return; }
|
|
423
469
|
const lines = event.delta * 3;
|
|
424
470
|
if (this.permission) this.permission.scroll = Math.max(0, this.permission.scroll + lines);
|
|
425
|
-
else if (['help', 'board'].includes(this.overlay?.kind)) this.overlay.scroll = Math.max(0, this.overlay.scroll + lines);
|
|
471
|
+
else if (['help', 'board', 'swarm'].includes(this.overlay?.kind)) this.overlay.scroll = Math.max(0, this.overlay.scroll + lines);
|
|
426
472
|
else if (this.overlay?.kind === 'sessions' || this.overlay?.kind === 'queue') {
|
|
427
473
|
const length = this.overlay.kind === 'sessions' ? this.filteredSessions().length : this.queue.items.length;
|
|
428
474
|
this.overlay.index = Math.max(0, Math.min(Math.max(0, length - 1), this.overlay.index + event.delta));
|
|
@@ -487,6 +533,15 @@ export class TerminalScreen {
|
|
|
487
533
|
if (this.hasModalOverlay()) {
|
|
488
534
|
if (name === 'escape') { this.closeOverlay(); return; }
|
|
489
535
|
if (['help', 'board'].includes(this.overlay.kind)) { if (name === 'pageup' || name === 'up') this.overlay.scroll = Math.max(0, this.overlay.scroll - 5); if (name === 'pagedown' || name === 'down') this.overlay.scroll += 5; this.schedule(); return; }
|
|
536
|
+
if (this.overlay.kind === 'swarm') {
|
|
537
|
+
const swarms = this.swarmView?.available ? this.swarmView.swarms.length : 0, actions = swarmActions(this.swarmView, this.overlay.swarm).length;
|
|
538
|
+
if (name === 'up' || name === 'down') { this.overlay.index = Math.max(0, Math.min(Math.max(0, actions - 1), this.overlay.index + (name === 'up' ? -1 : 1))); this.overlay.follow = true; }
|
|
539
|
+
else if (name === 'pageup') this.overlay.scroll = Math.max(0, this.overlay.scroll - 5);
|
|
540
|
+
else if (name === 'pagedown') this.overlay.scroll += 5;
|
|
541
|
+
else if ((name === 'tab' || name === 'right' || name === 'left') && swarms > 1) { this.overlay.swarm = (this.overlay.swarm + (name === 'left' || key.shift ? swarms - 1 : 1)) % swarms; this.overlay.index = 0; this.overlay.scroll = 0; this.overlay.follow = true; }
|
|
542
|
+
else if (name === 'return' && actions) { this.prepareSwarmRequest(); return; }
|
|
543
|
+
this.schedule(); return;
|
|
544
|
+
}
|
|
490
545
|
if (this.overlay.kind === 'sessions') {
|
|
491
546
|
const filtered = this.filteredSessions();
|
|
492
547
|
if (name === 'up') this.overlay.index = Math.max(0, this.overlay.index - 1);
|
|
@@ -564,6 +619,12 @@ export class TerminalScreen {
|
|
|
564
619
|
else output.push('', tone.accent(t(" COMAL · on the comal one cooks")), '', ...intro);
|
|
565
620
|
}
|
|
566
621
|
for (const block of this.blocks) {
|
|
622
|
+
if (block.role === 'timing') {
|
|
623
|
+
// Rendered each frame (language can change); it sits right under the answer.
|
|
624
|
+
const line = this.showTiming ? formatTurnTiming(block.timing, { width: columns - 3 }) : '';
|
|
625
|
+
if (line) { if (output.at(-1) === '') output.pop(); output.push(tone.muted(clip(' ' + line, columns - 1)), ''); }
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
567
628
|
const cacheKey = `${columns}:${this.expandedTools}:${Boolean(this.rawView)}:${block.text || ''}:${block.state || ''}:${block.output || ''}`;
|
|
568
629
|
if (block.cacheKey === cacheKey) { output.push(...block.cached); continue; }
|
|
569
630
|
let lines;
|
|
@@ -612,6 +673,7 @@ export class TerminalScreen {
|
|
|
612
673
|
return (selected ? tone.accent : String)(clip(line, columns));
|
|
613
674
|
}), ...(!items.length ? [tone.muted(t(" No matches. Change section with Tab."))] : [])];
|
|
614
675
|
}
|
|
676
|
+
if (this.overlay?.kind === 'swarm' && !this.permission) return swarmPanelLines(this.swarmView, this.overlay, columns, height, { busy: this.busy });
|
|
615
677
|
if (this.overlay?.kind === 'board' && !this.permission) {
|
|
616
678
|
const board = this.work, lines = [tone.accent(t(" Work · plan v{v0}", {v0: board?.plan?.version || 0})), ...wrap(safeText(board?.plan?.objective || t("No plan. Ask for a task with steps and checks.")), columns - 4).map(line => ' ' + line), tone.muted(t(" /work resume continues · wheel/PgUp/PgDn scrolls · Esc returns")), ''];
|
|
617
679
|
const labels = { pending: t("PENDING"), in_progress: t("IN PROGRESS"), blocked: t("BLOCKED"), done: t("DONE · checks recorded"), needs_verification: t("TO VERIFY") };
|
|
@@ -686,6 +748,7 @@ export class TerminalScreen {
|
|
|
686
748
|
const side = [tone.accent(t(" Queue · {v0}{v1}", {v0: this.queue.items.length, v1: this.queue.paused ? t(" paused") : ''})), tone.muted(t(" F4 manage")), ''];
|
|
687
749
|
if (this.queue.running) side.push(tone.muted(t(" Running")), ...wrap(safeText(this.queue.running.text), sideWidth - 2).slice(0, 2).map(line => ' ' + line), '');
|
|
688
750
|
this.queue.items.slice(0, 6).forEach((item, i) => side.push(tone.accent(` ${i + 1}. `) + clip(safeText(item.text).replace(/\n/g, ' ↵ '), sideWidth - 5), ''));
|
|
751
|
+
if (this.swarmStrip) side.push(...swarmStripLines(this.swarmView, sideWidth));
|
|
689
752
|
body = body.map((line, i) => pad(line, bodyWidth) + tone.muted(' │') + clip(side[i] || '', sideWidth));
|
|
690
753
|
}
|
|
691
754
|
const elapsed = this.startedAt ? ((Date.now() - this.startedAt) / 1000).toFixed(1) : '0.0';
|