@dungle-scrubs/tallow 0.8.26 → 0.8.28
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 +42 -1
- package/dist/cli.js +7 -1
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/interactive-mode-patch.d.ts +1 -0
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +40 -1
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +2 -5
- package/dist/model-metadata-overrides.d.ts.map +1 -1
- package/dist/model-metadata-overrides.js +23 -12
- package/dist/model-metadata-overrides.js.map +1 -1
- package/dist/pid-manager.d.ts +2 -9
- package/dist/pid-manager.d.ts.map +1 -1
- package/dist/pid-manager.js +1 -58
- package/dist/pid-manager.js.map +1 -1
- package/dist/pid-schema.d.ts +51 -0
- package/dist/pid-schema.d.ts.map +1 -0
- package/dist/pid-schema.js +70 -0
- package/dist/pid-schema.js.map +1 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +24 -17
- package/dist/sdk.js.map +1 -1
- package/dist/workspace-transition-interactive.d.ts.map +1 -1
- package/dist/workspace-transition-interactive.js +53 -3
- package/dist/workspace-transition-interactive.js.map +1 -1
- package/dist/workspace-transition.d.ts +2 -1
- package/dist/workspace-transition.d.ts.map +1 -1
- package/dist/workspace-transition.js +16 -4
- package/dist/workspace-transition.js.map +1 -1
- package/extensions/__integration__/audit-findings.test.ts +309 -0
- package/extensions/__integration__/cd-tool-guidelines.test.ts +46 -0
- package/extensions/__integration__/tasks-runtime.test.ts +63 -12
- package/extensions/__integration__/welcome-screen.test.ts +240 -0
- package/extensions/_shared/lazy-init.ts +88 -3
- package/extensions/_shared/pid-registry.ts +8 -82
- package/extensions/background-task-tool/index.ts +1 -1
- package/extensions/cd-tool/index.ts +4 -1
- package/extensions/cheatsheet/__tests__/cheatsheet.test.ts +47 -0
- package/extensions/clear/__tests__/clear.test.ts +38 -0
- package/extensions/edit-tool-enhanced/index.ts +3 -1
- package/extensions/git-status/__tests__/git-status.test.ts +32 -0
- package/extensions/health/__tests__/diagnostics.test.ts +25 -0
- package/extensions/health/index.ts +61 -0
- package/extensions/loop/__tests__/loop.test.ts +365 -1
- package/extensions/loop/index.ts +213 -3
- package/extensions/mcp-adapter-tool/index.ts +1 -1
- package/extensions/minimal-skill-display/__tests__/minimal-skill-display.test.ts +20 -0
- package/extensions/permissions/__tests__/permissions.test.ts +213 -0
- package/extensions/progress-indicator/__tests__/progress-indicator.test.ts +104 -0
- package/extensions/prompt-suggestions/__tests__/autocomplete.test.ts +111 -3
- package/extensions/prompt-suggestions/autocomplete.ts +23 -5
- package/extensions/prompt-suggestions/index.ts +62 -3
- package/extensions/random-spinner/__tests__/random-spinner.test.ts +35 -0
- package/extensions/read-tool-enhanced/index.ts +5 -1
- package/extensions/session-memory/index.ts +1 -1
- package/extensions/session-namer/index.ts +1 -1
- package/extensions/show-system-prompt/__tests__/show-system-prompt.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/presentation-rendering.test.ts +9 -8
- package/extensions/subagent-tool/__tests__/process-liveness.test.ts +51 -0
- package/extensions/subagent-tool/__tests__/subprocess-args.test.ts +120 -0
- package/extensions/subagent-tool/formatting.ts +2 -0
- package/extensions/subagent-tool/index.ts +160 -97
- package/extensions/subagent-tool/process.ts +152 -40
- package/extensions/tasks/commands/register-tasks-extension.ts +64 -20
- package/extensions/tasks/extension.json +1 -0
- package/extensions/tasks/index.ts +2 -12
- package/extensions/tasks/state/index.ts +26 -0
- package/extensions/teams-tool/dashboard.ts +13 -1
- package/extensions/teams-tool/sessions/spawn.ts +2 -2
- package/extensions/teams-tool/tools/register-extension.ts +10 -2
- package/extensions/upstream-check/__tests__/upstream-check.test.ts +49 -0
- package/extensions/welcome-screen/__tests__/welcome-screen.test.ts +35 -0
- package/extensions/welcome-screen/extension.json +20 -0
- package/extensions/welcome-screen/index.ts +189 -0
- package/extensions/wezterm-notify/__tests__/index.test.ts +49 -11
- package/extensions/wezterm-notify/index.ts +5 -3
- package/extensions/write-tool-enhanced/__tests__/write-tool-enhanced.test.ts +296 -0
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/index.js +2 -2
- package/node_modules/@mariozechner/pi-tui/dist/index.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts +309 -25
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js +392 -72
- package/node_modules/@mariozechner/pi-tui/dist/keybindings.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts +30 -0
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +50 -6
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts +27 -0
- package/node_modules/@mariozechner/pi-tui/dist/terminal.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js +59 -4
- package/node_modules/@mariozechner/pi-tui/dist/terminal.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts +9 -0
- package/node_modules/@mariozechner/pi-tui/dist/tui.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js +50 -1
- package/node_modules/@mariozechner/pi-tui/dist/tui.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/__tests__/mouse-events.test.ts +134 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tmux-compat.test.ts +204 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-diff-regression.test.ts +49 -0
- package/node_modules/@mariozechner/pi-tui/src/__tests__/tui-render-scheduling.test.ts +2 -0
- package/node_modules/@mariozechner/pi-tui/src/index.ts +11 -0
- package/node_modules/@mariozechner/pi-tui/src/keybindings.ts +478 -140
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +84 -6
- package/node_modules/@mariozechner/pi-tui/src/terminal.ts +69 -4
- package/node_modules/@mariozechner/pi-tui/src/tui.ts +64 -1
- package/package.json +11 -10
- package/runtime/config.ts +7 -0
- package/runtime/model-metadata-overrides.ts +7 -0
- package/runtime/pid-schema.ts +13 -0
- package/skills/tallow-expert/SKILL.md +7 -5
|
@@ -449,6 +449,7 @@ export interface ForegroundWatchdogThresholds {
|
|
|
449
449
|
readonly killGraceMs: number;
|
|
450
450
|
readonly startupTimeoutMs: number;
|
|
451
451
|
readonly toolExecutionTimeoutMs: number;
|
|
452
|
+
readonly wallClockTimeoutMs: number;
|
|
452
453
|
}
|
|
453
454
|
|
|
454
455
|
/** Heartbeat state tracked by the foreground subagent liveness watchdog. */
|
|
@@ -464,7 +465,7 @@ export type WatchdogStatus =
|
|
|
464
465
|
| {
|
|
465
466
|
readonly elapsedMs: number;
|
|
466
467
|
readonly kind: "stalled";
|
|
467
|
-
readonly phase: "inactivity" | "startup" | "tool_execution";
|
|
468
|
+
readonly phase: "inactivity" | "startup" | "tool_execution" | "wall_clock";
|
|
468
469
|
readonly timeoutMs: number;
|
|
469
470
|
};
|
|
470
471
|
|
|
@@ -477,17 +478,33 @@ export const SUBAGENT_INACTIVITY_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_INACTIVITY_TI
|
|
|
477
478
|
/** Env var overriding the foreground timeout while a tool call is still running. */
|
|
478
479
|
export const SUBAGENT_TOOL_EXECUTION_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_TOOL_EXECUTION_TIMEOUT_MS";
|
|
479
480
|
|
|
481
|
+
/** Env var overriding the total wall-clock timeout per foreground subagent. */
|
|
482
|
+
export const SUBAGENT_WALL_CLOCK_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_WALL_CLOCK_TIMEOUT_MS";
|
|
483
|
+
|
|
480
484
|
/** Env var overriding the SIGTERM → SIGKILL grace window for stalled workers. */
|
|
481
485
|
export const SUBAGENT_WATCHDOG_KILL_GRACE_MS_ENV = "TALLOW_SUBAGENT_WATCHDOG_KILL_GRACE_MS";
|
|
482
486
|
|
|
487
|
+
/** Env var overriding the total wall-clock timeout for the stalled-worker retry phase. */
|
|
488
|
+
export const SUBAGENT_RETRY_PHASE_TIMEOUT_MS_ENV = "TALLOW_SUBAGENT_RETRY_PHASE_TIMEOUT_MS";
|
|
489
|
+
|
|
483
490
|
/** Default watchdog thresholds used by foreground subagents in runSingleAgent. */
|
|
484
491
|
export const FOREGROUND_WATCHDOG_THRESHOLDS: ForegroundWatchdogThresholds = {
|
|
485
|
-
inactivityTimeoutMs:
|
|
492
|
+
inactivityTimeoutMs: 120_000, // 2 min without any heartbeat event
|
|
486
493
|
killGraceMs: 5_000,
|
|
487
|
-
startupTimeoutMs:
|
|
488
|
-
toolExecutionTimeoutMs:
|
|
494
|
+
startupTimeoutMs: 30_000, // 30s to emit first event
|
|
495
|
+
toolExecutionTimeoutMs: 300_000, // 5 min per tool call (was 10 min)
|
|
496
|
+
wallClockTimeoutMs: 480_000, // 8 min total (was 15 min)
|
|
489
497
|
};
|
|
490
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Default total wall-clock timeout for the entire stalled-worker retry phase.
|
|
501
|
+
*
|
|
502
|
+
* Without a cap, N stalled retries × per-worker watchdog timeout can block
|
|
503
|
+
* the parent for 30+ minutes. This limits the total retry phase so the
|
|
504
|
+
* parent agent can recover sooner.
|
|
505
|
+
*/
|
|
506
|
+
export const DEFAULT_RETRY_PHASE_TIMEOUT_MS = 180_000; // 3 minutes
|
|
507
|
+
|
|
491
508
|
/** How often the foreground watchdog checks for stalled subagents. */
|
|
492
509
|
const FOREGROUND_WATCHDOG_CHECK_INTERVAL_MS = 500;
|
|
493
510
|
|
|
@@ -532,9 +549,25 @@ export function resolveForegroundWatchdogThresholds(
|
|
|
532
549
|
toolExecutionTimeoutMs:
|
|
533
550
|
parseTimeoutOverrideMs(env[SUBAGENT_TOOL_EXECUTION_TIMEOUT_MS_ENV]) ??
|
|
534
551
|
FOREGROUND_WATCHDOG_THRESHOLDS.toolExecutionTimeoutMs,
|
|
552
|
+
wallClockTimeoutMs:
|
|
553
|
+
parseTimeoutOverrideMs(env[SUBAGENT_WALL_CLOCK_TIMEOUT_MS_ENV]) ??
|
|
554
|
+
FOREGROUND_WATCHDOG_THRESHOLDS.wallClockTimeoutMs,
|
|
535
555
|
};
|
|
536
556
|
}
|
|
537
557
|
|
|
558
|
+
/**
|
|
559
|
+
* Resolve the effective retry-phase wall-clock timeout from env overrides.
|
|
560
|
+
*
|
|
561
|
+
* @param env - Environment lookup map
|
|
562
|
+
* @returns Timeout in milliseconds for the entire stalled-worker retry phase
|
|
563
|
+
*/
|
|
564
|
+
export function resolveRetryPhaseTimeoutMs(env: EnvLookup = process.env): number {
|
|
565
|
+
return (
|
|
566
|
+
parseTimeoutOverrideMs(env[SUBAGENT_RETRY_PHASE_TIMEOUT_MS_ENV]) ??
|
|
567
|
+
DEFAULT_RETRY_PHASE_TIMEOUT_MS
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
538
571
|
/**
|
|
539
572
|
* Return whether an event type counts as watchdog progress.
|
|
540
573
|
* @param eventType - Raw child-process event type
|
|
@@ -619,6 +652,18 @@ export function evaluateWatchdogStatus(
|
|
|
619
652
|
nowMs: number,
|
|
620
653
|
thresholds: ForegroundWatchdogThresholds
|
|
621
654
|
): WatchdogStatus {
|
|
655
|
+
// Wall-clock timeout: hard cap on total execution time, regardless of activity.
|
|
656
|
+
// Catches "slow but active" agents that keep making tool calls without finishing.
|
|
657
|
+
const totalElapsedMs = nowMs - state.startedAtMs;
|
|
658
|
+
if (totalElapsedMs >= thresholds.wallClockTimeoutMs) {
|
|
659
|
+
return {
|
|
660
|
+
elapsedMs: totalElapsedMs,
|
|
661
|
+
kind: "stalled",
|
|
662
|
+
phase: "wall_clock",
|
|
663
|
+
timeoutMs: thresholds.wallClockTimeoutMs,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
|
|
622
667
|
if (state.lastHeartbeatAtMs === null) {
|
|
623
668
|
const startupElapsedMs = nowMs - state.startedAtMs;
|
|
624
669
|
if (startupElapsedMs >= thresholds.startupTimeoutMs) {
|
|
@@ -655,6 +700,15 @@ export function createStalledSubagentErrorMessage(
|
|
|
655
700
|
stalledStatus: Extract<WatchdogStatus, { kind: "stalled" }>
|
|
656
701
|
): string {
|
|
657
702
|
const timeoutSeconds = Math.max(1, Math.round(stalledStatus.timeoutMs / 1000));
|
|
703
|
+
const timeoutMinutes = Math.round(timeoutSeconds / 60);
|
|
704
|
+
if (stalledStatus.phase === "wall_clock") {
|
|
705
|
+
return (
|
|
706
|
+
`Subagent terminated after ${timeoutMinutes}m wall-clock timeout. ` +
|
|
707
|
+
"The agent was still active but exceeded the maximum allowed execution time. " +
|
|
708
|
+
"Action: break the task into smaller pieces, or increase " +
|
|
709
|
+
"TALLOW_SUBAGENT_WALL_CLOCK_TIMEOUT_MS for legitimately long work."
|
|
710
|
+
);
|
|
711
|
+
}
|
|
658
712
|
const phaseDescription =
|
|
659
713
|
stalledStatus.phase === "startup"
|
|
660
714
|
? "no startup activity was received"
|
|
@@ -743,6 +797,54 @@ export function terminateProcessWithGrace(
|
|
|
743
797
|
/** Callback for streaming partial results during subagent execution. */
|
|
744
798
|
export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
745
799
|
|
|
800
|
+
// ── Subprocess Arg Construction ──────────────────────────────────────────────
|
|
801
|
+
|
|
802
|
+
/** Options for building subprocess CLI arguments. */
|
|
803
|
+
export interface SubprocessArgsOptions {
|
|
804
|
+
/** Session file path for persistent teammates (omit for --no-session). */
|
|
805
|
+
session?: string;
|
|
806
|
+
/** Provider-qualified model display name (e.g. "anthropic/claude-sonnet-4-6"). */
|
|
807
|
+
modelDisplayName?: string;
|
|
808
|
+
/** Effective tool allowlist (already filtered by denylist). */
|
|
809
|
+
tools?: string[];
|
|
810
|
+
/** Skill names to pass via --skill flags. */
|
|
811
|
+
skills?: string[];
|
|
812
|
+
/** Path to temp file containing the system prompt. */
|
|
813
|
+
systemPromptPath?: string;
|
|
814
|
+
/** Task text (will be prefixed with "Task: "). */
|
|
815
|
+
task: string;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Build the CLI argument array for a subagent child process.
|
|
820
|
+
*
|
|
821
|
+
* **Invariant:** `-p <task>` is always the last pair in the array.
|
|
822
|
+
* Commander consumes the next token after `-p` as its required `<prompt>`
|
|
823
|
+
* value. Placing `-p` before other flags (e.g. `--no-session`) causes
|
|
824
|
+
* Commander to swallow the flag as the prompt text, leaving the real
|
|
825
|
+
* task as a stray positional argument.
|
|
826
|
+
*
|
|
827
|
+
* @param opts - Subprocess argument options
|
|
828
|
+
* @returns CLI argument array safe for child_process.spawn
|
|
829
|
+
*/
|
|
830
|
+
export function buildSubprocessArgs(opts: SubprocessArgsOptions): string[] {
|
|
831
|
+
const args: string[] = opts.session
|
|
832
|
+
? ["--mode", "json", "--session", opts.session]
|
|
833
|
+
: ["--mode", "json", "--no-session"];
|
|
834
|
+
|
|
835
|
+
if (opts.modelDisplayName) args.push("--model", opts.modelDisplayName);
|
|
836
|
+
if (opts.tools && opts.tools.length > 0) args.push("--tools", opts.tools.join(","));
|
|
837
|
+
if (opts.skills && opts.skills.length > 0) {
|
|
838
|
+
for (const skill of opts.skills) args.push("--skill", skill);
|
|
839
|
+
}
|
|
840
|
+
if (opts.systemPromptPath) args.push("--append-system-prompt", opts.systemPromptPath);
|
|
841
|
+
|
|
842
|
+
// CRITICAL: -p must be last — see JSDoc above.
|
|
843
|
+
args.push("-p", `Task: ${opts.task}`);
|
|
844
|
+
|
|
845
|
+
return args;
|
|
846
|
+
}
|
|
847
|
+
|
|
746
848
|
// ── Background Spawning ──────────────────────────────────────────────────────
|
|
747
849
|
|
|
748
850
|
/**
|
|
@@ -813,18 +915,6 @@ export async function spawnBackgroundSubagent(
|
|
|
813
915
|
const agent = { ...resolved.agent, model: routing.model.id };
|
|
814
916
|
const agentSource = resolved.resolution === "ephemeral" ? ("ephemeral" as const) : agent.source;
|
|
815
917
|
|
|
816
|
-
const args: string[] = session
|
|
817
|
-
? ["--mode", "json", "-p", "--session", session]
|
|
818
|
-
: ["--mode", "json", "-p", "--no-session"];
|
|
819
|
-
// Use provider-qualified name (e.g. "openai-codex/gpt-5.1") so the child process
|
|
820
|
-
// resolves to the exact provider the router selected, not just the first match.
|
|
821
|
-
if (agent.model) args.push("--model", routing.model.displayName);
|
|
822
|
-
const effectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
823
|
-
if (effectiveTools && effectiveTools.length > 0) args.push("--tools", effectiveTools.join(","));
|
|
824
|
-
if (agent.skills && agent.skills.length > 0) {
|
|
825
|
-
for (const skill of agent.skills) args.push("--skill", skill);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
918
|
let tmpPromptDir: string | undefined;
|
|
829
919
|
let tmpPromptPath: string | undefined;
|
|
830
920
|
|
|
@@ -839,7 +929,6 @@ export async function spawnBackgroundSubagent(
|
|
|
839
929
|
const tmp = writePromptToTempFile(agent.name, systemPrompt);
|
|
840
930
|
tmpPromptDir = tmp.dir;
|
|
841
931
|
tmpPromptPath = tmp.filePath;
|
|
842
|
-
args.push("--append-system-prompt", tmpPromptPath);
|
|
843
932
|
}
|
|
844
933
|
|
|
845
934
|
let expandedTask: string;
|
|
@@ -850,7 +939,16 @@ export async function spawnBackgroundSubagent(
|
|
|
850
939
|
const reason = error instanceof Error ? error.message : String(error);
|
|
851
940
|
return `Failed to expand task references for ${agentName}: ${reason}`;
|
|
852
941
|
}
|
|
853
|
-
|
|
942
|
+
|
|
943
|
+
const effectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
944
|
+
const args = buildSubprocessArgs({
|
|
945
|
+
session,
|
|
946
|
+
modelDisplayName: agent.model ? routing.model.displayName : undefined,
|
|
947
|
+
tools: effectiveTools && effectiveTools.length > 0 ? effectiveTools : undefined,
|
|
948
|
+
skills: agent.skills && agent.skills.length > 0 ? agent.skills : undefined,
|
|
949
|
+
systemPromptPath: tmpPromptPath,
|
|
950
|
+
task: expandedTask,
|
|
951
|
+
});
|
|
854
952
|
|
|
855
953
|
const childEnv: Record<string, string> = { ...process.env, PI_IS_SUBAGENT: "1" } as Record<
|
|
856
954
|
string,
|
|
@@ -951,8 +1049,13 @@ export async function spawnBackgroundSubagent(
|
|
|
951
1049
|
buffer = lines.pop() || "";
|
|
952
1050
|
for (const line of lines) {
|
|
953
1051
|
if (!line.trim()) continue;
|
|
1052
|
+
// Strip leading terminal escape sequences — see foreground processLine.
|
|
1053
|
+
let cleaned = line;
|
|
1054
|
+
const jsonStart = cleaned.indexOf("{");
|
|
1055
|
+
if (jsonStart > 0) cleaned = cleaned.slice(jsonStart);
|
|
1056
|
+
else if (jsonStart < 0) continue;
|
|
954
1057
|
try {
|
|
955
|
-
const event = JSON.parse(
|
|
1058
|
+
const event = JSON.parse(cleaned);
|
|
956
1059
|
|
|
957
1060
|
// Emit subagent_tool_call when tool starts
|
|
958
1061
|
if (event.type === "tool_call_start") {
|
|
@@ -1022,13 +1125,18 @@ export async function spawnBackgroundSubagent(
|
|
|
1022
1125
|
|
|
1023
1126
|
proc.on("close", (code) => {
|
|
1024
1127
|
if (buffer.trim()) {
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1128
|
+
let cleaned = buffer;
|
|
1129
|
+
const jsonStart = cleaned.indexOf("{");
|
|
1130
|
+
if (jsonStart > 0) cleaned = cleaned.slice(jsonStart);
|
|
1131
|
+
if (jsonStart >= 0) {
|
|
1132
|
+
try {
|
|
1133
|
+
const event = JSON.parse(cleaned);
|
|
1134
|
+
if (event.type === "message_end" && event.message) {
|
|
1135
|
+
result.messages.push(event.message);
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
/* ignore */
|
|
1029
1139
|
}
|
|
1030
|
-
} catch {
|
|
1031
|
-
/* ignore */
|
|
1032
1140
|
}
|
|
1033
1141
|
}
|
|
1034
1142
|
const finalOutput = getFinalOutput(result.messages);
|
|
@@ -1236,18 +1344,6 @@ export async function runSingleAgent(
|
|
|
1236
1344
|
background: false,
|
|
1237
1345
|
} satisfies SubagentStartEvent);
|
|
1238
1346
|
|
|
1239
|
-
const args: string[] = session
|
|
1240
|
-
? ["--mode", "json", "-p", "--session", session]
|
|
1241
|
-
: ["--mode", "json", "-p", "--no-session"];
|
|
1242
|
-
// Use provider-qualified name so the child process resolves to the exact provider.
|
|
1243
|
-
if (agent.model) args.push("--model", routing.model.displayName);
|
|
1244
|
-
const fgEffectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
1245
|
-
if (fgEffectiveTools && fgEffectiveTools.length > 0)
|
|
1246
|
-
args.push("--tools", fgEffectiveTools.join(","));
|
|
1247
|
-
if (agent.skills && agent.skills.length > 0) {
|
|
1248
|
-
for (const skill of agent.skills) args.push("--skill", skill);
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1251
1347
|
let tmpPromptDir: string | null = null;
|
|
1252
1348
|
let tmpPromptPath: string | null = null;
|
|
1253
1349
|
|
|
@@ -1290,6 +1386,7 @@ export async function runSingleAgent(
|
|
|
1290
1386
|
},
|
|
1291
1387
|
model: agent.model,
|
|
1292
1388
|
step,
|
|
1389
|
+
startTime: Date.now(),
|
|
1293
1390
|
};
|
|
1294
1391
|
|
|
1295
1392
|
/** Timestamp of the last emitted update, used for throttling. */
|
|
@@ -1327,11 +1424,18 @@ export async function runSingleAgent(
|
|
|
1327
1424
|
const tmp = writePromptToTempFile(agent.name, fgSystemPrompt);
|
|
1328
1425
|
tmpPromptDir = tmp.dir;
|
|
1329
1426
|
tmpPromptPath = tmp.filePath;
|
|
1330
|
-
args.push("--append-system-prompt", tmpPromptPath);
|
|
1331
1427
|
}
|
|
1332
1428
|
|
|
1333
1429
|
const expandedTask = await expandFileReferences(task, effectiveCwd);
|
|
1334
|
-
|
|
1430
|
+
const fgEffectiveTools = computeEffectiveTools(agent.tools, agent.disallowedTools);
|
|
1431
|
+
const args = buildSubprocessArgs({
|
|
1432
|
+
session,
|
|
1433
|
+
modelDisplayName: agent.model ? routing.model.displayName : undefined,
|
|
1434
|
+
tools: fgEffectiveTools && fgEffectiveTools.length > 0 ? fgEffectiveTools : undefined,
|
|
1435
|
+
skills: agent.skills && agent.skills.length > 0 ? agent.skills : undefined,
|
|
1436
|
+
systemPromptPath: tmpPromptPath ?? undefined,
|
|
1437
|
+
task: expandedTask,
|
|
1438
|
+
});
|
|
1335
1439
|
let wasAborted = false;
|
|
1336
1440
|
|
|
1337
1441
|
const fgChildEnv: Record<string, string> = {
|
|
@@ -1414,10 +1518,18 @@ export async function runSingleAgent(
|
|
|
1414
1518
|
|
|
1415
1519
|
const processLine = (line: string) => {
|
|
1416
1520
|
if (!line.trim()) return;
|
|
1521
|
+
// Strip leading terminal escape sequences (e.g. OSC 1337 SetUserVar)
|
|
1522
|
+
// that may leak into stdout when extensions write directly to
|
|
1523
|
+
// process.stdout in JSON-mode child processes. Without this,
|
|
1524
|
+
// JSON.parse silently fails and heartbeat events are lost.
|
|
1525
|
+
let cleaned = line;
|
|
1526
|
+
const jsonStart = cleaned.indexOf("{");
|
|
1527
|
+
if (jsonStart > 0) cleaned = cleaned.slice(jsonStart);
|
|
1528
|
+
else if (jsonStart < 0) return;
|
|
1417
1529
|
// biome-ignore lint/suspicious/noExplicitAny: pi subagent JSON protocol has dynamic shape
|
|
1418
1530
|
let event: Record<string, any>;
|
|
1419
1531
|
try {
|
|
1420
|
-
event = JSON.parse(
|
|
1532
|
+
event = JSON.parse(cleaned);
|
|
1421
1533
|
} catch {
|
|
1422
1534
|
return;
|
|
1423
1535
|
}
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
import { findCompletedTasks } from "../parsing/index.js";
|
|
33
33
|
import {
|
|
34
34
|
type BgTaskView,
|
|
35
|
+
buildSessionTaskGroupName,
|
|
35
36
|
cleanupStaleTeams,
|
|
36
37
|
getTextContent,
|
|
37
38
|
isAssistantMessage,
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
shouldClearOnAgentEnd,
|
|
42
43
|
type Task,
|
|
43
44
|
type TaskComment,
|
|
44
|
-
|
|
45
|
+
TaskListStore,
|
|
45
46
|
type TaskStatus,
|
|
46
47
|
type TasksState,
|
|
47
48
|
type TeamWidgetView,
|
|
@@ -75,15 +76,17 @@ const agentIdentities = new Map<string, AgentIdentity>();
|
|
|
75
76
|
* Registers task management tools, commands, and widget.
|
|
76
77
|
*
|
|
77
78
|
* @param pi - Extension API for registering tools, commands, and event handlers
|
|
78
|
-
* @param
|
|
79
|
-
* @param
|
|
79
|
+
* @param initialStore - Initial {@link TaskListStore} for file persistence
|
|
80
|
+
* @param initialTeamName - Active team name (or null for session-only mode)
|
|
80
81
|
*/
|
|
81
82
|
export function registerTasksExtension(
|
|
82
83
|
pi: ExtensionAPI,
|
|
83
|
-
|
|
84
|
-
|
|
84
|
+
initialStore: TaskListStore,
|
|
85
|
+
initialTeamName: string | null
|
|
85
86
|
): void {
|
|
86
87
|
const isSubagent = process.env.PI_IS_SUBAGENT === "1";
|
|
88
|
+
let store = initialStore;
|
|
89
|
+
let teamName = initialTeamName;
|
|
87
90
|
const state: TasksState = {
|
|
88
91
|
tasks: [],
|
|
89
92
|
visible: true,
|
|
@@ -1023,9 +1026,7 @@ export function registerTasksExtension(
|
|
|
1023
1026
|
return `${t.id}. ${icon} ${t.subject}${blocked}${comments}`;
|
|
1024
1027
|
})
|
|
1025
1028
|
.join("\n");
|
|
1026
|
-
const mode = store.isShared
|
|
1027
|
-
? ` [team: ${process.env.PI_TEAM_NAME}]`
|
|
1028
|
-
: " [session-only]";
|
|
1029
|
+
const mode = store.isShared ? ` [team: ${teamName}]` : " [session-only]";
|
|
1029
1030
|
ctx.ui.notify(`Tasks${mode}:\n${list}`, "info");
|
|
1030
1031
|
break;
|
|
1031
1032
|
}
|
|
@@ -1091,7 +1092,7 @@ export function registerTasksExtension(
|
|
|
1091
1092
|
}
|
|
1092
1093
|
|
|
1093
1094
|
case "team": {
|
|
1094
|
-
const current = store.isShared ?
|
|
1095
|
+
const current = store.isShared ? teamName : "(none — session-only)";
|
|
1095
1096
|
const teamPath = store.path ?? "N/A";
|
|
1096
1097
|
ctx.ui.notify(`Shared task group: ${current}\nPath: ${teamPath}`, "info");
|
|
1097
1098
|
break;
|
|
@@ -1916,8 +1917,37 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
|
|
|
1916
1917
|
let lastBgCount = 0;
|
|
1917
1918
|
let lastBgTaskCount = 0;
|
|
1918
1919
|
|
|
1919
|
-
|
|
1920
|
-
|
|
1920
|
+
/** Resolve the shared task-group name for the current session context.
|
|
1921
|
+
* @param ctx - Active extension context
|
|
1922
|
+
* @returns Shared task-group name, or null for session-only mode
|
|
1923
|
+
*/
|
|
1924
|
+
function resolveTaskGroupName(ctx: ExtensionContext): string | null {
|
|
1925
|
+
if (isSubagent) return process.env.PI_TEAM_NAME ?? teamName;
|
|
1926
|
+
const sessionId = ctx.sessionManager.getSessionId?.();
|
|
1927
|
+
if (!sessionId) return null;
|
|
1928
|
+
return buildSessionTaskGroupName(sessionId, ctx.cwd);
|
|
1929
|
+
}
|
|
1930
|
+
/** Rebind the file-backed task store for the current session.
|
|
1931
|
+
* @param ctx - Active extension context
|
|
1932
|
+
* @returns Nothing
|
|
1933
|
+
*/
|
|
1934
|
+
function syncTaskGroupStore(ctx: ExtensionContext): void {
|
|
1935
|
+
const nextTeamName = resolveTaskGroupName(ctx);
|
|
1936
|
+
store.close();
|
|
1937
|
+
if (nextTeamName !== teamName) {
|
|
1938
|
+
store = new TaskListStore(nextTeamName);
|
|
1939
|
+
teamName = nextTeamName;
|
|
1940
|
+
}
|
|
1941
|
+
if (!isSubagent) {
|
|
1942
|
+
if (teamName) process.env.PI_TEAM_NAME = teamName;
|
|
1943
|
+
else delete process.env.PI_TEAM_NAME;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
/** Reset runtime widget/session state before loading a different session.
|
|
1948
|
+
* @returns Nothing
|
|
1949
|
+
*/
|
|
1950
|
+
function resetSessionState(): void {
|
|
1921
1951
|
foregroundSubagents = [];
|
|
1922
1952
|
backgroundSubagents = [];
|
|
1923
1953
|
backgroundTasks = [];
|
|
@@ -1926,7 +1956,21 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
|
|
|
1926
1956
|
lastBackgroundTaskPresenterState = null;
|
|
1927
1957
|
lastBgCount = 0;
|
|
1928
1958
|
lastBgTaskCount = 0;
|
|
1959
|
+
agentActivity.clear();
|
|
1960
|
+
agentIdentities.clear();
|
|
1961
|
+
state.tasks = [];
|
|
1962
|
+
state.visible = true;
|
|
1963
|
+
state.nextId = 1;
|
|
1964
|
+
state.activeTaskId = null;
|
|
1965
|
+
}
|
|
1929
1966
|
|
|
1967
|
+
/** Restore tasks/widget state for the current session or switched session.
|
|
1968
|
+
* @param ctx - Active extension context
|
|
1969
|
+
* @returns Nothing
|
|
1970
|
+
*/
|
|
1971
|
+
function restoreSessionState(ctx: ExtensionContext): void {
|
|
1972
|
+
resetSessionState();
|
|
1973
|
+
syncTaskGroupStore(ctx);
|
|
1930
1974
|
// Restore meta state (visibility, nextId) from session entries
|
|
1931
1975
|
const entries = ctx.sessionManager.getEntries();
|
|
1932
1976
|
const stateEntry = entries
|
|
@@ -1947,14 +1991,11 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
|
|
|
1947
1991
|
// Load tasks: prefer file store (shared mode), fall back to session entries
|
|
1948
1992
|
if (store.isShared) {
|
|
1949
1993
|
loadFromStore();
|
|
1950
|
-
|
|
1951
|
-
// Start watching for cross-session changes
|
|
1952
1994
|
store.watch(() => {
|
|
1953
1995
|
loadFromStore();
|
|
1954
1996
|
updateWidget(ctx);
|
|
1955
1997
|
});
|
|
1956
1998
|
} else if (stateEntry?.data?.tasks) {
|
|
1957
|
-
// Session-only mode: restore from entries, migrating old schema
|
|
1958
1999
|
state.tasks = stateEntry.data.tasks.map((t) => ({
|
|
1959
2000
|
id: (t.id as string) ?? String(state.nextId++),
|
|
1960
2001
|
subject: (t.subject as string) ?? (t.title as string) ?? "Untitled",
|
|
@@ -1969,15 +2010,10 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
|
|
|
1969
2010
|
createdAt: (t.createdAt as number) ?? Date.now(),
|
|
1970
2011
|
completedAt: t.completedAt as number | undefined,
|
|
1971
2012
|
}));
|
|
1972
|
-
// Recalculate nextId
|
|
1973
2013
|
const maxId = state.tasks.reduce((max, t) => Math.max(max, Number(t.id) || 0), 0);
|
|
1974
2014
|
state.nextId = Math.max(state.nextId, maxId + 1);
|
|
1975
2015
|
}
|
|
1976
2016
|
|
|
1977
|
-
// Clear orphaned tasks on startup: at session_start no agents are running,
|
|
1978
|
-
// so any in_progress tasks are leftovers from a dead session.
|
|
1979
|
-
// Also clear if all tasks are already completed — the 2s auto-clear timer
|
|
1980
|
-
// from a previous turn may have been killed by an extension reload.
|
|
1981
2017
|
if (state.tasks.length > 0) {
|
|
1982
2018
|
const orphaned = state.tasks.filter((t) => t.status === "in_progress");
|
|
1983
2019
|
const allCompleted = state.tasks.every((t) => t.status === "completed");
|
|
@@ -1986,7 +2022,6 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
|
|
|
1986
2022
|
}
|
|
1987
2023
|
}
|
|
1988
2024
|
|
|
1989
|
-
// Clean up team directories older than 7 days
|
|
1990
2025
|
cleanupStaleTeams(teamName);
|
|
1991
2026
|
|
|
1992
2027
|
if (!isSubagent) {
|
|
@@ -2131,6 +2166,14 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
|
|
|
2131
2166
|
|
|
2132
2167
|
updateWidget(ctx);
|
|
2133
2168
|
updateAgentBar(ctx);
|
|
2169
|
+
}
|
|
2170
|
+
|
|
2171
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
2172
|
+
restoreSessionState(ctx);
|
|
2173
|
+
});
|
|
2174
|
+
|
|
2175
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
2176
|
+
restoreSessionState(ctx);
|
|
2134
2177
|
});
|
|
2135
2178
|
|
|
2136
2179
|
// Cleanup on session end
|
|
@@ -2150,6 +2193,7 @@ Before calling manage_tasks complete/update, call manage_tasks list first so ind
|
|
|
2150
2193
|
legacyInteropBridgeCleanup?.();
|
|
2151
2194
|
legacyInteropBridgeCleanup = undefined;
|
|
2152
2195
|
store.close();
|
|
2196
|
+
if (!isSubagent) delete process.env.PI_TEAM_NAME;
|
|
2153
2197
|
persistState();
|
|
2154
2198
|
});
|
|
2155
2199
|
}
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
* {@link registerTasksExtension}. Domain logic lives in sibling modules.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { randomUUID } from "node:crypto";
|
|
23
22
|
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
24
23
|
import { registerTasksExtension } from "./commands/register-tasks-extension.js";
|
|
25
24
|
import { TaskListStore } from "./state/index.js";
|
|
@@ -31,7 +30,7 @@ export type { AgentIdentity } from "./agents/index.js";
|
|
|
31
30
|
export { classifyAgent } from "./agents/index.js";
|
|
32
31
|
export { _extractTasksFromText, escapeRegex, findCompletedTasks } from "./parsing/index.js";
|
|
33
32
|
export type { Task, TaskComment, TaskStatus, TasksState } from "./state/index.js";
|
|
34
|
-
export { shouldClearOnAgentEnd } from "./state/index.js";
|
|
33
|
+
export { buildSessionTaskGroupName, shouldClearOnAgentEnd } from "./state/index.js";
|
|
35
34
|
|
|
36
35
|
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
37
36
|
|
|
@@ -42,16 +41,7 @@ export { shouldClearOnAgentEnd } from "./state/index.js";
|
|
|
42
41
|
*/
|
|
43
42
|
export default function tasksExtension(pi: ExtensionAPI): void {
|
|
44
43
|
const isSubagent = process.env.PI_IS_SUBAGENT === "1";
|
|
45
|
-
|
|
46
|
-
// Auto-generate a shared task-group name so subagents can coordinate via a
|
|
47
|
-
// file-backed store. PI_TEAM_NAME stays as the env var for backward compatibility.
|
|
48
|
-
const teamName =
|
|
49
|
-
process.env.PI_TEAM_NAME ?? (isSubagent ? null : `task-group-${randomUUID().slice(0, 8)}`);
|
|
50
|
-
if (teamName && !process.env.PI_TEAM_NAME) {
|
|
51
|
-
// Set on process.env so child subagents inherit it automatically
|
|
52
|
-
process.env.PI_TEAM_NAME = teamName;
|
|
53
|
-
}
|
|
54
|
-
|
|
44
|
+
const teamName = isSubagent ? (process.env.PI_TEAM_NAME ?? null) : null;
|
|
55
45
|
const store = new TaskListStore(teamName);
|
|
56
46
|
registerTasksExtension(pi, store, teamName);
|
|
57
47
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* locking and `fs.watch`, and pure predicates that operate on task arrays.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
9
10
|
import type { FSWatcher } from "node:fs";
|
|
10
11
|
import {
|
|
11
12
|
existsSync,
|
|
@@ -39,6 +40,31 @@ export const TEAM_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
|
39
40
|
/** Minimum width for side-by-side layout (tasks left, subagents right). */
|
|
40
41
|
export const MIN_SIDE_BY_SIDE_WIDTH = 120;
|
|
41
42
|
|
|
43
|
+
/** Prefix used for session-scoped shared task-group directories. */
|
|
44
|
+
const SESSION_TASK_GROUP_PREFIX = "task-group";
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a stable task-group name for a session/cwd pair.
|
|
48
|
+
*
|
|
49
|
+
* Main sessions use this to isolate task boards per session while keeping the
|
|
50
|
+
* same group name when the user reopens the same session later. Subagents
|
|
51
|
+
* inherit the resolved name via `PI_TEAM_NAME`.
|
|
52
|
+
*
|
|
53
|
+
* @param sessionId - Current session identifier
|
|
54
|
+
* @param cwd - Current working directory for the session
|
|
55
|
+
* @returns Stable, filesystem-safe task-group name
|
|
56
|
+
*/
|
|
57
|
+
export function buildSessionTaskGroupName(sessionId: string, cwd: string): string {
|
|
58
|
+
const safeSessionId =
|
|
59
|
+
sessionId
|
|
60
|
+
.replace(/[^a-zA-Z0-9._-]/g, "_")
|
|
61
|
+
.replace(/_+/g, "_")
|
|
62
|
+
.replace(/^_+|_+$/g, "")
|
|
63
|
+
.slice(0, 24) || "session";
|
|
64
|
+
const digest = createHash("sha256").update(`${sessionId}:${cwd}`).digest("hex").slice(0, 12);
|
|
65
|
+
return `${SESSION_TASK_GROUP_PREFIX}-${safeSessionId}-${digest}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
42
68
|
// ── Task Types ───────────────────────────────────────────────────────────────
|
|
43
69
|
|
|
44
70
|
/** Lifecycle state of a task. */
|
|
@@ -692,7 +692,19 @@ export class TeamDashboardEditor extends CustomEditor {
|
|
|
692
692
|
);
|
|
693
693
|
|
|
694
694
|
const footer = this.renderFooter(width);
|
|
695
|
-
|
|
695
|
+
const contentLines = [...merged, footer];
|
|
696
|
+
|
|
697
|
+
// Pad to fill the terminal height. On the alternate screen the TUI
|
|
698
|
+
// renders all children (header, chat history, widgets, editor, footer)
|
|
699
|
+
// and the visible viewport is the last `rows` lines. Without padding,
|
|
700
|
+
// stale conversation content bleeds through at the top of the dashboard.
|
|
701
|
+
// Prepending blank lines pushes that content off-screen.
|
|
702
|
+
const targetLines = this.tui.terminal.rows;
|
|
703
|
+
while (contentLines.length < targetLines) {
|
|
704
|
+
contentLines.unshift("");
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return contentLines;
|
|
696
708
|
}
|
|
697
709
|
|
|
698
710
|
/**
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
SessionManager,
|
|
16
16
|
SettingsManager,
|
|
17
17
|
} from "@mariozechner/pi-coding-agent";
|
|
18
|
-
import { applyKnownModelMetadataOverrides } from "../../../
|
|
18
|
+
import { applyKnownModelMetadataOverrides } from "../../../runtime/model-metadata-overrides.js";
|
|
19
19
|
import { getTallowHomeDir, getTallowPath } from "../../_shared/tallow-paths.js";
|
|
20
20
|
import {
|
|
21
21
|
type AgentConfig,
|
|
@@ -255,7 +255,7 @@ export async function spawnTeammateSession(
|
|
|
255
255
|
// Use the user's tallow auth and model config so teammates inherit
|
|
256
256
|
// API keys and custom model definitions from the main session.
|
|
257
257
|
const authStorage = AuthStorage.create(getTallowPath("auth.json"));
|
|
258
|
-
const modelRegistry =
|
|
258
|
+
const modelRegistry = ModelRegistry.create(authStorage, getTallowPath("models.json"));
|
|
259
259
|
applyKnownModelMetadataOverrides(modelRegistry);
|
|
260
260
|
const model = modelRegistry.find(resolvedModel.provider, resolvedModel.id);
|
|
261
261
|
if (!model) {
|
|
@@ -232,8 +232,11 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
232
232
|
*/
|
|
233
233
|
function handleDashboardEscape(ctx: ExtensionContext): void {
|
|
234
234
|
if (!dashboardEnabled) return;
|
|
235
|
+
const hasActiveTeams = (getTeams() as Map<string, unknown>).size > 0;
|
|
235
236
|
disableDashboard(ctx, false);
|
|
236
|
-
|
|
237
|
+
if (hasActiveTeams) {
|
|
238
|
+
ctx.ui.notify("Team dashboard disabled. Teammates keep running.", "info");
|
|
239
|
+
}
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
/**
|
|
@@ -283,7 +286,12 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
283
286
|
ctx.ui.setEditorComponent(undefined);
|
|
284
287
|
ctx.ui.setWorkingMessage();
|
|
285
288
|
ctx.ui.setStatus("team-dashboard", undefined);
|
|
286
|
-
|
|
289
|
+
// Only notify when there are active teams — the notification is
|
|
290
|
+
// confusing when it appears during unrelated flows (e.g. subagent
|
|
291
|
+
// parallel) because the dashboard auto-disabled as a side effect.
|
|
292
|
+
if (notify && (getTeams() as Map<string, unknown>).size > 0) {
|
|
293
|
+
ctx.ui.notify("Team dashboard disabled.", "info");
|
|
294
|
+
}
|
|
287
295
|
}
|
|
288
296
|
|
|
289
297
|
/**
|