pi-subagents-lite 1.0.1 → 1.1.0

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.
@@ -369,219 +369,207 @@ async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo> {
369
369
  };
370
370
  }
371
371
 
372
- export async function runAgent(
373
- ctx: ExtensionContext,
374
- type: SubagentType,
375
- prompt: string,
376
- options: RunOptions,
377
- ): Promise<RunResult> {
378
- const config = getConfig(type);
379
- const agentConfig = getAgentConfig(type);
380
-
381
- // Warn on mutual exclusion violations
382
- const notify = (msg: string) => {
383
- if (ctx.ui?.notify) {
384
- ctx.ui.notify(`[pi-subagents] ${msg}`, "warning");
385
- } else {
386
- console.warn(`[pi-subagents] ${msg}`);
387
- }
388
- };
389
- if (agentConfig?.excludeTools && Array.isArray(agentConfig.tools)) {
390
- notify(`agent "${type}": both tools and exclude_tools set — tools (whitelist) wins`);
391
- }
392
- if (agentConfig?.excludeExtensions && Array.isArray(agentConfig.extensions)) {
393
- notify(`agent "${type}": both extensions and exclude_extensions set — extensions (whitelist) wins`);
394
- }
395
-
396
- // Resolve working directory
397
- const effectiveCwd = options.cwd ?? ctx.cwd;
398
-
399
- const env = await detectEnv(options.pi, effectiveCwd);
400
-
401
- // Resolve extensions/skills from agent config (frontmatter)
402
- const extensions = config.extensions;
403
- const skills = config.skills;
404
- const preloadSkillsList = agentConfig?.preloadSkills;
372
+ // ── runAgent phases ────────────────────────────────────────────────
405
373
 
406
- // Build prompt extras (skills only).
374
+ /**
375
+ * Phase 1: Resolve system prompt from agent config, skills, and env info.
376
+ */
377
+ function buildPrompt(
378
+ type: SubagentType,
379
+ agentConfig: ReturnType<typeof getAgentConfig>,
380
+ config: ReturnType<typeof getConfig>,
381
+ cwd: string,
382
+ env: EnvInfo,
383
+ ): string {
407
384
  const extras: PromptExtras = {};
408
- if (Array.isArray(preloadSkillsList)) {
409
- extras.skillBlocks = preloadSkills(preloadSkillsList, effectiveCwd);
385
+ if (Array.isArray(agentConfig?.preloadSkills)) {
386
+ extras.skillBlocks = preloadSkills(agentConfig.preloadSkills, cwd);
410
387
  }
411
- if (Array.isArray(skills)) {
412
- extras.skillMetas = loadSkillMeta(skills, effectiveCwd);
388
+ if (Array.isArray(config.skills)) {
389
+ extras.skillMetas = loadSkillMeta(config.skills, cwd);
413
390
  }
414
-
415
- const toolNames = getToolNamesForType(type);
416
-
417
- // Build system prompt from agent config
418
- let systemPrompt: string;
419
391
  if (agentConfig) {
420
- systemPrompt = buildAgentPrompt(agentConfig, effectiveCwd, env, extras);
421
- } else {
422
- // Unknown type fallback: spread the canonical general-purpose config
423
- const fallback = DEFAULT_AGENTS.get("general-purpose");
424
- if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
425
- systemPrompt = buildAgentPrompt({ ...fallback, name: type }, effectiveCwd, env, extras);
392
+ return buildAgentPrompt(agentConfig, cwd, env, extras);
426
393
  }
394
+ const fallback = DEFAULT_AGENTS.get("general-purpose");
395
+ if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
396
+ return buildAgentPrompt({ ...fallback, name: type }, cwd, env, extras);
397
+ }
427
398
 
428
- // Skip the built-in skill loader when:
429
- // - skills is false (no skills)
430
- // - preloadSkills is string[] (we handle preloading ourselves)
431
- // - skills is string[] (we handle metadata ourselves)
432
- const skipSkillLoader = skills === false || Array.isArray(skills) || Array.isArray(preloadSkillsList);
433
-
434
- const agentDir = getAgentDir();
399
+ /** Build extension name tool names map from loaded extensions. */
400
+ function buildExtToolMap(extensions: Array<{ path: string; tools: Map<string, unknown> }>) {
401
+ const map = new Map<string, string[]>();
402
+ for (const ext of extensions) {
403
+ const name = extractExtensionName(ext.path);
404
+ const tools = [...ext.tools.keys()];
405
+ if (tools.length > 0) map.set(name, tools);
406
+ }
407
+ return map;
408
+ }
435
409
 
436
- // Load extensions/skills: true or string[] load; false → don't.
437
- // When extensions is an array, use extensionsOverride to selectively load
438
- // only the listed extensions (hooks/commands of excluded ones never fire).
439
- // When excludeExtensions is set (and extensions is not string[]), filter out those extensions.
440
- const loaderOpts: ConstructorParameters<typeof DefaultResourceLoader>[0] = {
441
- cwd: effectiveCwd,
442
- agentDir,
443
- noExtensions: extensions === false,
444
- noSkills: skipSkillLoader,
445
- noPromptTemplates: true,
446
- noThemes: true,
447
- noContextFiles: true,
448
- systemPromptOverride: () => systemPrompt,
449
- appendSystemPromptOverride: () => [],
450
- };
451
- const excludeExtSet = agentConfig?.excludeExtensions
452
- ? new Set(agentConfig.excludeExtensions)
453
- : undefined;
410
+ /** Build extension override for whitelist or blacklist filtering. */
411
+ function buildExtOverride(
412
+ extensions: true | string[] | false | undefined,
413
+ excludeExtensions?: string[],
414
+ ) {
454
415
  if (Array.isArray(extensions)) {
455
- // Whitelist mode: only load listed extensions
456
416
  const allowedNames = new Set(extensions.map(ext => {
457
417
  const slashIdx = ext.indexOf("/");
458
418
  return slashIdx !== -1 ? ext.slice(0, slashIdx) : ext;
459
419
  }));
460
- loaderOpts.extensionsOverride = (result) => ({
420
+ return (result: any) => ({
461
421
  ...result,
462
- extensions: result.extensions.filter(ext =>
422
+ extensions: result.extensions.filter((ext: { path: string }) =>
463
423
  allowedNames.has(extractExtensionName(ext.path)),
464
424
  ),
465
425
  });
466
- } else if (excludeExtSet) {
467
- // Blacklist mode: load all except excluded extensions
468
- loaderOpts.extensionsOverride = (result) => ({
426
+ }
427
+ if (excludeExtensions) {
428
+ const excludeSet = new Set(excludeExtensions);
429
+ return (result: any) => ({
469
430
  ...result,
470
- extensions: result.extensions.filter(ext =>
471
- !excludeExtSet.has(extractExtensionName(ext.path)),
431
+ extensions: result.extensions.filter((ext: { path: string }) =>
432
+ !excludeSet.has(extractExtensionName(ext.path)),
472
433
  ),
473
434
  });
474
435
  }
475
- const loader = new DefaultResourceLoader(loaderOpts);
476
- await loader.reload();
436
+ return undefined;
437
+ }
477
438
 
478
- // Build extension name → tool names map from loaded extensions.
479
- // Used by filterActiveTools to resolve extension names in the extensions frontmatter field.
480
- const extResult = loader.getExtensions();
481
- const extToolMap = new Map<string, string[]>();
482
- for (const ext of extResult.extensions) {
483
- const name = extractExtensionName(ext.path);
484
- const tools = [...ext.tools.keys()];
485
- if (tools.length > 0) extToolMap.set(name, tools);
486
- }
439
+ /**
440
+ * Phase 2: Build DefaultResourceLoader with extension filtering.
441
+ * Returns the loader and a function that reloads it and builds the ext→tool map.
442
+ */
443
+ function createResourceLoader(
444
+ config: ReturnType<typeof getConfig>,
445
+ agentConfig: ReturnType<typeof getAgentConfig>,
446
+ cwd: string,
447
+ systemPrompt: string,
448
+ ) {
449
+ const extensions = config.extensions;
450
+ const noSkills = config.skills === false
451
+ || Array.isArray(config.skills)
452
+ || Array.isArray(agentConfig?.preloadSkills);
453
+ const agentDir = getAgentDir();
454
+ const loaderOpts: ConstructorParameters<typeof DefaultResourceLoader>[0] = {
455
+ cwd, agentDir,
456
+ noExtensions: extensions === false, noSkills,
457
+ noPromptTemplates: true, noThemes: true, noContextFiles: true,
458
+ systemPromptOverride: () => systemPrompt,
459
+ appendSystemPromptOverride: () => [],
460
+ extensionsOverride: buildExtOverride(extensions, agentConfig?.excludeExtensions),
461
+ };
462
+ const loader = new DefaultResourceLoader(loaderOpts);
463
+ return {
464
+ loader,
465
+ reloadAndMap: async () => {
466
+ await loader.reload();
467
+ const extResult = loader.getExtensions();
468
+ return { extResult, extToolMap: buildExtToolMap(extResult.extensions) };
469
+ },
470
+ };
471
+ }
487
472
 
488
- // Resolve model: explicit option > config.model > parent model
473
+ /** Create an agent session with the resolved model and thinking level. */
474
+ async function initSession(
475
+ ctx: ExtensionContext,
476
+ options: RunOptions,
477
+ agentConfig: ReturnType<typeof getAgentConfig>,
478
+ type: SubagentType,
479
+ cwd: string,
480
+ loader: DefaultResourceLoader,
481
+ ) {
489
482
  const model = options.model ?? findModelInRegistry(
490
483
  agentConfig?.model, ctx.modelRegistry, ctx.model,
491
484
  );
492
-
493
- // Resolve thinking level: explicit option > agent config > undefined (inherit)
494
485
  const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinking;
495
-
486
+ const agentDir = getAgentDir();
496
487
  const sessionOpts: Parameters<typeof createAgentSession>[0] = {
497
- cwd: effectiveCwd,
498
- agentDir,
499
- sessionManager: SessionManager.inMemory(effectiveCwd),
500
- settingsManager: SettingsManager.create(effectiveCwd, agentDir),
501
- modelRegistry: ctx.modelRegistry,
502
- model,
503
- tools: toolNames,
504
- resourceLoader: loader,
488
+ cwd, agentDir,
489
+ sessionManager: SessionManager.inMemory(cwd),
490
+ settingsManager: SettingsManager.create(cwd, agentDir),
491
+ modelRegistry: ctx.modelRegistry, model,
492
+ tools: getToolNamesForType(type), resourceLoader: loader,
505
493
  };
506
- if (thinkingLevel) {
507
- sessionOpts.thinkingLevel = thinkingLevel;
508
- }
509
-
510
- const { session } = await createAgentSession(sessionOpts);
494
+ if (thinkingLevel) sessionOpts.thinkingLevel = thinkingLevel;
495
+ return createAgentSession(sessionOpts);
496
+ }
511
497
 
512
- const baseSessionName = agentConfig?.name ?? type;
498
+ /**
499
+ * Phase 3: Create session, bind extensions, filter tools.
500
+ */
501
+ async function createAndConfigureSession(
502
+ ctx: ExtensionContext,
503
+ options: RunOptions,
504
+ agentConfig: ReturnType<typeof getAgentConfig>,
505
+ type: SubagentType,
506
+ cwd: string,
507
+ loader: DefaultResourceLoader,
508
+ extResult: { extensions: Array<{ path: string; tools: Map<string, unknown> }> },
509
+ notify: (msg: string) => void,
510
+ ): Promise<AgentSession> {
511
+ const { session } = await initSession(ctx, options, agentConfig, type, cwd, loader);
512
+ const baseName = agentConfig?.name ?? type;
513
513
  session.setSessionName(
514
- options.agentId ? `${baseSessionName}#${options.agentId.slice(0, SHORT_ID_LENGTH)}` : baseSessionName,
514
+ options.agentId ? `${baseName}#${options.agentId.slice(0, SHORT_ID_LENGTH)}` : baseName,
515
515
  );
516
-
517
- // Bind extensions so that session_start fires and extensions can initialize
518
- // This must happen BEFORE tool filtering — extensions like pi-mcp-adapter
519
- // register tools lazily during session_start, not at extension load time.
520
516
  await session.bindExtensions({
521
- onError: (err) => {
522
- options.onToolActivity?.({
523
- type: "end",
524
- toolName: `extension-error:${err.extensionPath}`,
525
- });
526
- },
517
+ onError: (err) => options.onToolActivity?.({
518
+ type: "end", toolName: `extension-error:${err.extensionPath}`,
519
+ }),
527
520
  });
528
-
529
- // Rebuild extToolMap after session_start — extensions may have registered
530
- // new tools (e.g., pi-mcp-adapter registers 'mcp' tool at session_start).
531
- const postBindExtToolMap = new Map<string, string[]>();
532
- for (const ext of extResult.extensions) {
533
- const name = extractExtensionName(ext.path);
534
- const tools = [...ext.tools.keys()];
535
- if (tools.length > 0) postBindExtToolMap.set(name, tools);
536
- }
537
-
538
- // Filter active tools: apply tools allowlist/denylist and EXCLUDED_TOOL_NAMES
539
521
  const filteredTools = filterActiveTools(
540
- session.getActiveToolNames(),
541
- postBindExtToolMap,
542
- agentConfig?.tools,
543
- agentConfig?.excludeTools,
544
- (msg) => {
545
- if (ctx.ui?.notify) {
546
- ctx.ui.notify(`[pi-subagents] ${msg}`, "warning");
547
- } else {
548
- console.warn(`[pi-subagents] ${msg}`);
549
- }
550
- },
522
+ session.getActiveToolNames(), buildExtToolMap(extResult.extensions),
523
+ agentConfig?.tools, agentConfig?.excludeTools, notify,
551
524
  );
552
- if (filteredTools) {
553
- session.setActiveToolsByName(filteredTools);
554
- }
555
-
525
+ if (filteredTools) session.setActiveToolsByName(filteredTools);
556
526
  options.onSessionCreated?.(session);
527
+ return session;
528
+ }
557
529
 
558
- // Track turns for graceful max_turns enforcement
530
+ /**
531
+ * Phase 4: Subscribe to turn_end events for graceful max_turns enforcement.
532
+ * Returns an unsubscribe function and state getters.
533
+ */
534
+ function wireTurnTracking(
535
+ session: AgentSession,
536
+ options: Pick<RunOptions, "maxTurns" | "graceTurns" | "onTurnEnd">,
537
+ ) {
559
538
  let turnCount = 0;
560
- const maxTurns = normalizeMaxTurns(options.maxTurns ?? agentConfig?.maxTurns);
539
+ const maxTurns = normalizeMaxTurns(options.maxTurns);
561
540
  let softLimitReached = false;
562
541
  let aborted = false;
563
542
  const graceTurns = options.graceTurns ?? DEFAULT_GRACE_TURNS;
564
543
 
565
- const unsubEvents = subscribeToSessionEvents(session, options);
566
-
567
- const unsubTurns = session.subscribe((event: AgentSessionEvent) => {
568
- if (event.type === "turn_end") {
569
- turnCount++;
570
- options.onTurnEnd?.(turnCount);
571
- if (maxTurns == null) return;
572
- if (!softLimitReached && turnCount >= maxTurns) {
573
- softLimitReached = true;
574
- session.steer("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
575
- } else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
576
- aborted = true;
577
- session.abort();
578
- }
544
+ const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
545
+ if (event.type !== "turn_end") return;
546
+ turnCount++;
547
+ options.onTurnEnd?.(turnCount);
548
+ if (maxTurns == null) return;
549
+ if (!softLimitReached && turnCount >= maxTurns) {
550
+ softLimitReached = true;
551
+ session.steer("You have reached your turn limit. Wrap up immediately — provide your final answer now.");
552
+ } else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
553
+ aborted = true;
554
+ session.abort();
579
555
  }
580
556
  });
581
557
 
558
+ return { unsubscribe, getAborted: () => aborted, getSteered: () => softLimitReached };
559
+ }
560
+
561
+ /**
562
+ * Phase 5: Execute the prompt turn loop with event wiring and cleanup.
563
+ */
564
+ async function runTurnLoop(
565
+ session: AgentSession,
566
+ prompt: string,
567
+ options: RunOptions,
568
+ unsubTurns: () => void,
569
+ ) {
570
+ const unsubEvents = subscribeToSessionEvents(session, options);
582
571
  const collector = collectResponseText(session, options.onTextDelta);
583
572
  const cleanupAbort = forwardAbortSignal(session, options.signal);
584
-
585
573
  try {
586
574
  await session.prompt(prompt);
587
575
  } finally {
@@ -590,7 +578,46 @@ export async function runAgent(
590
578
  collector.unsubscribe();
591
579
  cleanupAbort();
592
580
  }
581
+ return collector.getText().trim() || getLastAssistantText(session);
582
+ }
583
+
584
+ // ── main entry ─────────────────────────────────────────────────────
585
+
586
+ export async function runAgent(
587
+ ctx: ExtensionContext,
588
+ type: SubagentType,
589
+ prompt: string,
590
+ options: RunOptions,
591
+ ): Promise<RunResult> {
592
+ const config = getConfig(type);
593
+ const agentConfig = getAgentConfig(type);
594
+
595
+ // Warn on mutual exclusion violations
596
+ const notify = (msg: string) => {
597
+ if (ctx.ui?.notify) ctx.ui.notify(`[pi-subagents] ${msg}`, "warning");
598
+ else console.warn(`[pi-subagents] ${msg}`);
599
+ };
600
+ if (agentConfig?.excludeTools && Array.isArray(agentConfig.tools)) {
601
+ notify(`agent "${type}": both tools and exclude_tools set — tools (whitelist) wins`);
602
+ }
603
+ if (agentConfig?.excludeExtensions && Array.isArray(agentConfig.extensions)) {
604
+ notify(`agent "${type}": both extensions and exclude_extensions set — extensions (whitelist) wins`);
605
+ }
606
+
607
+ const effectiveCwd = options.cwd ?? ctx.cwd;
608
+ const env = await detectEnv(options.pi, effectiveCwd);
609
+
610
+ const systemPrompt = buildPrompt(type, agentConfig, config, effectiveCwd, env);
611
+ const { loader, reloadAndMap } = createResourceLoader(config, agentConfig, effectiveCwd, systemPrompt);
612
+ const { extResult } = await reloadAndMap();
613
+ const session = await createAndConfigureSession(
614
+ ctx, options, agentConfig, type, effectiveCwd, loader, extResult, notify,
615
+ );
616
+ const { unsubscribe: unsubTurns, getAborted, getSteered } = wireTurnTracking(session, {
617
+ ...options,
618
+ maxTurns: options.maxTurns ?? agentConfig?.maxTurns,
619
+ });
593
620
 
594
- const responseText = collector.getText().trim() || getLastAssistantText(session);
595
- return { responseText, session, aborted, steered: softLimitReached };
621
+ const responseText = await runTurnLoop(session, prompt, options, unsubTurns);
622
+ return { responseText, session, aborted: getAborted(), steered: getSteered() };
596
623
  }
@@ -17,7 +17,7 @@ import type { AgentConfig } from "./types.js";
17
17
  * `find` and `ls` were removed — they're thin wrappers over bash commands
18
18
  * that add ~180 tokens/turn with no real benefit.
19
19
  */
20
- export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep"];
20
+ export const BUILTIN_TOOL_NAMES: string[] = ["read", "bash", "edit", "write", "grep", "find"];
21
21
 
22
22
  /** Unified runtime registry of all agents (defaults + user-defined). */
23
23
  const agents = new Map<string, AgentConfig>();
package/src/config-io.ts CHANGED
@@ -14,7 +14,15 @@ const CONFIG_PATH = path.join(CONFIG_DIR, "subagents-lite.json");
14
14
 
15
15
  /** Default configuration — used when config file doesn't exist or is invalid. */
16
16
  export const DEFAULT_CONFIG: SubagentsConfig = {
17
- agent: { default: null, forceBackground: false, graceTurns: 6 },
17
+ agent: {
18
+ default: null,
19
+ forceBackground: false,
20
+ graceTurns: 6,
21
+ widgetMaxLines: 12,
22
+ // widgetMaxLinesCompact intentionally omitted — derives from widgetMaxLines
23
+ widgetCompact: false,
24
+ widgetShortcut: false,
25
+ },
18
26
  concurrency: { default: 4 },
19
27
  };
20
28
 
@@ -0,0 +1,183 @@
1
+ /**
2
+ * config-mutator.ts — Typed setters for all __config mutations.
3
+ *
4
+ * Every setter saves (saveConfigAtomic) and syncs internally.
5
+ * menus.ts calls setters instead of directly mutating __config.
6
+ *
7
+ * Sync responsibilities:
8
+ * - Widget settings (compact, maxLines, shortcut) → syncWidgetSettings
9
+ * - Cost display → setShowCostEnabled (syncs to widget)
10
+ * - Agent bulk replace → syncWidgetSettings
11
+ * - Concurrency → getManager().setConcurrency()
12
+ * - All others → saveConfigAtomic only
13
+ */
14
+
15
+ import {
16
+ __config,
17
+ getManager,
18
+ setShowCostEnabled,
19
+ syncWidgetSettings,
20
+ } from "./state.js";
21
+ import { saveConfigAtomic, DEFAULT_CONFIG } from "./config-io.js";
22
+ import { CONFIG_AGENT_NON_MODEL_KEYS } from "./types.js";
23
+
24
+ // ============================================================================
25
+ // Local helpers
26
+ // ============================================================================
27
+
28
+ /**
29
+ * Persist concurrency config to disk and apply to the running manager.
30
+ * Defined locally so concurrency setters don't double-save.
31
+ */
32
+ function applyConcurrencyConfig(): void {
33
+ saveConfigAtomic(__config);
34
+ getManager()?.setConcurrency(__config.concurrency);
35
+ }
36
+
37
+ // ============================================================================
38
+ // Model override setters
39
+ // ============================================================================
40
+
41
+ /** Set or update a model override for a type (or "default" for global). */
42
+ export function setModelOverride(type: string, value: string | null): void {
43
+ __config.agent[type] = value;
44
+ saveConfigAtomic(__config);
45
+ }
46
+
47
+ /** Set the global default model. */
48
+ export function setDefaultModel(value: string | null): void {
49
+ __config.agent.default = value;
50
+ saveConfigAtomic(__config);
51
+ }
52
+
53
+ /** Clear a single per-type model override. */
54
+ export function clearModelOverride(type: string): void {
55
+ delete __config.agent[type];
56
+ saveConfigAtomic(__config);
57
+ }
58
+
59
+ /** Clear all model overrides, preserving non-model settings. */
60
+ export function clearAllModelOverrides(): void {
61
+ const preserved: Record<string, unknown> = {};
62
+ for (const key of CONFIG_AGENT_NON_MODEL_KEYS) {
63
+ const val = __config.agent[key];
64
+ if (val != null || key === "default" || key === "forceBackground") {
65
+ preserved[key] = val;
66
+ }
67
+ }
68
+ __config.agent = preserved as typeof __config.agent;
69
+ saveConfigAtomic(__config);
70
+ syncWidgetSettings();
71
+ }
72
+
73
+ // ============================================================================
74
+ // Simple agent settings
75
+ // ============================================================================
76
+
77
+ /** Toggle force-background mode. */
78
+ export function setForceBackground(enabled: boolean): void {
79
+ __config.agent.forceBackground = enabled;
80
+ saveConfigAtomic(__config);
81
+ }
82
+
83
+ /** Set the cost display toggle (syncs to widget via setShowCostEnabled). */
84
+ export function setShowCost(enabled: boolean): void {
85
+ setShowCostEnabled(enabled);
86
+ saveConfigAtomic(__config);
87
+ }
88
+
89
+ /** Set grace turns (number of turns after timeout before hard kill). */
90
+ export function setGraceTurns(n: number): void {
91
+ __config.agent.graceTurns = n;
92
+ saveConfigAtomic(__config);
93
+ }
94
+
95
+ // ============================================================================
96
+ // Widget settings (sync via syncWidgetSettings)
97
+ // ============================================================================
98
+
99
+ /** Toggle force-compact widget mode. */
100
+ export function setWidgetCompact(enabled: boolean): void {
101
+ __config.agent.widgetCompact = enabled;
102
+ saveConfigAtomic(__config);
103
+ syncWidgetSettings();
104
+ }
105
+
106
+ /**
107
+ * Set max lines for full widget mode.
108
+ * Auto-derives widgetMaxLinesCompact if not explicitly set.
109
+ */
110
+ export function setWidgetMaxLines(lines: number): void {
111
+ __config.agent.widgetMaxLines = lines;
112
+ if (__config.agent.widgetMaxLinesCompact === undefined) {
113
+ __config.agent.widgetMaxLinesCompact = Math.floor(lines / 2);
114
+ }
115
+ saveConfigAtomic(__config);
116
+ syncWidgetSettings();
117
+ }
118
+
119
+ /** Set max lines for compact widget mode. */
120
+ export function setWidgetMaxLinesCompact(lines: number): void {
121
+ __config.agent.widgetMaxLinesCompact = lines;
122
+ saveConfigAtomic(__config);
123
+ syncWidgetSettings();
124
+ }
125
+
126
+ /** Toggle ctrl+o widget shortcut. */
127
+ export function setWidgetShortcut(enabled: boolean): void {
128
+ __config.agent.widgetShortcut = enabled;
129
+ saveConfigAtomic(__config);
130
+ }
131
+
132
+ /** Replace the entire agent config object (used by "clear all overrides"). */
133
+ export function setAgent(agent: typeof __config.agent): void {
134
+ __config.agent = agent;
135
+ saveConfigAtomic(__config);
136
+ syncWidgetSettings();
137
+ }
138
+
139
+ // ============================================================================
140
+ // Concurrency setters (save + sync via getManager().setConcurrency)
141
+ // ============================================================================
142
+
143
+ /** Set the global concurrency default. */
144
+ export function setConcurrencyDefault(n: number): void {
145
+ __config.concurrency.default = n;
146
+ applyConcurrencyConfig();
147
+ }
148
+
149
+ /** Set or update a per-provider concurrency limit. */
150
+ export function setConcurrencyProvider(key: string, n: number): void {
151
+ const current = __config.concurrency.providers ?? {};
152
+ __config.concurrency.providers = { ...current, [key]: n };
153
+ applyConcurrencyConfig();
154
+ }
155
+
156
+ /** Set or update a per-model concurrency limit. */
157
+ export function setConcurrencyModel(key: string, n: number): void {
158
+ const current = __config.concurrency.models ?? {};
159
+ __config.concurrency.models = { ...current, [key]: n };
160
+ applyConcurrencyConfig();
161
+ }
162
+
163
+ /** Remove a per-provider concurrency limit. */
164
+ export function removeConcurrencyProvider(key: string): void {
165
+ if (__config.concurrency.providers) {
166
+ delete __config.concurrency.providers[key];
167
+ }
168
+ applyConcurrencyConfig();
169
+ }
170
+
171
+ /** Remove a per-model concurrency limit. */
172
+ export function removeConcurrencyModel(key: string): void {
173
+ if (__config.concurrency.models) {
174
+ delete __config.concurrency.models[key];
175
+ }
176
+ applyConcurrencyConfig();
177
+ }
178
+
179
+ /** Reset all concurrency settings to defaults. */
180
+ export function resetConcurrency(): void {
181
+ __config.concurrency = { ...DEFAULT_CONFIG.concurrency };
182
+ applyConcurrencyConfig();
183
+ }
package/src/context.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * buildSnapshotMarkdown: format agent conversation as markdown for snapshot viewer.
6
6
  */
7
7
 
8
- import { summarizeToolArgs } from "./output-file.js";
8
+ import { summarizeToolArgs } from "./format.js";
9
9
 
10
10
  function isTextBlock(c: unknown): c is { type: "text"; text: string } {
11
11
  return typeof c === "object" && c !== null && (c as Record<string, unknown>).type === "text";
@@ -7,7 +7,7 @@
7
7
 
8
8
  import type { AgentConfig } from "./types.js";
9
9
 
10
- const READ_ONLY_TOOLS = ["read", "bash", "grep"];
10
+ const READ_ONLY_TOOLS = ["read", "bash", "grep", "find"];
11
11
 
12
12
  export const DEFAULT_AGENTS: Map<string, AgentConfig> = new Map([
13
13
  [