@bahulam/code 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/install.mjs +87 -0
- package/src/commands/plugin-manage.mjs +215 -29
- package/src/config/model-catalog-default.json +0 -4
- package/src/core/resume-mode.mjs +0 -1
- package/src/core/stream-client.mjs +18 -0
- package/src/daemon/session-core.mjs +3 -0
- package/src/local-service/agent-relay.mjs +6 -1
- package/src/local-service/file-access.mjs +116 -2
- package/src/local-service/server.mjs +209 -20
- package/src/plugins/npm-install.mjs +138 -0
- package/src/plugins/pi-compat/requirements.mjs +465 -0
- package/src/plugins/pi-compat/scaffold.mjs +86 -10
- package/src/plugins/pi-compat/shim.mjs +29 -1
- package/src/plugins/preflight.mjs +2 -0
- package/src/terminal/main.mjs +2 -1
- package/src/terminal/repl.mjs +52 -23
- package/src/ui/input-dock.mjs +48 -10
- package/src/ui/text-layout.mjs +4 -3
|
@@ -24,20 +24,24 @@ import * as path from 'node:path';
|
|
|
24
24
|
import { COMPOSED_TOOL_SEPARATOR } from '../pi-compose.mjs';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
|
-
* Derive a pack slug from a
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
27
|
+
* Derive a pack slug from a source package name. No forced suffix — a
|
|
28
|
+
* pack can be anything (studio, analyzer, connector, worker, …), and
|
|
29
|
+
* pinning a semantic to the slug guesses wrong most of the time. The
|
|
30
|
+
* default is the source name, sanitized. Author overrides with --slug.
|
|
31
|
+
*
|
|
32
|
+
* pi-web-access → pi-web-access
|
|
33
|
+
* pi-redmine → pi-redmine
|
|
34
|
+
* @ffmpeg/transitions → transitions (scope stripped)
|
|
35
|
+
* filesystem-mcp → filesystem-mcp
|
|
36
|
+
* plain-name → plain-name
|
|
32
37
|
*/
|
|
33
38
|
export function deriveSlug(packageName) {
|
|
34
39
|
let base = String(packageName || '').trim();
|
|
35
40
|
const scoped = base.match(/^@[^/]+\/(.+)$/);
|
|
36
41
|
if (scoped) base = scoped[1];
|
|
37
|
-
base = base.replace(/^pi-/, '');
|
|
38
42
|
base = base.replace(/[^a-z0-9-]+/gi, '-').replace(/^-+|-+$/g, '').toLowerCase();
|
|
39
|
-
if (!base) base = '
|
|
40
|
-
return
|
|
43
|
+
if (!base) base = 'pack';
|
|
44
|
+
return base;
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
/**
|
|
@@ -77,13 +81,75 @@ function truncate(s, n) {
|
|
|
77
81
|
return str.slice(0, n - 1) + '…';
|
|
78
82
|
}
|
|
79
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Compose the "Requirements & constraints" block from the analyzer's
|
|
86
|
+
* findings. Injected into the generated agent's system prompt so the
|
|
87
|
+
* sub-agent knows what its composed tools need — no user teaching
|
|
88
|
+
* required. Falls back to an empty list if requirements is absent (fresh
|
|
89
|
+
* install where the analyzer didn't run, etc.).
|
|
90
|
+
*/
|
|
91
|
+
function requirementsPromptLines(requirements, namespace) {
|
|
92
|
+
if (!requirements) return [];
|
|
93
|
+
const lines = ['', 'Requirements & constraints (from ingredient analysis):'];
|
|
94
|
+
const bins = requirements.system_binaries || [];
|
|
95
|
+
if (bins.length) {
|
|
96
|
+
const names = bins.map(b => b.name).join(', ');
|
|
97
|
+
lines.push(
|
|
98
|
+
`- System binaries required: ${names}. If a tool errors with "ENOENT" or "spawn ${bins[0].name}", tell the user to install them (macOS: \`${bins[0].install_hints?.darwin || 'via brew'}\`; Linux: \`${bins[0].install_hints?.linux || 'via package manager'}\`).`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const creds = (requirements.env_vars || []).filter(v => v.credential);
|
|
102
|
+
if (creds.length) {
|
|
103
|
+
lines.push(
|
|
104
|
+
`- API keys / credentials expected: ${creds.map(v => v.name).join(', ')}. If a tool fails with an auth error, ask the user to set the missing env var.`,
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
if (requirements.workspace_scoped_paths) {
|
|
108
|
+
lines.push(
|
|
109
|
+
'- Paths passed to composed tools MUST be workspace-relative (relative to the current working directory). Absolute paths outside cwd are rejected with "Path is outside the workspace". If the user references an absolute path, ask them to `cd` closer to it or copy the file into the workspace.',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
// Per-tool schema constraints the agent must respect. Emit BOTH required
|
|
113
|
+
// fields AND regex/range constraints — the underlying pi tools throw
|
|
114
|
+
// opaque path/type errors when a required param is missing, and the
|
|
115
|
+
// agent's default reasoning tends to skip params whose descriptions
|
|
116
|
+
// sound "optional" even when the schema marks them required.
|
|
117
|
+
const tc = requirements.tool_constraints || {};
|
|
118
|
+
const toolsWithConstraints = Object.keys(tc).filter(t => Object.keys(tc[t]).length);
|
|
119
|
+
if (toolsWithConstraints.length) {
|
|
120
|
+
lines.push('- Strict input schemas — supply EVERY required field and respect all constraints. Missing a required field usually throws an opaque error like `paths[1] argument must be of type string`:');
|
|
121
|
+
for (const t of toolsWithConstraints) {
|
|
122
|
+
const params = tc[t];
|
|
123
|
+
const required = Object.keys(params).filter(p => params[p].required);
|
|
124
|
+
const regexed = Object.entries(params).filter(([, c]) => c.regex);
|
|
125
|
+
const ranged = Object.entries(params).filter(([, c]) => c.min != null || c.max != null || c.enum);
|
|
126
|
+
if (required.length) {
|
|
127
|
+
lines.push(` - \`${namespace}${COMPOSED_TOOL_SEPARATOR}${t}\` requires: ${required.map(p => `\`${p}\``).join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
for (const [param, c] of regexed) {
|
|
130
|
+
lines.push(` · \`${param}\` must match \`${c.regex}\``);
|
|
131
|
+
}
|
|
132
|
+
for (const [param, c] of ranged) {
|
|
133
|
+
const parts = [];
|
|
134
|
+
if (c.min != null) parts.push(`min ${c.min}`);
|
|
135
|
+
if (c.max != null) parts.push(`max ${c.max}`);
|
|
136
|
+
if (c.enum) parts.push(`one of ${JSON.stringify(c.enum)}`);
|
|
137
|
+
lines.push(` · \`${param}\` ${parts.join(', ')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
// If we detected no external requirements at all, keep the block out so
|
|
142
|
+
// the prompt stays clean.
|
|
143
|
+
return lines.length > 1 ? lines : [];
|
|
144
|
+
}
|
|
145
|
+
|
|
80
146
|
/**
|
|
81
147
|
* Compose an agent system prompt from the pi package + its tools.
|
|
82
148
|
* Focused on WHAT the agent should do, not step-by-step recipes — the
|
|
83
149
|
* generic template can't know the pack's domain. Users are expected to
|
|
84
150
|
* edit the prompt after generation.
|
|
85
151
|
*/
|
|
86
|
-
function generatePrompt(packageName, namespace, toolNames, hasState) {
|
|
152
|
+
function generatePrompt(packageName, namespace, toolNames, hasState, requirements = null) {
|
|
87
153
|
const composed = toolNames.map(t => `${namespace}${COMPOSED_TOOL_SEPARATOR}${t}`);
|
|
88
154
|
const stateLines = hasState
|
|
89
155
|
? [
|
|
@@ -103,6 +169,7 @@ function generatePrompt(packageName, namespace, toolNames, hasState) {
|
|
|
103
169
|
'Available composed tools:',
|
|
104
170
|
...composed.map(t => `- \`${t}\``),
|
|
105
171
|
...stateLines,
|
|
172
|
+
...requirementsPromptLines(requirements, namespace),
|
|
106
173
|
'',
|
|
107
174
|
'Rules:',
|
|
108
175
|
'- Use the composed tools directly — do not describe what you would do, DO it.',
|
|
@@ -453,7 +520,16 @@ export function scaffoldPiPack({
|
|
|
453
520
|
`Specialist agent for ${packageName}. Composes ${toolNames.length} tool${toolNames.length === 1 ? '' : 's'} exposed as ${namespace}${COMPOSED_TOOL_SEPARATOR}*.`,
|
|
454
521
|
240,
|
|
455
522
|
);
|
|
456
|
-
|
|
523
|
+
|
|
524
|
+
// Pull the requirements sidecar the analyzer wrote at install time
|
|
525
|
+
// (may be absent if user is scaffolding manually with an older ingredient).
|
|
526
|
+
let requirements = null;
|
|
527
|
+
const reqSidecar = path.join(piDir, '.bahulam-requirements.json');
|
|
528
|
+
if (fs.existsSync(reqSidecar)) {
|
|
529
|
+
try { requirements = JSON.parse(fs.readFileSync(reqSidecar, 'utf-8')); } catch { /* skip */ }
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const systemPrompt = generatePrompt(packageName, namespace, toolNames, state, requirements);
|
|
457
533
|
|
|
458
534
|
const manifest = renderManifest({
|
|
459
535
|
slug,
|
|
@@ -130,5 +130,33 @@ export function createPiShim({ pluginName = 'pi', captured }) {
|
|
|
130
130
|
},
|
|
131
131
|
};
|
|
132
132
|
|
|
133
|
-
|
|
133
|
+
// Pi's ExtensionAPI is a moving target — packages call methods we haven't
|
|
134
|
+
// stubbed yet (registerMessageRenderer, registerRoute, registerHandler,
|
|
135
|
+
// …). Any unstubbed method call throws, aborting activation before
|
|
136
|
+
// registerTool ever runs, and the probe reports 0 tools.
|
|
137
|
+
//
|
|
138
|
+
// Fall back to a no-op returner for every unknown property so activation
|
|
139
|
+
// reaches its full extent. A tool's runtime call may still fail if it
|
|
140
|
+
// needed that surface — that's an accurate signal at execution time,
|
|
141
|
+
// not a silent black hole at load time.
|
|
142
|
+
return new Proxy(pi, {
|
|
143
|
+
get(target, prop, receiver) {
|
|
144
|
+
if (prop in target) return Reflect.get(target, prop, receiver);
|
|
145
|
+
if (typeof prop === 'symbol') return undefined;
|
|
146
|
+
if (process.env.DEBUG) {
|
|
147
|
+
process.stderr.write(`[pi:${pluginName}] shim: pi.${String(prop)} stubbed (no-op)\n`);
|
|
148
|
+
}
|
|
149
|
+
// Return a callable that also has method access (e.g. pi.foo.bar).
|
|
150
|
+
// Property access on the stub returns another stub, so chains never
|
|
151
|
+
// throw. Result is undefined so anything that reads a return value
|
|
152
|
+
// treats it as "not present" (typeof result === 'undefined').
|
|
153
|
+
const stub = function stub() { return undefined; };
|
|
154
|
+
return new Proxy(stub, {
|
|
155
|
+
get(t, p) {
|
|
156
|
+
if (typeof p === 'symbol') return t[p];
|
|
157
|
+
return stub;
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
});
|
|
134
162
|
}
|
|
@@ -39,6 +39,8 @@ export const RESERVED_TOOL_NAMES = new Set([
|
|
|
39
39
|
// write
|
|
40
40
|
'write_file', 'write_project', 'edit_file', 'delete_file', 'shell',
|
|
41
41
|
'analyze_image', 'generate_image',
|
|
42
|
+
// background jobs (PRD-102 §6.2.3) — long-running renders, builds, etc.
|
|
43
|
+
'job_output', 'job_kill', 'job_status', 'job_list',
|
|
42
44
|
// agent/skill/workflow admin
|
|
43
45
|
'ask_user', 'agent_create', 'agent_sync', 'agents_list',
|
|
44
46
|
'skill_install', 'skill_update', 'skill_remove', 'skill_view', 'skills_list',
|
package/src/terminal/main.mjs
CHANGED
|
@@ -27,6 +27,7 @@ const PLUGIN_MANAGEMENT_COMMANDS = new Set([
|
|
|
27
27
|
'validate', 'check', 'lint',
|
|
28
28
|
'list', 'ls', 'remove', 'rm', 'uninstall',
|
|
29
29
|
'enable', 'disable', 'info', 'update', 'upgrade',
|
|
30
|
+
'doctor',
|
|
30
31
|
]);
|
|
31
32
|
|
|
32
33
|
function parsePluginArgs(argv) {
|
|
@@ -69,7 +70,7 @@ function parsePluginArgs(argv) {
|
|
|
69
70
|
if (arg && (arg.includes('/') || fs.existsSync?.(arg))) parsed.source = arg;
|
|
70
71
|
else parsed.pluginName = arg;
|
|
71
72
|
}
|
|
72
|
-
else if (['info', 'remove', 'rm', 'uninstall', 'enable', 'disable', 'update', 'upgrade'].includes(parsed.action)) {
|
|
73
|
+
else if (['info', 'remove', 'rm', 'uninstall', 'enable', 'disable', 'update', 'upgrade', 'doctor'].includes(parsed.action)) {
|
|
73
74
|
parsed.pluginName = positional.shift() || null;
|
|
74
75
|
}
|
|
75
76
|
} else {
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -184,6 +184,7 @@ import {
|
|
|
184
184
|
moveToContent,
|
|
185
185
|
prepareInputPrompt,
|
|
186
186
|
redrawDockFrame,
|
|
187
|
+
redrawDockInput,
|
|
187
188
|
renderDockInput,
|
|
188
189
|
unmountInputDock,
|
|
189
190
|
} from '../ui/input-dock.mjs';
|
|
@@ -1559,11 +1560,35 @@ function subAgentRunId(data = {}) {
|
|
|
1559
1560
|
return data?.run_id || data?.sub_agent_run_id || null;
|
|
1560
1561
|
}
|
|
1561
1562
|
|
|
1563
|
+
function activeSubAgentRunsMap() {
|
|
1564
|
+
const runs = session.activeSubAgentRuns;
|
|
1565
|
+
if (runs instanceof Map) return runs;
|
|
1566
|
+
|
|
1567
|
+
const restored = new Map();
|
|
1568
|
+
if (Array.isArray(runs)) {
|
|
1569
|
+
for (const item of runs) {
|
|
1570
|
+
if (Array.isArray(item) && item.length >= 2) {
|
|
1571
|
+
restored.set(item[0], item[1]);
|
|
1572
|
+
} else if (item && typeof item === 'object') {
|
|
1573
|
+
const runId = item.runId || item.run_id || item.id;
|
|
1574
|
+
if (runId) restored.set(runId, item);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
} else if (runs && typeof runs === 'object') {
|
|
1578
|
+
for (const [key, value] of Object.entries(runs)) {
|
|
1579
|
+
if (value && typeof value === 'object') restored.set(key, value);
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
session.activeSubAgentRuns = restored;
|
|
1584
|
+
return restored;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1562
1587
|
function normalizeSubAgentRunData(data = {}) {
|
|
1563
1588
|
if (!data || typeof data !== 'object') return data;
|
|
1564
1589
|
const runId = subAgentRunId(data);
|
|
1565
1590
|
if (!runId) return data;
|
|
1566
|
-
const laneRun =
|
|
1591
|
+
const laneRun = activeSubAgentRunsMap().get(runId);
|
|
1567
1592
|
const patch = {};
|
|
1568
1593
|
if (!data.run_id) patch.run_id = runId;
|
|
1569
1594
|
if (laneRun?.type && !data.sub_agent) patch.sub_agent = laneRun.type;
|
|
@@ -1591,7 +1616,7 @@ function ensureFoldedSubAgentTools(agentType, key = agentType, data = {}) {
|
|
|
1591
1616
|
let fold = runtime.foldedSubAgentToolMap.get(key);
|
|
1592
1617
|
if (!fold) {
|
|
1593
1618
|
const runId = subAgentRunId(data);
|
|
1594
|
-
const laneRun = runId ?
|
|
1619
|
+
const laneRun = runId ? activeSubAgentRunsMap().get(runId) : null;
|
|
1595
1620
|
fold = {
|
|
1596
1621
|
key,
|
|
1597
1622
|
runId,
|
|
@@ -1604,7 +1629,7 @@ function ensureFoldedSubAgentTools(agentType, key = agentType, data = {}) {
|
|
|
1604
1629
|
runtime.foldedSubAgentToolMap.set(key, fold);
|
|
1605
1630
|
} else {
|
|
1606
1631
|
const runId = subAgentRunId(data);
|
|
1607
|
-
const laneRun = runId ?
|
|
1632
|
+
const laneRun = runId ? activeSubAgentRunsMap().get(runId) : null;
|
|
1608
1633
|
if (runId && !fold.runId) fold.runId = runId;
|
|
1609
1634
|
if (laneRun) fold.label = subAgentLaneLabel(laneRun);
|
|
1610
1635
|
if (laneRun?.query && !fold.query) fold.query = laneRun.query;
|
|
@@ -1614,8 +1639,8 @@ function ensureFoldedSubAgentTools(agentType, key = agentType, data = {}) {
|
|
|
1614
1639
|
}
|
|
1615
1640
|
|
|
1616
1641
|
function createSubAgentLane(agentType, query, runId, data = {}) {
|
|
1617
|
-
|
|
1618
|
-
const sameTypeOrdinals = [...
|
|
1642
|
+
const activeRuns = activeSubAgentRunsMap();
|
|
1643
|
+
const sameTypeOrdinals = [...activeRuns.values()]
|
|
1619
1644
|
.filter(run => run.type === agentType)
|
|
1620
1645
|
.map(run => Number(run.ordinal || 1));
|
|
1621
1646
|
const ordinal = sameTypeOrdinals.length ? Math.max(...sameTypeOrdinals) + 1 : 1;
|
|
@@ -1631,16 +1656,14 @@ function createSubAgentLane(agentType, query, runId, data = {}) {
|
|
|
1631
1656
|
// the close line agree even for the first run of a batch.
|
|
1632
1657
|
forceOrdinal: Number(data?.parallel_batch) > 1,
|
|
1633
1658
|
};
|
|
1634
|
-
|
|
1659
|
+
activeRuns.set(runId, lane);
|
|
1635
1660
|
ensureFoldedSubAgentTools(agentType, runId, { type: agentType, query, run_id: runId });
|
|
1636
1661
|
_syncSubAgentWindow();
|
|
1637
1662
|
return lane;
|
|
1638
1663
|
}
|
|
1639
1664
|
|
|
1640
1665
|
function activeSubAgentLanes() {
|
|
1641
|
-
return
|
|
1642
|
-
? [...session.activeSubAgentRuns.values()]
|
|
1643
|
-
: [];
|
|
1666
|
+
return [...activeSubAgentRunsMap().values()];
|
|
1644
1667
|
}
|
|
1645
1668
|
|
|
1646
1669
|
function subAgentLaneLabel(lane, lanes = activeSubAgentLanes()) {
|
|
@@ -2322,8 +2345,8 @@ function renderEvent(event) {
|
|
|
2322
2345
|
// concurrent runs of the same type are only distinguishable by the
|
|
2323
2346
|
// backend-issued run_id; label lanes explore#1 / explore#2 when
|
|
2324
2347
|
// more than one run is active. Solo runs render exactly as before.
|
|
2325
|
-
|
|
2326
|
-
const hadActiveRuns =
|
|
2348
|
+
const activeRuns = activeSubAgentRunsMap();
|
|
2349
|
+
const hadActiveRuns = activeRuns.size > 0;
|
|
2327
2350
|
if (!hadActiveRuns) {
|
|
2328
2351
|
stopSpinner();
|
|
2329
2352
|
clearPendingHead();
|
|
@@ -2331,7 +2354,8 @@ function renderEvent(event) {
|
|
|
2331
2354
|
}
|
|
2332
2355
|
const runId = data?.run_id || `${agentType}:${Date.now().toString(36)}`;
|
|
2333
2356
|
const lane = createSubAgentLane(agentType, query, runId, data);
|
|
2334
|
-
const
|
|
2357
|
+
const activeRunsAfterStart = activeSubAgentRunsMap();
|
|
2358
|
+
const parallel = activeRunsAfterStart.size > 1 || lane.forceOrdinal;
|
|
2335
2359
|
const label = subAgentLaneLabel(lane);
|
|
2336
2360
|
renderBlockBoundary('subagent');
|
|
2337
2361
|
process.stderr.write(renderSubAgentOpen({ id: runId, type: label, query, parentDepth: parallel ? 0 : undefined }).replace(/^\n/, '') + '\n');
|
|
@@ -2342,7 +2366,7 @@ function renderEvent(event) {
|
|
|
2342
2366
|
// inner tool calls stream here instead of flooding the transcript.
|
|
2343
2367
|
setSubAgentWindowActive(true);
|
|
2344
2368
|
if (hadActiveRuns) {
|
|
2345
|
-
updateSpinner(`${
|
|
2369
|
+
updateSpinner(`${activeRunsAfterStart.size} agents running`);
|
|
2346
2370
|
} else {
|
|
2347
2371
|
// Phase per sub-agent run: the status line counts elapsed time and
|
|
2348
2372
|
// tool calls live ("plan agent · 4 calls · 32s") for the whole run.
|
|
@@ -2367,16 +2391,17 @@ function renderEvent(event) {
|
|
|
2367
2391
|
if (!eventRunId && hasParallelSubAgentRuns()) {
|
|
2368
2392
|
break;
|
|
2369
2393
|
}
|
|
2370
|
-
const laneRun =
|
|
2394
|
+
const laneRun = activeSubAgentRunsMap().get(eventRunId);
|
|
2371
2395
|
if (laneRun) laneRun.tools++;
|
|
2372
2396
|
foldSubAgentToolProgress(eventData);
|
|
2373
|
-
const
|
|
2397
|
+
const activeRuns = activeSubAgentRunsMap();
|
|
2398
|
+
const laneParallel = activeRuns.size > 1;
|
|
2374
2399
|
if (laneParallel) {
|
|
2375
2400
|
bumpSpinnerProgress();
|
|
2376
2401
|
const activeLanes = activeSubAgentLanes();
|
|
2377
2402
|
const lanes = activeLanes
|
|
2378
2403
|
.map(r => `${subAgentLaneLabel(r, activeLanes)} ${r.tools}`).join(' · ');
|
|
2379
|
-
updateSpinner(`${
|
|
2404
|
+
updateSpinner(`${activeRuns.size} agents · ${lanes}`);
|
|
2380
2405
|
break;
|
|
2381
2406
|
}
|
|
2382
2407
|
// Feed the live window from THIS event — it always fires (55/55 in
|
|
@@ -2433,16 +2458,17 @@ function renderEvent(event) {
|
|
|
2433
2458
|
// Retire this run's display lane; while sibling runs are still
|
|
2434
2459
|
// active keep the shared window/spinner alive for them.
|
|
2435
2460
|
const eventRunId = subAgentRunId(eventData);
|
|
2436
|
-
const
|
|
2461
|
+
const activeRuns = activeSubAgentRunsMap();
|
|
2462
|
+
const doneRun = activeRuns.get(eventRunId);
|
|
2437
2463
|
const doneLabel = doneRun ? subAgentLaneLabel(doneRun) : agentType;
|
|
2438
2464
|
const doneFold = eventRunId ? removeFoldedSubAgentTools(eventRunId) : null;
|
|
2439
2465
|
if (doneFold) {
|
|
2440
2466
|
doneFold.agentType = doneLabel;
|
|
2441
2467
|
doneFold.label = doneLabel;
|
|
2442
2468
|
}
|
|
2443
|
-
if (eventRunId)
|
|
2444
|
-
else
|
|
2445
|
-
const siblingsActive =
|
|
2469
|
+
if (eventRunId) activeRuns.delete(eventRunId);
|
|
2470
|
+
else activeRuns.clear();
|
|
2471
|
+
const siblingsActive = activeRuns.size > 0;
|
|
2446
2472
|
// In verbose mode each sub-agent tool already rendered a full
|
|
2447
2473
|
// transcript card live — flushing the fold would list every tool a
|
|
2448
2474
|
// second time. The fold batch is the durable record ONLY when tools
|
|
@@ -2733,17 +2759,18 @@ function renderEvent(event) {
|
|
|
2733
2759
|
// late events land correctly instead of corrupting fresh state —
|
|
2734
2760
|
// but only for ONE turn boundary: if they're still around at the
|
|
2735
2761
|
// next complete with no closure, force-clean to avoid stale lanes.
|
|
2736
|
-
const
|
|
2762
|
+
const activeRuns = activeSubAgentRunsMap();
|
|
2763
|
+
const lanesLive = activeRuns.size > 0;
|
|
2737
2764
|
if (lanesLive && !session._lanesPreservedAtTurnEnd) {
|
|
2738
2765
|
session._lanesPreservedAtTurnEnd = true;
|
|
2739
|
-
const n =
|
|
2766
|
+
const n = activeRuns.size;
|
|
2740
2767
|
process.stderr.write(` ${c.dim(`${n} agent run${n === 1 ? '' : 's'} still active in background — progress continues below`)}\n`);
|
|
2741
2768
|
} else {
|
|
2742
2769
|
session._lanesPreservedAtTurnEnd = false;
|
|
2743
2770
|
if (showSubAgentTools(getVerbosity())) resetFoldedSubAgentTools();
|
|
2744
2771
|
else flushFoldedSubAgentTools();
|
|
2745
2772
|
resetSubAgents();
|
|
2746
|
-
|
|
2773
|
+
activeRuns.clear();
|
|
2747
2774
|
setSubAgentWindowActive(false);
|
|
2748
2775
|
}
|
|
2749
2776
|
session.inSubAgent = false;
|
|
@@ -4298,11 +4325,13 @@ export async function startTerminalRepl() {
|
|
|
4298
4325
|
options: Array.isArray(req?.options) ? req.options : [],
|
|
4299
4326
|
context: req?.context || '',
|
|
4300
4327
|
});
|
|
4328
|
+
if (isInputDockMounted()) redrawDockInput();
|
|
4301
4329
|
if (res?.answer) {
|
|
4302
4330
|
process.stderr.write(` ${c.green('✓')} ${c.dim('answered:')} ${c.brand(res.answer)}\n`);
|
|
4303
4331
|
} else {
|
|
4304
4332
|
process.stderr.write(` ${c.dim('Question declined — agent proceeds with its own judgment.')}\n`);
|
|
4305
4333
|
}
|
|
4334
|
+
if (isInputDockMounted()) redrawDockInput();
|
|
4306
4335
|
return res;
|
|
4307
4336
|
};
|
|
4308
4337
|
|
package/src/ui/input-dock.mjs
CHANGED
|
@@ -518,6 +518,21 @@ function drawInputLines(lines) {
|
|
|
518
518
|
// No save/restore — caller parks cursor via focusDockInput.
|
|
519
519
|
}
|
|
520
520
|
|
|
521
|
+
function terminalCellWidthFromColumn(text, startColumn = 1) {
|
|
522
|
+
let col = Math.max(1, Math.floor(Number(startColumn) || 1));
|
|
523
|
+
const start = col;
|
|
524
|
+
for (const ch of queue.stripSequences(String(text ?? ''))) {
|
|
525
|
+
const cp = ch.codePointAt(0);
|
|
526
|
+
if (cp === 0x09) {
|
|
527
|
+
col = (Math.floor((col - 1) / 8) + 1) * 8 + 1;
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (cp === 0x0a || cp === 0x0d) continue;
|
|
531
|
+
col += queue.cellWidth(ch);
|
|
532
|
+
}
|
|
533
|
+
return Math.max(0, col - start);
|
|
534
|
+
}
|
|
535
|
+
|
|
521
536
|
export function isInputDockMounted() {
|
|
522
537
|
return mounted;
|
|
523
538
|
}
|
|
@@ -650,6 +665,20 @@ export function redrawDockFrame() {
|
|
|
650
665
|
return true;
|
|
651
666
|
}
|
|
652
667
|
|
|
668
|
+
export function redrawDockInput() {
|
|
669
|
+
if (!mounted) return false;
|
|
670
|
+
contentTrackingActive = false;
|
|
671
|
+
renderFrame(lastFrame);
|
|
672
|
+
if (Array.isArray(lastFrame.overlayLines)) {
|
|
673
|
+
drawInputLines(lastFrame.overlayLines);
|
|
674
|
+
} else {
|
|
675
|
+
const layout = layoutInput(lastFrame.prefix, lastFrame.value);
|
|
676
|
+
drawInputLines(layout.lines);
|
|
677
|
+
}
|
|
678
|
+
parkCursorAtInput();
|
|
679
|
+
return true;
|
|
680
|
+
}
|
|
681
|
+
|
|
653
682
|
export function prepareInputPrompt({ context = '', tips = '', meta = '' } = {}) {
|
|
654
683
|
if (!mounted) return false;
|
|
655
684
|
contentTrackingActive = false;
|
|
@@ -725,28 +754,35 @@ export function renderDockOverlay({
|
|
|
725
754
|
export function focusDockInput(prefix, value = '', cursorInValue = null) {
|
|
726
755
|
if (!mounted) return false;
|
|
727
756
|
contentTrackingActive = false;
|
|
757
|
+
const target = cursorTargetForInput(prefix, value, cursorInValue);
|
|
758
|
+
if (queue.isActive()) {
|
|
759
|
+
// Record the park position — every queue op re-parks here so readline
|
|
760
|
+
// echoes always land in the input row, even mid-stream.
|
|
761
|
+
queue.park(target.row, target.col);
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
moveTo(target.row, target.col);
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function cursorTargetForInput(prefix, value = '', cursorInValue = null) {
|
|
728
769
|
const layout = layoutInput(prefix, value);
|
|
729
770
|
const valueStr = String(value || '');
|
|
730
771
|
const rawCursor = cursorInValue == null
|
|
731
772
|
? valueStr.length
|
|
732
773
|
: Math.max(0, Math.min(valueStr.length, Math.floor(cursorInValue)));
|
|
733
774
|
const cursorSlice = valueStr.slice(0, rawCursor);
|
|
734
|
-
const
|
|
735
|
-
const
|
|
775
|
+
const inputColumn = INPUT_INDENT + 1;
|
|
776
|
+
const measureInputLine = (line) => terminalCellWidthFromColumn(line, inputColumn);
|
|
777
|
+
const offset = terminalCellWidthFromColumn(`${prefix || ''}${cursorSlice}`, inputColumn);
|
|
778
|
+
const pos = cursorPositionInLines(layout.wrapped, offset, measureInputLine);
|
|
736
779
|
const visibleRowIdx = Math.max(
|
|
737
780
|
0,
|
|
738
781
|
Math.min(inputRows - 1, pos.row - Math.max(0, layout.wrapped.length - inputRows)),
|
|
739
782
|
);
|
|
740
783
|
const row = inputRowStart() + visibleRowIdx;
|
|
741
784
|
const col = Math.min(cols(), INPUT_INDENT + 1 + Math.max(0, pos.col));
|
|
742
|
-
|
|
743
|
-
// Record the park position — every queue op re-parks here so readline
|
|
744
|
-
// echoes always land in the input row, even mid-stream.
|
|
745
|
-
queue.park(row, col);
|
|
746
|
-
return true;
|
|
747
|
-
}
|
|
748
|
-
moveTo(row, col);
|
|
749
|
-
return true;
|
|
785
|
+
return { row, col };
|
|
750
786
|
}
|
|
751
787
|
|
|
752
788
|
export function inputRowColumn() {
|
|
@@ -766,6 +802,8 @@ export function _internals() {
|
|
|
766
802
|
drawableColumns,
|
|
767
803
|
resetContentCursor,
|
|
768
804
|
contentCursor: () => ({ row: contentCursorRow, col: contentCursorCol, active: contentTrackingActive }),
|
|
805
|
+
cursorTargetForInput,
|
|
806
|
+
terminalCellWidthFromColumn,
|
|
769
807
|
overlayRowsForWrapped,
|
|
770
808
|
FIXED_ROWS,
|
|
771
809
|
MAX_INPUT_ROWS_CAP,
|
package/src/ui/text-layout.mjs
CHANGED
|
@@ -76,19 +76,20 @@ export function tailWithEllipsis(lines, maxRows, ellipsis = '… ') {
|
|
|
76
76
|
*
|
|
77
77
|
* Used to place the terminal cursor after rendering the input buffer.
|
|
78
78
|
*/
|
|
79
|
-
export function cursorPositionInLines(visibleLines, offset) {
|
|
79
|
+
export function cursorPositionInLines(visibleLines, offset, measure = visibleWidth) {
|
|
80
80
|
const arr = Array.isArray(visibleLines) ? visibleLines : [];
|
|
81
|
+
const widthOf = typeof measure === 'function' ? measure : visibleWidth;
|
|
81
82
|
let remaining = Math.max(0, Math.floor(offset));
|
|
82
83
|
for (let row = 0; row < arr.length; row++) {
|
|
83
84
|
const line = arr[row] || '';
|
|
84
|
-
const w =
|
|
85
|
+
const w = widthOf(line);
|
|
85
86
|
if (remaining <= w) return { row, col: remaining };
|
|
86
87
|
remaining -= w;
|
|
87
88
|
// Newline between wrapped lines doesn't count as a visible column,
|
|
88
89
|
// but consumes zero of the remaining offset either.
|
|
89
90
|
}
|
|
90
91
|
const lastRow = Math.max(0, arr.length - 1);
|
|
91
|
-
return { row: lastRow, col:
|
|
92
|
+
return { row: lastRow, col: widthOf(arr[lastRow] || '') };
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
// ── internals ────────────────────────────────────────────────────────────
|