pi-open-agents 0.1.23 → 0.1.25
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 +20 -0
- package/package.json +1 -1
- package/src/discovery/loader.ts +13 -2
- package/src/index.ts +43 -2
- package/src/permission/evaluator.ts +1 -1
- package/src/permission/gate.ts +194 -0
- package/src/permission/parser.ts +3 -2
- package/src/primary/manager.ts +5 -0
- package/src/tui/banner.ts +12 -2
package/README.md
CHANGED
|
@@ -147,6 +147,26 @@ Tool names are normalized automatically: `task` → `subagent`, `vscode` → `re
|
|
|
147
147
|
**separate** tools — `write` creates/overwrites files, `edit` does search-and-
|
|
148
148
|
replace — so they have separate permission categories.
|
|
149
149
|
|
|
150
|
+
**Runtime enforcement (v0.1.24).** Every tool call is evaluated against the
|
|
151
|
+
active agent's rules *before it runs* — not just used to filter the toolset:
|
|
152
|
+
|
|
153
|
+
- `deny` blocks the call; the model receives a clear denial reason
|
|
154
|
+
- `ask` opens a dialog: *Allow once / Allow always (this session) / Deny*
|
|
155
|
+
- Patterns are **argument-aware**: file paths for `read`/`edit`/`write`,
|
|
156
|
+
command strings for `bash` (`"rm -rf *": deny`, `"**/*.env": deny` — `*`
|
|
157
|
+
doesn't cross `/`, use `**` for nested paths)
|
|
158
|
+
- Rules are evaluated top-to-bottom, **last match wins** — put specific rules
|
|
159
|
+
*after* catch-alls (`"*": ask` first, then `"git *": allow`)
|
|
160
|
+
- A call matching no pattern within a configured category proceeds (rule lists
|
|
161
|
+
are deny/ask lists, not exhaustive allowlists)
|
|
162
|
+
|
|
163
|
+
> **⚠️ Migration note (v0.1.24):** `ask` is now enforced — previously it was
|
|
164
|
+
> parsed but silently ignored. In non-interactive processes (subagent
|
|
165
|
+
> children, print/JSON mode) an `ask` rule **blocks** the call (fail-closed):
|
|
166
|
+
> add an explicit `allow` rule if a subagent needs that tool. `deny` now also
|
|
167
|
+
> blocks matching *individual calls* (patterns included), not just statically
|
|
168
|
+
> hiding whole tools. (#20)
|
|
169
|
+
|
|
150
170
|
### Auto-injected delegation guidance
|
|
151
171
|
|
|
152
172
|
If an agent has the `subagent` tool, the plugin automatically appends a
|
package/package.json
CHANGED
package/src/discovery/loader.ts
CHANGED
|
@@ -92,6 +92,9 @@ export async function loadAgents(options: LoadAgentsOptions = {}): Promise<Agent
|
|
|
92
92
|
const projectAgents = new Map<string, AgentDefinition>();
|
|
93
93
|
const warnings: AgentLoadResult["warnings"] = [];
|
|
94
94
|
|
|
95
|
+
const GLOBAL_FAMILY_RANK: Record<string, number> = { pi: 2, opencode: 1, shared: 1 };
|
|
96
|
+
const globalRanks = new Map<string, number>();
|
|
97
|
+
|
|
95
98
|
for (const discPath of paths) {
|
|
96
99
|
// For each subdir pattern, scan the directory
|
|
97
100
|
for (const subdir of discPath.subdirs) {
|
|
@@ -113,8 +116,16 @@ export async function loadAgents(options: LoadAgentsOptions = {}): Promise<Agent
|
|
|
113
116
|
if (agent.disable) continue;
|
|
114
117
|
|
|
115
118
|
// Add to the appropriate map
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
if (discPath.source === "project") {
|
|
120
|
+
projectAgents.set(agent.name, agent);
|
|
121
|
+
} else {
|
|
122
|
+
const rank = GLOBAL_FAMILY_RANK[discPath.family] ?? 0;
|
|
123
|
+
const prevRank = globalRanks.get(agent.name);
|
|
124
|
+
if (prevRank === undefined || rank >= prevRank) {
|
|
125
|
+
globalAgents.set(agent.name, agent);
|
|
126
|
+
globalRanks.set(agent.name, rank);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
118
129
|
} catch (error) {
|
|
119
130
|
warnings.push({
|
|
120
131
|
filePath,
|
package/src/index.ts
CHANGED
|
@@ -22,17 +22,19 @@ import type { AgentDefinition, ThinkingLevel } from "./types.ts";
|
|
|
22
22
|
import { loadAgents, selectableAgents, spawnableAgents } from "./discovery/loader.ts";
|
|
23
23
|
import { AgentManager } from "./primary/manager.ts";
|
|
24
24
|
import { refreshAgentTools, buildSystemPrompt, buildSubagentGuidance } from "./primary/executor.ts";
|
|
25
|
-
import { updateBanner } from "./tui/banner.ts";
|
|
25
|
+
import { setShowAgentDescription, updateBanner } from "./tui/banner.ts";
|
|
26
26
|
import { registerTuiControls } from "./tui/selector.ts";
|
|
27
27
|
import { registerAgentTools } from "./tui/tools.ts";
|
|
28
28
|
import { registerSubagentTool } from "./subagent/tool.ts";
|
|
29
29
|
import { allowedAgentNames, isSubagentProcess } from "./subagent/env.ts";
|
|
30
|
+
import { registerPermissionGate } from "./permission/gate.ts";
|
|
30
31
|
|
|
31
32
|
// ─── Settings ────────────────────────────────────────────────────────────────
|
|
32
33
|
|
|
33
34
|
interface Settings {
|
|
34
35
|
defaultAgent?: string;
|
|
35
36
|
defaultThinkingLevel?: ThinkingLevel;
|
|
37
|
+
showAgentDescription?: boolean;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function loadSettings(cwd: string): Settings {
|
|
@@ -74,6 +76,7 @@ export default function piOpenAgents(pi: ExtensionAPI): void {
|
|
|
74
76
|
pi.on("session_start", async (event, ctx) => {
|
|
75
77
|
// Load settings for thinking fallback
|
|
76
78
|
const settings = loadSettings(ctx.cwd);
|
|
79
|
+
setShowAgentDescription(settings.showAgentDescription);
|
|
77
80
|
|
|
78
81
|
// Load all agent definitions from discovery paths
|
|
79
82
|
const result = await loadAgents({
|
|
@@ -98,10 +101,32 @@ export default function piOpenAgents(pi: ExtensionAPI): void {
|
|
|
98
101
|
getActiveAgent: () => manager.getActive(),
|
|
99
102
|
});
|
|
100
103
|
|
|
104
|
+
// Runtime permission gate: enforces allow/deny/ask on every tool call
|
|
105
|
+
// (argument-aware patterns + interactive ask; fail-closed in subagents).
|
|
106
|
+
registerPermissionGate(pi, {
|
|
107
|
+
resolveAgent: () => {
|
|
108
|
+
// In subagent children the active agent is not applied (the executor
|
|
109
|
+
// configures them via CLI flags) — resolve by the env var it passes.
|
|
110
|
+
const childName = process.env.PI_OPEN_AGENTS_NAME;
|
|
111
|
+
if (childName) return manager.getAgent(childName);
|
|
112
|
+
return manager.getActive();
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
101
116
|
// Subagent child processes are fully configured by the executor via CLI flags
|
|
102
117
|
// (--model, --system-prompt, --tools). Do NOT apply defaultAgent — it would
|
|
103
118
|
// override the executor's model/tools/prompt settings.
|
|
104
119
|
if (isSubagentProcess(process.env)) {
|
|
120
|
+
// A forked child inherits the parent's session entries (pi core copies
|
|
121
|
+
// them into the branched session) and never re-announces (this early
|
|
122
|
+
// return). Without this, the permission system would resolve the
|
|
123
|
+
// parent's active_agent entry and attribute the parent's policy to the
|
|
124
|
+
// child. Append an explicit terminator so the child falls back to the
|
|
125
|
+
// global policy — the pre-patch baseline. Falls back to the global policy
|
|
126
|
+
// because nothing has resolved an agent name yet in this process — the
|
|
127
|
+
// permission system caches the last name it resolved (knownAgentName) and
|
|
128
|
+
// does not clear it between sessions.
|
|
129
|
+
pi.appendEntry("active_agent", { name: null });
|
|
105
130
|
updateBanner(ctx.ui, undefined, 0);
|
|
106
131
|
return;
|
|
107
132
|
}
|
|
@@ -127,10 +152,26 @@ export default function piOpenAgents(pi: ExtensionAPI): void {
|
|
|
127
152
|
|
|
128
153
|
if (agentEntry?.data?.name) {
|
|
129
154
|
manager.restoreFromSession(agentEntry.data.name);
|
|
130
|
-
//
|
|
155
|
+
// Only announce the agent when the restore actually produced an active
|
|
156
|
+
// agent. restoreFromSession() silently no-ops on an unknown name
|
|
157
|
+
// (deleted, renamed, or disable:true — the loader skips those), and
|
|
158
|
+
// announcing one would make the permission system apply a disabled
|
|
159
|
+
// agent's policy to this session.
|
|
131
160
|
const active = manager.getActive();
|
|
132
161
|
if (active) {
|
|
162
|
+
pi.appendEntry("active_agent", { name: active.name });
|
|
163
|
+
// Re-apply tools on resume (model preserved from session)
|
|
133
164
|
refreshAgentTools(active, pi);
|
|
165
|
+
} else {
|
|
166
|
+
// The restore no-op'd (see above), so no new entry was written. An
|
|
167
|
+
// older active_agent entry written by a previous apply() is still the
|
|
168
|
+
// newest one, and the consumer's backward scan would resolve it —
|
|
169
|
+
// silently applying that (now disabled/unknown) agent's policy.
|
|
170
|
+
// Append an explicit terminator so it stops here and falls back to
|
|
171
|
+
// the global policy — nothing has resolved an agent name yet in this
|
|
172
|
+
// process, since the consumer caches the last name it resolved
|
|
173
|
+
// (knownAgentName) and does not clear it between sessions.
|
|
174
|
+
pi.appendEntry("active_agent", { name: null });
|
|
134
175
|
}
|
|
135
176
|
updateBanner(ctx.ui, manager.getActive(), selectableAgents(manager.getAgents()).length);
|
|
136
177
|
return;
|
|
@@ -112,7 +112,7 @@ export function evaluate(
|
|
|
112
112
|
* "*" matches anything
|
|
113
113
|
* "edit" matches "e*" (not typical, but supported)
|
|
114
114
|
*/
|
|
115
|
-
function matchPermissionKey(rulePermission: string, toolPermission: string): boolean {
|
|
115
|
+
export function matchPermissionKey(rulePermission: string, toolPermission: string): boolean {
|
|
116
116
|
if (rulePermission === "*") return true;
|
|
117
117
|
return matchPattern(rulePermission, toolPermission);
|
|
118
118
|
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime permission gate.
|
|
3
|
+
*
|
|
4
|
+
* Enforces allow/deny/ask on every tool call via pi's `tool_call` hook:
|
|
5
|
+
*
|
|
6
|
+
* - deny → the call is blocked with a model-visible reason
|
|
7
|
+
* - ask → interactive select dialog (allow once / always this session / deny)
|
|
8
|
+
* - ask in a non-interactive process (headless subagent) → fail-closed block
|
|
9
|
+
* - allow / no matching rule → the call proceeds
|
|
10
|
+
*
|
|
11
|
+
* This complements the static toolset filtering in `evaluator.ts`:
|
|
12
|
+
* pre-filtering hides tools that are denied with pattern "*"; the gate
|
|
13
|
+
* enforces argument-aware rules (file paths, bash commands) that cannot be
|
|
14
|
+
* derived statically, and implements `ask` — which has no other executor.
|
|
15
|
+
*
|
|
16
|
+
* Semantics (documented):
|
|
17
|
+
* - Rules are scoped by permission category: if the agent has no rule whose
|
|
18
|
+
* permission key matches the call's category, the call is not gated.
|
|
19
|
+
* - Within a gated category, last-match-wins; a call that matches no pattern
|
|
20
|
+
* proceeds (unmatched ≠ ask — write an explicit `"*": "ask"` rule to ask
|
|
21
|
+
* for the remainder).
|
|
22
|
+
* - A dialog returning "always" memoizes the category for the process
|
|
23
|
+
* (i.e. for the session — subagent children are separate processes).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
27
|
+
|
|
28
|
+
import type { AgentDefinition, PermissionAction, PermissionRule } from "../types.ts";
|
|
29
|
+
import { evaluate, matchPermissionKey, toolToPermission } from "./evaluator.ts";
|
|
30
|
+
import { parsePermissionRules } from "./parser.ts";
|
|
31
|
+
|
|
32
|
+
// ─── Argument Extraction ─────────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extract the string argument a permission pattern is matched against.
|
|
36
|
+
*
|
|
37
|
+
* - bash/powershell → the command
|
|
38
|
+
* - file tools → the path (pi uses `file_path`; some tools use `path`)
|
|
39
|
+
* - grep/find → path when present, else the pattern
|
|
40
|
+
* - unknown tools → first conventional string field, else undefined
|
|
41
|
+
* (only `"*"` patterns match undefined)
|
|
42
|
+
*/
|
|
43
|
+
export function extractToolArg(toolName: string, input: Record<string, unknown>): string | undefined {
|
|
44
|
+
const str = (key: string): string | undefined =>
|
|
45
|
+
typeof input[key] === "string" ? (input[key] as string) : undefined;
|
|
46
|
+
|
|
47
|
+
switch (toolName) {
|
|
48
|
+
case "bash":
|
|
49
|
+
case "powershell":
|
|
50
|
+
case "shell":
|
|
51
|
+
return str("command");
|
|
52
|
+
case "read":
|
|
53
|
+
case "edit":
|
|
54
|
+
case "write":
|
|
55
|
+
case "apply_patch":
|
|
56
|
+
case "ls":
|
|
57
|
+
return str("file_path") ?? str("path");
|
|
58
|
+
case "grep":
|
|
59
|
+
case "find":
|
|
60
|
+
return str("path") ?? str("pattern");
|
|
61
|
+
default:
|
|
62
|
+
return str("path") ?? str("file_path") ?? str("command") ?? str("pattern") ?? str("url");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ─── Decision ────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
export interface GateDecision {
|
|
69
|
+
/** Effective action for this call */
|
|
70
|
+
action: PermissionAction;
|
|
71
|
+
/** Permission category (write → edit, etc.) */
|
|
72
|
+
category: string;
|
|
73
|
+
/** Matched rule, if any */
|
|
74
|
+
matched?: { permission: string; pattern: string; action: PermissionAction };
|
|
75
|
+
/** Whether the call is gated at all (false = no rules for the category) */
|
|
76
|
+
gated: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Decide the effective action for a tool call.
|
|
81
|
+
*
|
|
82
|
+
* Ungated categories (no rules) return `allow` — this keeps the gate opt-in
|
|
83
|
+
* per category and preserves behavior for agents without permission config.
|
|
84
|
+
*/
|
|
85
|
+
export function decideToolCall(
|
|
86
|
+
toolName: string,
|
|
87
|
+
input: Record<string, unknown>,
|
|
88
|
+
rules: PermissionRule[],
|
|
89
|
+
): GateDecision {
|
|
90
|
+
const category = toolToPermission(toolName);
|
|
91
|
+
|
|
92
|
+
const categoryHasRules = rules.some((rule) => matchPermissionKey(rule.permission, category));
|
|
93
|
+
if (!categoryHasRules) {
|
|
94
|
+
return { action: "allow", category, gated: false };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const arg = extractToolArg(toolName, input);
|
|
98
|
+
const result = evaluate(toolName, arg, rules);
|
|
99
|
+
|
|
100
|
+
if (!result.matched) {
|
|
101
|
+
// Explicit rules exist for the category but none matched this call:
|
|
102
|
+
// unmatched proceeds (deny/ask lists are not exhaustive allowlists).
|
|
103
|
+
return { action: "allow", category, gated: true };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
action: result.action,
|
|
108
|
+
category,
|
|
109
|
+
gated: true,
|
|
110
|
+
matched: { permission: result.permission, pattern: result.pattern, action: result.action },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ─── Reason Formatting ───────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
function preview(value: string, max = 80): string {
|
|
117
|
+
const flat = value.replace(/\s+/g, " ").trim();
|
|
118
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function formatDenyReason(toolName: string, decision: GateDecision, arg: string | undefined): string {
|
|
122
|
+
const rule = decision.matched;
|
|
123
|
+
const target = arg ? ` (${preview(arg)})` : "";
|
|
124
|
+
return `Permission denied: agent rules deny "${toolName}"${target} [rule: ${rule?.permission} "${rule?.pattern}" → deny]`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function formatHeadlessAskReason(toolName: string, decision: GateDecision): string {
|
|
128
|
+
const rule = decision.matched;
|
|
129
|
+
return (
|
|
130
|
+
`Permission required: the active agent asks for confirmation on "${toolName}" ` +
|
|
131
|
+
`[rule: ${rule?.permission} "${rule?.pattern}" → ask], but this process is non-interactive ` +
|
|
132
|
+
`(subagent) — the call was blocked (fail-closed). Add an explicit allow rule to permit it.`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ─── Gate Registration ───────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
export interface PermissionGateOptions {
|
|
139
|
+
/** Resolve the agent whose rules apply (active agent, or the subagent's own definition). */
|
|
140
|
+
resolveAgent: () => AgentDefinition | undefined;
|
|
141
|
+
/** Session-scoped "always allow" memo (per process). Injectable for tests. */
|
|
142
|
+
alwaysAllow?: Map<string, boolean>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Register the permission gate on the `tool_call` event.
|
|
147
|
+
*
|
|
148
|
+
* Does nothing for agents without permission rules.
|
|
149
|
+
*/
|
|
150
|
+
export function registerPermissionGate(pi: ExtensionAPI, options: PermissionGateOptions): void {
|
|
151
|
+
const alwaysAllow = options.alwaysAllow ?? new Map<string, boolean>();
|
|
152
|
+
|
|
153
|
+
pi.on("tool_call", async (event, ctx: ExtensionContext) => {
|
|
154
|
+
const agent = options.resolveAgent();
|
|
155
|
+
if (!agent?.permission) return undefined;
|
|
156
|
+
|
|
157
|
+
const input = (event as { input?: Record<string, unknown> }).input ?? {};
|
|
158
|
+
const rules = parsePermissionRules(agent.permission);
|
|
159
|
+
const decision = decideToolCall(event.toolName, input, rules);
|
|
160
|
+
|
|
161
|
+
if (!decision.gated || decision.action === "allow") return undefined;
|
|
162
|
+
|
|
163
|
+
const arg = extractToolArg(event.toolName, input);
|
|
164
|
+
|
|
165
|
+
if (decision.action === "deny") {
|
|
166
|
+
return { block: true, reason: formatDenyReason(event.toolName, decision, arg) };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ── ask ──
|
|
170
|
+
if (alwaysAllow.get(decision.category)) return undefined;
|
|
171
|
+
|
|
172
|
+
if (!ctx.hasUI) {
|
|
173
|
+
return { block: true, reason: formatHeadlessAskReason(event.toolName, decision) };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const target = arg ? `: ${preview(arg)}` : "";
|
|
177
|
+
const choice = await ctx.ui.select(
|
|
178
|
+
`Permission: allow ${event.toolName}${target}?`,
|
|
179
|
+
["Allow once", "Allow always (this session)", "Deny"],
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
if (choice === "Allow always (this session)") {
|
|
183
|
+
alwaysAllow.set(decision.category, true);
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
if (choice === "Allow once") return undefined;
|
|
187
|
+
|
|
188
|
+
const how = choice === undefined ? "no answer was given" : "the user denied it";
|
|
189
|
+
return {
|
|
190
|
+
block: true,
|
|
191
|
+
reason: `Permission denied: ${how} for "${event.toolName}".`,
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
}
|
package/src/permission/parser.ts
CHANGED
|
@@ -29,8 +29,9 @@ import { toolToPermission } from "./evaluator.ts";
|
|
|
29
29
|
* Rules from nested objects are expanded with their pattern.
|
|
30
30
|
* Simple string values get pattern "*".
|
|
31
31
|
*
|
|
32
|
-
* Permission keys are normalized via toolToPermission (
|
|
33
|
-
*
|
|
32
|
+
* Permission keys are normalized via toolToPermission (apply_patch → edit,
|
|
33
|
+
* mcp read tools → read). In pi, `write` and `edit` are separate tools and
|
|
34
|
+
* stay separate permission categories (see evaluator.ts).
|
|
34
35
|
*
|
|
35
36
|
* Order is preserved as written (object key order).
|
|
36
37
|
*/
|
package/src/primary/manager.ts
CHANGED
|
@@ -69,6 +69,7 @@ export class AgentManager {
|
|
|
69
69
|
this.active = agent;
|
|
70
70
|
|
|
71
71
|
await applyAgentConfig(agent, pi, ctx);
|
|
72
|
+
pi.appendEntry("active_agent", { name });
|
|
72
73
|
|
|
73
74
|
return true;
|
|
74
75
|
}
|
|
@@ -93,6 +94,10 @@ export class AgentManager {
|
|
|
93
94
|
|
|
94
95
|
this.activeName = undefined;
|
|
95
96
|
this.active = undefined;
|
|
97
|
+
// The consumer treats a null-name entry as "stop scanning" and then falls
|
|
98
|
+
// back to its last known name, so clearing does not restore the global
|
|
99
|
+
// policy for a session that already resolved a non-null agent.
|
|
100
|
+
pi.appendEntry("active_agent", { name: null });
|
|
96
101
|
}
|
|
97
102
|
|
|
98
103
|
/**
|
package/src/tui/banner.ts
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Status banner widget for the TUI.
|
|
3
3
|
*
|
|
4
|
-
* Shows the active agent name,
|
|
4
|
+
* Shows the active agent name, description (when enabled), and model above the editor.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { AgentDefinition } from "../types.ts";
|
|
9
9
|
|
|
10
|
+
let showDescription = true;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configure whether the active agent's description is shown in the status banner.
|
|
14
|
+
* Absent/undefined falls back to true, preserving the default behavior.
|
|
15
|
+
*/
|
|
16
|
+
export function setShowAgentDescription(value: boolean | undefined): void {
|
|
17
|
+
showDescription = value ?? true;
|
|
18
|
+
}
|
|
19
|
+
|
|
10
20
|
/**
|
|
11
21
|
* Update the status widget to reflect the active agent.
|
|
12
22
|
* Pass undefined to show "no agent selected" hint.
|
|
@@ -20,7 +30,7 @@ export function updateBanner(
|
|
|
20
30
|
const parts: string[] = [];
|
|
21
31
|
parts.push(ui.theme.bold(ui.theme.fg("accent", activeAgent.name)));
|
|
22
32
|
|
|
23
|
-
if (activeAgent.description) {
|
|
33
|
+
if (showDescription && activeAgent.description) {
|
|
24
34
|
parts.push(ui.theme.fg("muted", `— ${activeAgent.description}`));
|
|
25
35
|
}
|
|
26
36
|
|