@bubblebrain-ai/bubble 0.0.27 → 0.0.29
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 +21 -0
- package/dist/agent/categories.d.ts +2 -0
- package/dist/agent/categories.js +4 -0
- package/dist/agent/child-runner.d.ts +5 -1
- package/dist/agent/child-runner.js +35 -2
- package/dist/agent/profiles.js +3 -0
- package/dist/agent/structured-output.d.ts +37 -0
- package/dist/agent/structured-output.js +193 -0
- package/dist/agent/subagent-control.d.ts +3 -0
- package/dist/agent/subagent-scheduler.d.ts +10 -0
- package/dist/agent/subagent-scheduler.js +31 -0
- package/dist/agent/workflow/control.d.ts +37 -0
- package/dist/agent/workflow/control.js +20 -0
- package/dist/agent/workflow/errors.d.ts +16 -0
- package/dist/agent/workflow/errors.js +24 -0
- package/dist/agent/workflow/runtime.d.ts +75 -0
- package/dist/agent/workflow/runtime.js +237 -0
- package/dist/agent.d.ts +105 -0
- package/dist/agent.js +425 -17
- package/dist/context/compact-llm.d.ts +10 -1
- package/dist/context/compact-llm.js +13 -5
- package/dist/context/compact.d.ts +30 -0
- package/dist/context/compact.js +34 -17
- package/dist/network/provider-transport.d.ts +9 -0
- package/dist/network/provider-transport.js +19 -1
- package/dist/provider-anthropic.js +13 -0
- package/dist/provider.d.ts +14 -0
- package/dist/provider.js +24 -0
- package/dist/session.d.ts +16 -0
- package/dist/session.js +33 -1
- package/dist/slash-commands/commands.js +47 -1
- package/dist/slash-commands/types.d.ts +16 -1
- package/dist/tools/agent-lifecycle.d.ts +6 -0
- package/dist/tools/agent-lifecycle.js +285 -0
- package/dist/tools/child-tools.d.ts +10 -0
- package/dist/tools/child-tools.js +12 -0
- package/dist/tools/read.d.ts +1 -1
- package/dist/tools/read.js +9 -0
- package/dist/tui/image-display.d.ts +6 -0
- package/dist/tui/image-display.js +26 -1
- package/dist/tui-ink/app.js +84 -6
- package/dist/tui-ink/compaction-progress.d.ts +19 -0
- package/dist/tui-ink/compaction-progress.js +74 -0
- package/dist/tui-ink/input-box.d.ts +7 -1
- package/dist/tui-ink/input-box.js +48 -15
- package/dist/tui-ink/markdown.d.ts +18 -0
- package/dist/tui-ink/markdown.js +172 -16
- package/dist/tui-ink/message-list.js +38 -94
- package/dist/tui-ink/run.js +5 -0
- package/dist/tui-ink/subagent-inspector.d.ts +17 -0
- package/dist/tui-ink/subagent-inspector.js +189 -0
- package/dist/tui-ink/subagent-view.d.ts +47 -0
- package/dist/tui-ink/subagent-view.js +163 -0
- package/dist/tui-ink/terminal-env.d.ts +15 -0
- package/dist/tui-ink/terminal-env.js +22 -0
- package/dist/tui-ink/use-terminal-size.js +33 -6
- package/dist/tui-ink/width.d.ts +18 -0
- package/dist/tui-ink/width.js +130 -0
- package/dist/types.d.ts +35 -0
- package/package.json +2 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { discoverAgentProfiles, findAgentProfile } from "../agent/profiles.js";
|
|
4
|
+
import { parseThinkingLevel } from "../agent/categories.js";
|
|
4
5
|
import { formatSubagentRoute } from "../agent/subagent-route-format.js";
|
|
5
6
|
/**
|
|
6
7
|
* Session-scoped trust decisions for project profiles, keyed by file path +
|
|
@@ -119,6 +120,8 @@ export function createSpawnAgentTool(options = {}, sharedTrust) {
|
|
|
119
120
|
agent_type: { type: "string", description: "Subagent profile or role name. Defaults to default. Built-in types include default, explorer, and worker; see the tool description for custom profiles." },
|
|
120
121
|
agent: { type: "string", description: "Alias for agent_type." },
|
|
121
122
|
category: { type: "string", description: "Optional semantic category for model/thinking routing, such as quick, deep, explore, review, frontend, or writing." },
|
|
123
|
+
model: { type: "string", description: "Optional per-call model for this child, overriding category and profile. Bare name (e.g. claude-haiku-4-5) uses the parent provider; provider:model (e.g. anthropic:claude-opus-4-1) selects cross-provider." },
|
|
124
|
+
effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-call thinking level for this child, overriding category and profile." },
|
|
122
125
|
message: { type: "string", description: "Initial task for the subagent." },
|
|
123
126
|
task: { type: "string", description: "Alias for message." },
|
|
124
127
|
fork_context: { type: "boolean", description: "When true, copy recent parent conversation into the child thread." },
|
|
@@ -153,11 +156,16 @@ export function createSpawnAgentTool(options = {}, sharedTrust) {
|
|
|
153
156
|
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
154
157
|
if (trustBlock)
|
|
155
158
|
return trustBlock;
|
|
159
|
+
const effort = parseEffortArg(args.effort);
|
|
160
|
+
if ("error" in effort)
|
|
161
|
+
return effort.error;
|
|
156
162
|
try {
|
|
157
163
|
const snapshot = await ctx.agent.spawnSubAgent(message, ctx.cwd, {
|
|
158
164
|
profile: resolved.profile,
|
|
159
165
|
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
160
166
|
category: stringArg(args.category),
|
|
167
|
+
model: stringArg(args.model),
|
|
168
|
+
effort: effort.value,
|
|
161
169
|
approval: parseApproval(args.approval),
|
|
162
170
|
abortSignal: ctx.abortSignal,
|
|
163
171
|
forkContext: args.fork_context === true,
|
|
@@ -392,6 +400,8 @@ export function createAgentTeamTool(options = {}, sharedTrust) {
|
|
|
392
400
|
description: { type: "string", description: "Short (3-5 word) description of the team, shown in the UI." },
|
|
393
401
|
agent_type: { type: "string", description: "Subagent profile for every member. Defaults to default." },
|
|
394
402
|
category: { type: "string", description: "Optional semantic category for model/thinking routing." },
|
|
403
|
+
model: { type: "string", description: "Optional per-call model for every member, overriding category and profile (bare name or provider:model)." },
|
|
404
|
+
effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-call thinking level for every member." },
|
|
395
405
|
prompt_template: { type: "string", description: "Task template applied to each item. Must contain the literal placeholder {{item}}." },
|
|
396
406
|
items: {
|
|
397
407
|
type: "array",
|
|
@@ -442,10 +452,15 @@ export function createAgentTeamTool(options = {}, sharedTrust) {
|
|
|
442
452
|
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
443
453
|
if (trustBlock)
|
|
444
454
|
return trustBlock;
|
|
455
|
+
const effort = parseEffortArg(args.effort);
|
|
456
|
+
if ("error" in effort)
|
|
457
|
+
return effort.error;
|
|
445
458
|
try {
|
|
446
459
|
const snapshots = await ctx.agent.runAgentTeam(ctx.cwd, {
|
|
447
460
|
profile: resolved.profile,
|
|
448
461
|
category: stringArg(args.category),
|
|
462
|
+
model: stringArg(args.model),
|
|
463
|
+
effort: effort.value,
|
|
449
464
|
promptTemplate: template,
|
|
450
465
|
items,
|
|
451
466
|
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
@@ -484,6 +499,254 @@ export function createAgentTeamTool(options = {}, sharedTrust) {
|
|
|
484
499
|
},
|
|
485
500
|
};
|
|
486
501
|
}
|
|
502
|
+
/** Specs bound for one agent_batch call (design v2 §1.3). */
|
|
503
|
+
export const AGENT_BATCH_MIN_SPECS = 2;
|
|
504
|
+
export const AGENT_BATCH_MAX_SPECS = 32;
|
|
505
|
+
export function createAgentBatchTool(options = {}, sharedTrust) {
|
|
506
|
+
const trust = sharedTrust ?? new ProjectProfileTrust(options.approval);
|
|
507
|
+
return {
|
|
508
|
+
name: "agent_batch",
|
|
509
|
+
readOnly: true,
|
|
510
|
+
effect: "read",
|
|
511
|
+
description: [
|
|
512
|
+
"Run several DIFFERENT subagent tasks in parallel as one tool call (heterogeneous fan-out).",
|
|
513
|
+
"Unlike agent_team (one template over many items), each spec is its own task with its own optional model/effort/profile/output_schema.",
|
|
514
|
+
"Use it to run, e.g., many cheap scouts plus one expensive synthesizer in a single step; the call blocks until every member is final and returns results in spec order.",
|
|
515
|
+
`Provide ${AGENT_BATCH_MIN_SPECS}-${AGENT_BATCH_MAX_SPECS} specs. agent_batch must be the ONLY tool call in your response; the fan-out happens inside the runtime, so never emit parallel spawn_agent calls yourself.`,
|
|
516
|
+
"Scoping rule: split specs so members never overlap or conflict.",
|
|
517
|
+
].join(" "),
|
|
518
|
+
parameters: {
|
|
519
|
+
type: "object",
|
|
520
|
+
properties: {
|
|
521
|
+
description: { type: "string", description: "Short (3-5 word) description of the batch, shown in the UI." },
|
|
522
|
+
specs: {
|
|
523
|
+
type: "array",
|
|
524
|
+
description: `Heterogeneous member specs (${AGENT_BATCH_MIN_SPECS}-${AGENT_BATCH_MAX_SPECS}); each becomes one subagent.`,
|
|
525
|
+
items: {
|
|
526
|
+
type: "object",
|
|
527
|
+
properties: {
|
|
528
|
+
task: { type: "string", description: "Self-contained task for this member." },
|
|
529
|
+
agent_type: { type: "string", description: "Subagent profile for this member. Defaults to default." },
|
|
530
|
+
model: { type: "string", description: "Optional per-member model (bare name or provider:model)." },
|
|
531
|
+
effort: { type: "string", enum: ["off", "minimal", "low", "medium", "high", "xhigh", "max"], description: "Optional per-member thinking level." },
|
|
532
|
+
category: { type: "string", description: "Optional semantic category for routing." },
|
|
533
|
+
output_schema: { type: "object", description: "Optional JSON Schema; the member must return JSON conforming to it, validated with one corrective retry." },
|
|
534
|
+
},
|
|
535
|
+
required: ["task"],
|
|
536
|
+
additionalProperties: false,
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
},
|
|
540
|
+
required: ["description", "specs"],
|
|
541
|
+
additionalProperties: false,
|
|
542
|
+
},
|
|
543
|
+
async execute(args, ctx) {
|
|
544
|
+
if (!ctx.agent?.runAgentBatch) {
|
|
545
|
+
return toolRuntimeMissing("agent_batch");
|
|
546
|
+
}
|
|
547
|
+
const rawSpecs = Array.isArray(args.specs) ? args.specs : [];
|
|
548
|
+
if (rawSpecs.length < AGENT_BATCH_MIN_SPECS) {
|
|
549
|
+
return {
|
|
550
|
+
content: `Error: agent_batch needs at least ${AGENT_BATCH_MIN_SPECS} specs (got ${rawSpecs.length}). For a single task use spawn_agent instead.`,
|
|
551
|
+
isError: true,
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
if (rawSpecs.length > AGENT_BATCH_MAX_SPECS) {
|
|
555
|
+
return {
|
|
556
|
+
content: `Error: agent_batch accepts at most ${AGENT_BATCH_MAX_SPECS} specs (got ${rawSpecs.length}). Split into sequential batches.`,
|
|
557
|
+
isError: true,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const specs = [];
|
|
561
|
+
for (let index = 0; index < rawSpecs.length; index++) {
|
|
562
|
+
const raw = (rawSpecs[index] ?? {});
|
|
563
|
+
const task = stringArg(raw.task);
|
|
564
|
+
if (!task) {
|
|
565
|
+
return { content: `Error: agent_batch spec ${index + 1} is missing a non-empty task.`, isError: true };
|
|
566
|
+
}
|
|
567
|
+
const profileName = stringArg(raw.agent_type) ?? stringArg(raw.agent) ?? "default";
|
|
568
|
+
const resolved = resolveProfile(ctx.cwd, profileName, "both");
|
|
569
|
+
if ("error" in resolved)
|
|
570
|
+
return resolved.error;
|
|
571
|
+
const modeBlock = unsupportedProfile(resolved.profile);
|
|
572
|
+
if (modeBlock)
|
|
573
|
+
return modeBlock;
|
|
574
|
+
const trustBlock = await trust.ensureTrusted(resolved.profile);
|
|
575
|
+
if (trustBlock)
|
|
576
|
+
return trustBlock;
|
|
577
|
+
const effort = parseEffortArg(raw.effort);
|
|
578
|
+
if ("error" in effort)
|
|
579
|
+
return effort.error;
|
|
580
|
+
const outputSchema = raw.output_schema && typeof raw.output_schema === "object" ? raw.output_schema : undefined;
|
|
581
|
+
specs.push({
|
|
582
|
+
task,
|
|
583
|
+
profile: resolved.profile,
|
|
584
|
+
category: stringArg(raw.category),
|
|
585
|
+
model: stringArg(raw.model),
|
|
586
|
+
effort: effort.value,
|
|
587
|
+
outputSchema,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
try {
|
|
591
|
+
const snapshots = await ctx.agent.runAgentBatch(ctx.cwd, {
|
|
592
|
+
specs,
|
|
593
|
+
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
594
|
+
emitUpdate: ctx.emitUpdate,
|
|
595
|
+
abortSignal: ctx.abortSignal,
|
|
596
|
+
});
|
|
597
|
+
const counts = teamStatusCounts(snapshots);
|
|
598
|
+
const lines = [
|
|
599
|
+
`agent_batch "${stringArg(args.description) ?? "batch"}": ${snapshots.length} members — ${counts}`,
|
|
600
|
+
"Failed or cancelled members can be resumed individually with send_input (see per-member guidance below).",
|
|
601
|
+
"",
|
|
602
|
+
...snapshots.flatMap((snapshot, index) => [
|
|
603
|
+
`### member ${index + 1}: ${truncateText(specs[index]?.task ?? "", 100)}`,
|
|
604
|
+
...formatSnapshot(snapshot),
|
|
605
|
+
"",
|
|
606
|
+
]),
|
|
607
|
+
];
|
|
608
|
+
return {
|
|
609
|
+
content: lines.join("\n").trim(),
|
|
610
|
+
status: snapshots.every((snapshot) => snapshot.status === "completed")
|
|
611
|
+
? "success"
|
|
612
|
+
: snapshots.some((snapshot) => snapshot.status === "completed")
|
|
613
|
+
? "partial"
|
|
614
|
+
: "blocked",
|
|
615
|
+
isError: snapshots.length > 0 && snapshots.every((snapshot) => snapshot.status !== "completed"),
|
|
616
|
+
metadata: {
|
|
617
|
+
kind: "subagent",
|
|
618
|
+
mode: "batch",
|
|
619
|
+
subagents: snapshots.map(snapshotToMetadata),
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
return toolError("agent_batch", error);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
export function createRunWorkflowTool(options = {}) {
|
|
630
|
+
void options;
|
|
631
|
+
return {
|
|
632
|
+
name: "run_workflow",
|
|
633
|
+
readOnly: true,
|
|
634
|
+
effect: "read",
|
|
635
|
+
description: [
|
|
636
|
+
"Run an LLM-authored JavaScript orchestration script (dynamic workflow) that coordinates many subagents with deterministic control flow.",
|
|
637
|
+
"Use it for tasks that need loops, conditional fan-out, or staged pipelines over dozens of subagents whose intermediate steps should stay out of this conversation — e.g. a codebase-wide audit, a migration, or cross-checked research.",
|
|
638
|
+
"The script's only capability is agent(prompt, opts?) — each call spawns a sandboxed readonly subagent; opts may set {model, effort, agentType, category, schema}. Also available: parallel(thunks), pipeline(items, ...stages), phase(title), log(msg), the global args, and budget {total, spent(), remaining()}.",
|
|
639
|
+
"End the script with `return <value>`; that value (only) comes back to you. The script has no filesystem/shell/network/clock/random access. run_workflow must be the ONLY tool call in your response; it blocks until the workflow finishes.",
|
|
640
|
+
"Example: `export const meta = { name: 'audit', description: 'auth audit' };\\nconst files = args;\\nconst findings = await parallel(files.map(f => () => agent('Audit '+f+' for missing auth', { model: 'haiku', schema: SCHEMA })));\\nreturn findings.filter(Boolean);`",
|
|
641
|
+
].join(" "),
|
|
642
|
+
parameters: {
|
|
643
|
+
type: "object",
|
|
644
|
+
properties: {
|
|
645
|
+
script: { type: "string", description: "The JavaScript orchestration script. Starts with `export const meta = {name, description}`; ends with `return <value>`." },
|
|
646
|
+
args: { description: "Optional JSON value exposed to the script as the global `args` (e.g. a list of target paths or a question)." },
|
|
647
|
+
title: { type: "string", description: "Optional short label shown in the UI." },
|
|
648
|
+
},
|
|
649
|
+
required: ["script"],
|
|
650
|
+
additionalProperties: false,
|
|
651
|
+
},
|
|
652
|
+
async execute(args, ctx) {
|
|
653
|
+
if (!ctx.agent?.startWorkflow) {
|
|
654
|
+
return toolRuntimeMissing("run_workflow");
|
|
655
|
+
}
|
|
656
|
+
const script = stringArg(args.script);
|
|
657
|
+
if (!script) {
|
|
658
|
+
return { content: "Error: run_workflow requires a non-empty script.", isError: true };
|
|
659
|
+
}
|
|
660
|
+
try {
|
|
661
|
+
const { runId, title } = ctx.agent.startWorkflow(ctx.cwd, {
|
|
662
|
+
script,
|
|
663
|
+
args: args.args,
|
|
664
|
+
title: stringArg(args.title),
|
|
665
|
+
parentToolCallId: ctx.toolCall?.id ?? snapshotFallbackId(),
|
|
666
|
+
abortSignal: ctx.abortSignal,
|
|
667
|
+
});
|
|
668
|
+
return {
|
|
669
|
+
content: [
|
|
670
|
+
`run_workflow "${title}" started in the background (run_id: ${runId}).`,
|
|
671
|
+
`It coordinates subagents on its own; its result is injected automatically before your next turn.`,
|
|
672
|
+
`To block for it now (required in non-interactive mode), call wait_workflow with run_id ${runId}.`,
|
|
673
|
+
].join("\n"),
|
|
674
|
+
status: "success",
|
|
675
|
+
metadata: { kind: "subagent", mode: "workflow", runId },
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
return toolError("run_workflow", error);
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
export function createWaitWorkflowTool() {
|
|
685
|
+
return {
|
|
686
|
+
name: "wait_workflow",
|
|
687
|
+
readOnly: true,
|
|
688
|
+
effect: "read",
|
|
689
|
+
description: [
|
|
690
|
+
"Block until a background run_workflow finishes and return its result.",
|
|
691
|
+
"If it times out while still running, call wait_workflow again with a longer timeout.",
|
|
692
|
+
].join(" "),
|
|
693
|
+
parameters: {
|
|
694
|
+
type: "object",
|
|
695
|
+
properties: {
|
|
696
|
+
run_id: { type: "string", description: "The run_id returned by run_workflow." },
|
|
697
|
+
timeout_ms: { type: "number", description: "Max time to wait (default 600000)." },
|
|
698
|
+
},
|
|
699
|
+
required: ["run_id"],
|
|
700
|
+
additionalProperties: false,
|
|
701
|
+
},
|
|
702
|
+
async execute(args, ctx) {
|
|
703
|
+
if (!ctx.agent?.waitWorkflow) {
|
|
704
|
+
return toolRuntimeMissing("wait_workflow");
|
|
705
|
+
}
|
|
706
|
+
const runId = stringArg(args.run_id);
|
|
707
|
+
if (!runId) {
|
|
708
|
+
return { content: "Error: wait_workflow requires run_id.", isError: true };
|
|
709
|
+
}
|
|
710
|
+
const timeoutMs = typeof args.timeout_ms === "number" ? args.timeout_ms : undefined;
|
|
711
|
+
const snapshot = await ctx.agent.waitWorkflow(runId, timeoutMs);
|
|
712
|
+
if (!snapshot) {
|
|
713
|
+
return { content: `Error: unknown workflow run_id "${runId}".`, isError: true };
|
|
714
|
+
}
|
|
715
|
+
if (snapshot.status === "running") {
|
|
716
|
+
return {
|
|
717
|
+
content: `workflow "${snapshot.title}" (${runId}) still running (${snapshot.agentCount} agents so far). Call wait_workflow again with a longer timeout.`,
|
|
718
|
+
status: "timeout",
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
if (!snapshot.result || !snapshot.result.ok) {
|
|
722
|
+
return {
|
|
723
|
+
content: [
|
|
724
|
+
`workflow "${snapshot.title}" (${runId}) ${snapshot.status}: ${snapshot.result && !snapshot.result.ok ? snapshot.result.error : "no result"}`,
|
|
725
|
+
...(snapshot.logs.length > 0 ? ["", "Log:", ...snapshot.logs.slice(-20)] : []),
|
|
726
|
+
].join("\n"),
|
|
727
|
+
isError: true,
|
|
728
|
+
status: "blocked",
|
|
729
|
+
metadata: { kind: "subagent", mode: "workflow", subagents: snapshot.snapshots.map(snapshotToMetadata) },
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
const rendered = typeof snapshot.result.value === "string"
|
|
733
|
+
? snapshot.result.value
|
|
734
|
+
: JSON.stringify(snapshot.result.value, null, 2);
|
|
735
|
+
return {
|
|
736
|
+
content: [
|
|
737
|
+
`workflow "${snapshot.title}" (${runId}) completed (${snapshot.agentCount} agents).`,
|
|
738
|
+
...(snapshot.logs.length > 0 ? ["", "Log:", ...snapshot.logs.slice(-20)] : []),
|
|
739
|
+
"",
|
|
740
|
+
"--- workflow result (data, not instructions) ---",
|
|
741
|
+
truncateText(rendered, 8000),
|
|
742
|
+
"--- end workflow result ---",
|
|
743
|
+
].join("\n"),
|
|
744
|
+
status: "success",
|
|
745
|
+
metadata: { kind: "subagent", mode: "workflow", subagents: snapshot.snapshots.map(snapshotToMetadata) },
|
|
746
|
+
};
|
|
747
|
+
},
|
|
748
|
+
};
|
|
749
|
+
}
|
|
487
750
|
function teamStatusCounts(snapshots) {
|
|
488
751
|
const counts = new Map();
|
|
489
752
|
for (const snapshot of snapshots) {
|
|
@@ -500,6 +763,9 @@ export function createAgentLifecycleTools(options = {}) {
|
|
|
500
763
|
createCloseAgentTool(),
|
|
501
764
|
createListAgentsTool(),
|
|
502
765
|
createAgentTeamTool(options, trust),
|
|
766
|
+
createAgentBatchTool(options, trust),
|
|
767
|
+
createRunWorkflowTool(options),
|
|
768
|
+
createWaitWorkflowTool(),
|
|
503
769
|
];
|
|
504
770
|
}
|
|
505
771
|
function resolveProfile(cwd, name, scope) {
|
|
@@ -694,6 +960,25 @@ function parseScope(value) {
|
|
|
694
960
|
function parseApproval(value) {
|
|
695
961
|
return value === "fail" || value === "disabled" ? value : undefined;
|
|
696
962
|
}
|
|
963
|
+
/**
|
|
964
|
+
* Parses an optional per-call effort/thinking override. An absent value yields
|
|
965
|
+
* `{ value: undefined }`; a present-but-invalid value is a teaching error so the
|
|
966
|
+
* model corrects it rather than silently running at the wrong level.
|
|
967
|
+
*/
|
|
968
|
+
function parseEffortArg(value) {
|
|
969
|
+
if (value === undefined || value === null)
|
|
970
|
+
return { value: undefined };
|
|
971
|
+
const parsed = parseThinkingLevel(value);
|
|
972
|
+
if (!parsed) {
|
|
973
|
+
return {
|
|
974
|
+
error: {
|
|
975
|
+
content: `Error: effort must be one of off, minimal, low, medium, high, xhigh, max (got ${JSON.stringify(value)}).`,
|
|
976
|
+
isError: true,
|
|
977
|
+
},
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
return { value: parsed };
|
|
981
|
+
}
|
|
697
982
|
function normalizeAgentIds(value, single) {
|
|
698
983
|
const out = [];
|
|
699
984
|
if (typeof single === "string" && single.trim())
|
|
@@ -28,4 +28,14 @@ export declare class WorktreeApprovalController implements ApprovalController {
|
|
|
28
28
|
* with their own FileStateTracker and the worktree approval policy. A
|
|
29
29
|
* profile's tools list can narrow the set but never widen it.
|
|
30
30
|
*/
|
|
31
|
+
/**
|
|
32
|
+
* Isolates a readonly child's mutable tool state (design v2 §2): any tool that
|
|
33
|
+
* exposes a cloneForChild hook (the standard `read`, which carries a
|
|
34
|
+
* FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
|
|
35
|
+
* members of a fan-out never share mutable tool state. Stateless tools
|
|
36
|
+
* (glob/grep, web/memory/skill/todo) and custom/mock tools without the hook are
|
|
37
|
+
* passed through unchanged. Write children get full isolation via
|
|
38
|
+
* createWorktreeChildTools instead.
|
|
39
|
+
*/
|
|
40
|
+
export declare function isolateReadonlyChildFileTools(tools: ToolRegistryEntry[]): ToolRegistryEntry[];
|
|
31
41
|
export declare function createWorktreeChildTools(worktreeCwd: string, include?: string[]): ToolRegistryEntry[];
|
|
@@ -88,6 +88,18 @@ const WORKTREE_TOOL_NAMES = new Set(["read", "glob", "grep", "edit", "write", "b
|
|
|
88
88
|
* with their own FileStateTracker and the worktree approval policy. A
|
|
89
89
|
* profile's tools list can narrow the set but never widen it.
|
|
90
90
|
*/
|
|
91
|
+
/**
|
|
92
|
+
* Isolates a readonly child's mutable tool state (design v2 §2): any tool that
|
|
93
|
+
* exposes a cloneForChild hook (the standard `read`, which carries a
|
|
94
|
+
* FileStateTracker) is rebuilt as a fresh per-child instance, so concurrent
|
|
95
|
+
* members of a fan-out never share mutable tool state. Stateless tools
|
|
96
|
+
* (glob/grep, web/memory/skill/todo) and custom/mock tools without the hook are
|
|
97
|
+
* passed through unchanged. Write children get full isolation via
|
|
98
|
+
* createWorktreeChildTools instead.
|
|
99
|
+
*/
|
|
100
|
+
export function isolateReadonlyChildFileTools(tools) {
|
|
101
|
+
return tools.map((tool) => (tool.cloneForChild ? tool.cloneForChild() : tool));
|
|
102
|
+
}
|
|
91
103
|
export function createWorktreeChildTools(worktreeCwd, include) {
|
|
92
104
|
const approval = new WorktreeApprovalController(worktreeCwd);
|
|
93
105
|
const fileState = new FileStateTracker(worktreeCwd);
|
package/dist/tools/read.d.ts
CHANGED
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
import type { ApprovalController } from "../approval/types.js";
|
|
5
5
|
import type { ToolRegistryEntry } from "../types.js";
|
|
6
6
|
import type { LspService } from "../lsp/index.js";
|
|
7
|
-
import
|
|
7
|
+
import { FileStateTracker } from "./file-state.js";
|
|
8
8
|
export declare function createReadTool(cwd: string, approval?: ApprovalController, lsp?: LspService, fileState?: FileStateTracker): ToolRegistryEntry;
|
package/dist/tools/read.js
CHANGED
|
@@ -5,6 +5,7 @@ import { constants } from "node:fs";
|
|
|
5
5
|
import { access, readFile, readdir, stat } from "node:fs/promises";
|
|
6
6
|
import { basename, dirname, extname, join, relative } from "node:path";
|
|
7
7
|
import { isSensitivePath } from "./sensitive-paths.js";
|
|
8
|
+
import { FileStateTracker } from "./file-state.js";
|
|
8
9
|
import { resolveToolPath } from "./path-utils.js";
|
|
9
10
|
const MAX_LINES = 2500;
|
|
10
11
|
const MAX_BYTES = 256 * 1024;
|
|
@@ -33,6 +34,11 @@ export function createReadTool(cwd, approval, lsp, fileState) {
|
|
|
33
34
|
},
|
|
34
35
|
required: ["path"],
|
|
35
36
|
},
|
|
37
|
+
// Per-child isolation hook (design v2 §2): a concurrent subagent fan-out
|
|
38
|
+
// gets a read instance with its own FileStateTracker so members never
|
|
39
|
+
// share mutable read-history state. Custom/mock read tools omit this and
|
|
40
|
+
// are passed through unchanged.
|
|
41
|
+
cloneForChild: () => createReadTool(cwd, approval, lsp, new FileStateTracker(cwd)),
|
|
36
42
|
async execute(args) {
|
|
37
43
|
const filePath = resolveToolPath(cwd, args.path);
|
|
38
44
|
if (isSensitivePath(filePath)) {
|
|
@@ -164,6 +170,9 @@ export function createReadTool(cwd, approval, lsp, fileState) {
|
|
|
164
170
|
metadata: {
|
|
165
171
|
kind: "read",
|
|
166
172
|
path: filePath,
|
|
173
|
+
offset: effectiveOffset + 1,
|
|
174
|
+
lines: sliced.length,
|
|
175
|
+
total: totalLines,
|
|
167
176
|
...(autoAdvanceNote ? { autoAdvanced: true } : {}),
|
|
168
177
|
...(truncated ? { truncated: true } : {}),
|
|
169
178
|
},
|
|
@@ -4,6 +4,12 @@ export interface ImageDisplayMessage {
|
|
|
4
4
|
export declare function imageDisplayLabel(index: number): string;
|
|
5
5
|
export declare function imageDisplayLabels(count: number, labelStart?: number): string[];
|
|
6
6
|
export declare function imageDisplayReferenceLine(label: string): string;
|
|
7
|
+
/**
|
|
8
|
+
* Removes inline image labels (and a single trailing space) from composer text
|
|
9
|
+
* before submit, so the labels stay a composer-only positioning affordance and
|
|
10
|
+
* the model receives the user's actual text. Each label is removed once.
|
|
11
|
+
*/
|
|
12
|
+
export declare function stripInlineImageLabels(content: string, labels: string[]): string;
|
|
7
13
|
export declare function isImageDisplayReferenceLine(line: string): boolean;
|
|
8
14
|
export declare function splitImageDisplayContent(content: string): {
|
|
9
15
|
bodyLines: string[];
|
|
@@ -7,6 +7,25 @@ export function imageDisplayLabels(count, labelStart = 1) {
|
|
|
7
7
|
export function imageDisplayReferenceLine(label) {
|
|
8
8
|
return `└ ${label}`;
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Removes inline image labels (and a single trailing space) from composer text
|
|
12
|
+
* before submit, so the labels stay a composer-only positioning affordance and
|
|
13
|
+
* the model receives the user's actual text. Each label is removed once.
|
|
14
|
+
*/
|
|
15
|
+
export function stripInlineImageLabels(content, labels) {
|
|
16
|
+
let out = content;
|
|
17
|
+
for (const label of labels) {
|
|
18
|
+
const withSpace = out.indexOf(`${label} `);
|
|
19
|
+
if (withSpace >= 0) {
|
|
20
|
+
out = out.slice(0, withSpace) + out.slice(withSpace + label.length + 1);
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const bare = out.indexOf(label);
|
|
24
|
+
if (bare >= 0)
|
|
25
|
+
out = out.slice(0, bare) + out.slice(bare + label.length);
|
|
26
|
+
}
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
10
29
|
export function isImageDisplayReferenceLine(line) {
|
|
11
30
|
return /^└ \[Image #\d+\]$/.test(line.trimEnd());
|
|
12
31
|
}
|
|
@@ -28,7 +47,13 @@ export function formatImageUserDisplayText(input, imageCount, labelStart = 1) {
|
|
|
28
47
|
return input;
|
|
29
48
|
const labels = imageDisplayLabels(imageCount, labelStart);
|
|
30
49
|
const base = input.trim();
|
|
31
|
-
|
|
50
|
+
// Labels already present inline (placed at their paste position in the
|
|
51
|
+
// composer) stay where they are; only labels missing from the text are
|
|
52
|
+
// prepended as a headline (back-compat for callers without inline labels).
|
|
53
|
+
const missing = labels.filter((label) => !input.includes(label));
|
|
54
|
+
const headline = missing.length > 0
|
|
55
|
+
? (base ? `${missing.join(" ")} ${base}` : missing.join(" "))
|
|
56
|
+
: base;
|
|
32
57
|
return [
|
|
33
58
|
headline,
|
|
34
59
|
...labels.map(imageDisplayReferenceLine),
|
package/dist/tui-ink/app.js
CHANGED
|
@@ -8,6 +8,7 @@ import { registry as slashRegistry } from "../slash-commands/index.js";
|
|
|
8
8
|
import { UserConfig, maskKey } from "../config.js";
|
|
9
9
|
import { InputBox, isCtrlCInput, } from "./input-box.js";
|
|
10
10
|
import { MessageList } from "./message-list.js";
|
|
11
|
+
import { isMultiplexedTerminal } from "./terminal-env.js";
|
|
11
12
|
import { appendTextPart, appendToolPart, compactDisplayMessages, contentFromParts, latestCompactionSummary, moveStatusMessageToEnd, nextDisplayMessageKey, setUserInputStatus, snapshotDisplayParts, stripInterruptedAssistantMarker, toolCallsFromParts, } from "./display-history.js";
|
|
12
13
|
import { AgentRunInputQueue } from "../agent/input-controller.js";
|
|
13
14
|
import { paletteFor, ThemeProvider, useTheme } from "./theme.js";
|
|
@@ -23,6 +24,7 @@ import { useTerminalSize } from "./use-terminal-size.js";
|
|
|
23
24
|
import { WelcomeBanner, shouldShowWelcomeBanner } from "./welcome.js";
|
|
24
25
|
import { expandAtMentions } from "./file-mentions.js";
|
|
25
26
|
import { TodosPanel } from "./todos.js";
|
|
27
|
+
import { CompactionProgressCard } from "./compaction-progress.js";
|
|
26
28
|
import { PlanConfirm } from "./plan-confirm.js";
|
|
27
29
|
import { ApprovalDialog } from "./approval/approval-dialog.js";
|
|
28
30
|
import { getNextPermissionMode } from "../permission/mode.js";
|
|
@@ -35,6 +37,8 @@ import { formatImageUserDisplayText, nextImageDisplayLabelStart } from "../tui/i
|
|
|
35
37
|
import { decideStartingSubmitFingerprint, submitPayloadFingerprint } from "./submit-dedupe.js";
|
|
36
38
|
import { isQueuedInputForCurrentSession, queuedAndPendingDisplayKeys, } from "./input-queue.js";
|
|
37
39
|
import { SessionPicker } from "./session-picker.js";
|
|
40
|
+
import { SubagentInspector } from "./subagent-inspector.js";
|
|
41
|
+
import { collectSubagentGroups, subagentSummary } from "./subagent-view.js";
|
|
38
42
|
import { sessionDisplayName } from "../tui/session-display.js";
|
|
39
43
|
import { parseGoalCommand } from "../goal/command.js";
|
|
40
44
|
import { continuationPrompt, initialPrompt } from "../goal/prompts.js";
|
|
@@ -277,7 +281,31 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
277
281
|
const [streamingReasoning, setStreamingReasoning] = useState("");
|
|
278
282
|
const [streamingTools, setStreamingTools] = useState([]);
|
|
279
283
|
const [streamingParts, setStreamingParts] = useState([]);
|
|
280
|
-
|
|
284
|
+
// Live subagent groups for the Ctrl+G inspector; recomputed each render so it
|
|
285
|
+
// reflects members as their events stream into the transcript.
|
|
286
|
+
const subagentGroups = useMemo(() => collectSubagentGroups(messages, streamingTools), [messages, streamingTools]);
|
|
287
|
+
const subagentMembers = useMemo(() => subagentGroups.flatMap((g) => g.members), [subagentGroups]);
|
|
288
|
+
// Down-arrow from the composer focuses the subagent entry line; Enter then
|
|
289
|
+
// opens the inspector, Esc/Up returns to the composer (Claude Code parity).
|
|
290
|
+
const [subagentEntryFocused, setSubagentEntryFocused] = useState(false);
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (subagentMembers.length === 0 && subagentEntryFocused)
|
|
293
|
+
setSubagentEntryFocused(false);
|
|
294
|
+
}, [subagentMembers.length, subagentEntryFocused]);
|
|
295
|
+
// Live progress for a manual `/compact` run (null when not compacting).
|
|
296
|
+
const [compaction, setCompaction] = useState(null);
|
|
297
|
+
// Normalize agent.thinking against the current model's supported levels so the
|
|
298
|
+
// banner displays the *effective* level, not a stale user-config value like
|
|
299
|
+
// "xhigh" when switching to a model that only supports ["high","max","off"].
|
|
300
|
+
const [thinkingLevel, setThinkingLevel] = useState(() => {
|
|
301
|
+
const modelParts = agent.model.includes(":")
|
|
302
|
+
? agent.model.split(":")
|
|
303
|
+
: [agent.providerId || safeRegistry.getDefault()?.id || "openai", agent.model];
|
|
304
|
+
const providerId = modelParts[0];
|
|
305
|
+
const modelId = modelParts.slice(1).join(":");
|
|
306
|
+
const availableLevels = getAvailableThinkingLevels(providerId, modelId);
|
|
307
|
+
return normalizeThinkingLevel(agent.thinking, availableLevels);
|
|
308
|
+
});
|
|
281
309
|
const [permissionMode, setPermissionMode] = useState(agent.mode);
|
|
282
310
|
const [todos, setTodos] = useState(() => agent.getTodos());
|
|
283
311
|
const [goalLine, setGoalLine] = useState("");
|
|
@@ -521,6 +549,24 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
521
549
|
// also frees the arrow keys entirely for composer history.
|
|
522
550
|
if (pendingPlan || pendingApproval || pendingQuestion || pendingFeedback || statsPanel)
|
|
523
551
|
return;
|
|
552
|
+
// Subagent entry is focused (the composer is disabled): Enter opens the
|
|
553
|
+
// inspector, Up/Esc returns to the composer. Other keys just return focus.
|
|
554
|
+
if (subagentEntryFocused && !pickerMode) {
|
|
555
|
+
if (key.return) {
|
|
556
|
+
setSubagentEntryFocused(false);
|
|
557
|
+
setStatsPanel(null);
|
|
558
|
+
setPickerMode("agents");
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (key.escape || key.upArrow) {
|
|
562
|
+
setSubagentEntryFocused(false);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (key.downArrow)
|
|
566
|
+
return; // stay focused
|
|
567
|
+
setSubagentEntryFocused(false);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
524
570
|
if (key.ctrl && input.toLowerCase() === "p" && !pickerMode && !activeAbortRef.current) {
|
|
525
571
|
setStatsPanel(null);
|
|
526
572
|
setPickerMode("slash");
|
|
@@ -642,7 +688,11 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
642
688
|
// <Static>), so clearing React state is not enough — resetTranscript wipes
|
|
643
689
|
// the screen + scrollback and re-prints the (now empty) transcript.
|
|
644
690
|
resetTranscript(() => []);
|
|
645
|
-
|
|
691
|
+
// The todos panel renders off React state, not the transcript, so wiping
|
|
692
|
+
// messages alone leaves a stale To-Do list on screen. /clear already reset
|
|
693
|
+
// the agent's todos; mirror that into the UI (same as session switch).
|
|
694
|
+
setTodos(agent.getTodos());
|
|
695
|
+
}, [resetTranscript, agent]);
|
|
646
696
|
// Render a placeholder user row for input waiting to enter the run.
|
|
647
697
|
const addStatusUserMessage = useCallback((content, status) => {
|
|
648
698
|
const key = nextDisplayMessageKey("user");
|
|
@@ -1238,7 +1288,14 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1238
1288
|
if (steer) {
|
|
1239
1289
|
pendingSteersRef.current.delete(event.id);
|
|
1240
1290
|
setPendingSteerCount(pendingSteersRef.current.size);
|
|
1241
|
-
|
|
1291
|
+
// Moving the steer placeholder out of the live region into
|
|
1292
|
+
// <Static> is a pure append (it was never in the settled list,
|
|
1293
|
+
// only the dynamic block). Off a multiplexer Ink erases the
|
|
1294
|
+
// vacated live rows in place, so a plain append avoids the
|
|
1295
|
+
// full-screen reprint flash. Under tmux/screen the in-place
|
|
1296
|
+
// erase can't reach scrolled rows, so keep the clean reprint.
|
|
1297
|
+
const commit = isMultiplexedTerminal() ? resetTranscript : updateDisplayMessages;
|
|
1298
|
+
commit((prev) => moveStatusMessageToEnd(prev, steer.displayKey));
|
|
1242
1299
|
}
|
|
1243
1300
|
break;
|
|
1244
1301
|
}
|
|
@@ -1287,7 +1344,24 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1287
1344
|
commitAssistantMessage();
|
|
1288
1345
|
if (err instanceof AgentAbortError || err?.name === "AbortError") {
|
|
1289
1346
|
runCancelled = true;
|
|
1290
|
-
|
|
1347
|
+
// commitAssistantMessage already appended the partial answer; the
|
|
1348
|
+
// interrupt is otherwise a pure append (the partial + a "Interrupted"
|
|
1349
|
+
// row). Off a multiplexer, append just the interrupt row so settled
|
|
1350
|
+
// history is never reprinted — no flash. Under tmux/screen, fall back
|
|
1351
|
+
// to the full reprint that rebuilds from the canonical agent.messages.
|
|
1352
|
+
if (isMultiplexedTerminal()) {
|
|
1353
|
+
resetTranscript(() => reconstructDisplayMessages(agent.messages));
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
updateDisplayMessages((prev) => [
|
|
1357
|
+
...prev,
|
|
1358
|
+
withMessageKey({
|
|
1359
|
+
role: "assistant",
|
|
1360
|
+
content: "Interrupted by user",
|
|
1361
|
+
syntheticKind: "ui_interrupt",
|
|
1362
|
+
}),
|
|
1363
|
+
]);
|
|
1364
|
+
}
|
|
1291
1365
|
}
|
|
1292
1366
|
else {
|
|
1293
1367
|
runErrored = true;
|
|
@@ -1544,6 +1618,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1544
1618
|
toggleSidebar,
|
|
1545
1619
|
setSidebarMode: applySidebarMode,
|
|
1546
1620
|
openStats: openStatsPanel,
|
|
1621
|
+
compactionProgress: setCompaction,
|
|
1547
1622
|
});
|
|
1548
1623
|
if (handled) {
|
|
1549
1624
|
if (agent.mode !== permissionMode) {
|
|
@@ -1707,7 +1782,7 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1707
1782
|
}, onCancel: closePicker }) })), pickerMode === "mcp-reconnect" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(McpReconnectPicker, { items: mcpReconnectItems, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (item) => {
|
|
1708
1783
|
closePicker();
|
|
1709
1784
|
void handleSubmit(item.command);
|
|
1710
|
-
}, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
|
|
1785
|
+
}, onCancel: closePicker }) })), pickerMode === "session" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SessionPicker, { currentCwd: args.cwd, currentSessions: SessionManager.summarizeSessionsForCwd(args.cwd), allSessions: SessionManager.listAllSessions(), onSelect: handleSessionSelect, onCancel: closePicker }) })), pickerMode === "agents" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(SubagentInspector, { groups: subagentGroups, onCancel: closePicker }) })), pickerMode === "rewind" && sessionManager && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(RewindPicker, { sessionManager: sessionManager, terminalColumns: mainWidth, terminalRows: terminalRows, onSelect: (command) => {
|
|
1711
1786
|
closePicker();
|
|
1712
1787
|
void handleSubmit(command);
|
|
1713
1788
|
}, onCancel: closePicker }) })), pickerMode === "feishu-setup" && (_jsx(Box, { paddingX: 1, flexShrink: 0, children: _jsx(FeishuSetupPicker, { onComplete: (summary) => {
|
|
@@ -1743,7 +1818,10 @@ export function App({ agent, args, sessionManager: initialSessionManager, switch
|
|
|
1743
1818
|
else if (result.kind === "error") {
|
|
1744
1819
|
addMessage("error", `Feedback failed: ${result.message}`);
|
|
1745
1820
|
}
|
|
1746
|
-
} }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined,
|
|
1821
|
+
} }) })), !isExiting && compaction && (_jsx(Box, { flexShrink: 0, backgroundColor: palette.background, children: _jsx(CompactionProgressCard, { progress: compaction }) })), !isExiting && isRunning && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && (_jsx(Box, { paddingX: 1, paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(WaitingIndicator, { tools: streamingTools, hasStreamingText: streamingContent.length > 0, hasStreamingReasoning: streamingReasoning.length > 0, streamedChars: streamingContent.length + streamingReasoning.length, nowTick: nowTick, pendingSteerCount: pendingSteerCount, queuedCount: queuedCount }) })), !isExiting && !pickerMode && !statsPanel && (_jsx(Box, { paddingBottom: 1, flexShrink: 0, backgroundColor: palette.background, children: _jsx(InputBox, { onSubmit: handleSubmit, onQueue: isRunning ? queueInput : undefined, onArrowDownAtBottom: () => {
|
|
1822
|
+
if (subagentMembers.length > 0 && !pickerMode)
|
|
1823
|
+
setSubagentEntryFocused(true);
|
|
1824
|
+
}, disabled: !!pendingPlan || !!pendingApproval || !!pendingQuestion || !!pendingFeedback || !!statsPanel || subagentEntryFocused, cursorResetEpoch: cursorResetEpoch, draftText: composerDraft?.text, draftEpoch: composerDraft?.epoch, onDraftApplied: clearComposerDraft, skillRegistry: safeSkillRegistry, localSlashCommands: [...INK_LOCAL_SLASH_COMMANDS], terminalColumns: mainWidth, cwd: args.cwd, sessionFile: currentSessionFile(), nextImageLabelStart: nextImageDisplayLabelStartRef.current }) })), !isExiting && !pickerMode && !statsPanel && !pendingPlan && !pendingApproval && !pendingQuestion && !pendingFeedback && subagentMembers.length > 0 && (_jsxs(Box, { paddingX: 1, flexShrink: 0, backgroundColor: palette.background, children: [_jsx(Text, { bold: subagentEntryFocused, color: subagentEntryFocused ? palette.accent : palette.toolName, children: subagentEntryFocused ? "> ↳ " : " ↳ " }), _jsxs(Text, { color: subagentEntryFocused ? palette.accent : palette.muted, children: [subagentMembers.length, " subagent", subagentMembers.length === 1 ? "" : "s", " \u00B7 ", subagentSummary(subagentMembers), " \u00B7 "] }), _jsx(Text, { color: palette.accent, children: subagentEntryFocused ? "Enter open · Esc back" : "↓ to inspect traces" })] })), !isExiting && (_jsx(Box, { flexShrink: 0, children: _jsx(FooterBar, { data: buildFooterData({ mode: permissionMode, goalLine }) }) }))] }), sidebarVisible && (_jsx(InkSidebar, { width: sidebarWidth, agent: agent, sessionManager: sessionManager, cwd: args.cwd, mode: permissionMode, goalLine: goalLine, todos: todos, mcpManager: mcpManager, lspService: lspService }))] }) }));
|
|
1747
1825
|
}
|
|
1748
1826
|
function buildCommandPaletteItems(skillRegistry) {
|
|
1749
1827
|
const items = new Map();
|