@selesai/code 0.2.1 → 0.3.1
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/dist/config.d.ts +4 -15
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -45
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session.d.ts +1 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +8 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/diagnostics.d.ts +1 -0
- package/dist/core/diagnostics.d.ts.map +1 -1
- package/dist/core/diagnostics.js.map +1 -1
- package/dist/core/model-registry.d.ts +4 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +101 -51
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/package-manager.d.ts +10 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +93 -8
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/package-manager.test.d.ts +2 -0
- package/dist/core/package-manager.test.d.ts.map +1 -0
- package/dist/core/package-manager.test.js +20 -0
- package/dist/core/package-manager.test.js.map +1 -0
- package/dist/core/resource-loader.d.ts +3 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +10 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/settings-manager.d.ts +1 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/defaults/models.json +64 -113
- package/dist/extensions/caveman/caveman-instructions.cjs +39 -0
- package/dist/extensions/caveman/index.js +116 -0
- package/dist/extensions/caveman/package.json +8 -0
- package/dist/extensions/caveman/test/extension.test.js +185 -0
- package/dist/extensions/caveman/test/helpers.test.js +67 -0
- package/dist/extensions/copy-turn.ts +2 -2
- package/dist/extensions/pi-intercom/broker/broker.ts +2 -3
- package/dist/extensions/pi-intercom/broker/paths.ts +10 -5
- package/dist/extensions/pi-intercom/broker/spawn.ts +2 -3
- package/dist/extensions/pi-intercom/config.ts +2 -2
- package/dist/extensions/pi-intercom/index.ts +2 -2
- package/dist/extensions/pi-intercom/package.json +2 -2
- package/dist/extensions/pi-intercom/ui/compose.ts +3 -3
- package/dist/extensions/pi-intercom/ui/inline-message.ts +3 -3
- package/dist/extensions/pi-intercom/ui/session-list.ts +3 -3
- package/dist/extensions/pi-powerline-footer/bash-mode/editor.ts +2 -2
- package/dist/extensions/pi-powerline-footer/bash-mode/history.ts +2 -1
- package/dist/extensions/pi-powerline-footer/index.ts +40 -47
- package/dist/extensions/pi-powerline-footer/package.json +2 -2
- package/dist/extensions/pi-powerline-footer/tests/bash-mode.test.ts +10 -10
- package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +1 -1
- package/dist/extensions/pi-powerline-footer/tests/working-vibes.test.ts +2 -2
- package/dist/extensions/pi-powerline-footer/theme.ts +6 -6
- package/dist/extensions/pi-powerline-footer/tps.ts +1 -1
- package/dist/extensions/pi-powerline-footer/types.ts +7 -7
- package/dist/extensions/pi-powerline-footer/welcome.ts +23 -16
- package/dist/extensions/pi-powerline-footer/working-vibes.ts +52 -59
- package/dist/extensions/pi-rewind-hook/index.ts +1 -1
- package/dist/extensions/pi-rewind-hook/package.json +1 -1
- package/dist/extensions/pi-subagents/README.md +224 -52
- package/dist/extensions/pi-subagents/agents/architect.md +188 -0
- package/dist/extensions/pi-subagents/agents/builder.md +120 -0
- package/dist/extensions/pi-subagents/agents/commentator.md +131 -0
- package/dist/extensions/pi-subagents/agents/explorer.md +51 -0
- package/dist/extensions/pi-subagents/agents/recapper.md +21 -0
- package/dist/extensions/pi-subagents/install.mjs +93 -0
- package/dist/extensions/pi-subagents/package.json +4 -5
- package/dist/extensions/pi-subagents/prompts/parallel-context-build.md +1 -1
- package/dist/extensions/pi-subagents/prompts/parallel-handoff-plan.md +1 -1
- package/dist/extensions/pi-subagents/skills/pi-subagents/SKILL.md +76 -13
- package/dist/extensions/pi-subagents/src/agents/agent-management.ts +179 -2
- package/dist/extensions/pi-subagents/src/agents/agent-memory.ts +254 -0
- package/dist/extensions/pi-subagents/src/agents/agent-serializer.ts +11 -0
- package/dist/extensions/pi-subagents/src/agents/agents.ts +193 -19
- package/dist/extensions/pi-subagents/src/agents/chain-serializer.ts +27 -2
- package/dist/extensions/pi-subagents/src/extension/config.ts +27 -4
- package/dist/extensions/pi-subagents/src/extension/doctor.ts +1 -7
- package/dist/extensions/pi-subagents/src/extension/fanout-child.ts +3 -2
- package/dist/extensions/pi-subagents/src/extension/index.ts +73 -71
- package/dist/extensions/pi-subagents/src/extension/rpc.ts +369 -0
- package/dist/extensions/pi-subagents/src/extension/schemas.ts +54 -10
- package/dist/extensions/pi-subagents/src/extension/tool-description.ts +200 -0
- package/dist/extensions/pi-subagents/src/intercom/intercom-bridge.ts +21 -253
- package/dist/extensions/pi-subagents/src/intercom/native-supervisor-channel.ts +519 -0
- package/dist/extensions/pi-subagents/src/profiles/profiles.ts +1 -1
- package/dist/extensions/pi-subagents/src/runs/background/async-execution.ts +195 -105
- package/dist/extensions/pi-subagents/src/runs/background/async-job-tracker.ts +88 -2
- package/dist/extensions/pi-subagents/src/runs/background/async-status.ts +67 -10
- package/dist/extensions/pi-subagents/src/runs/background/chain-root-attachment.ts +34 -4
- package/dist/extensions/pi-subagents/src/runs/background/completion-batcher.ts +166 -0
- package/dist/extensions/pi-subagents/src/runs/background/control-channel.ts +156 -1
- package/dist/extensions/pi-subagents/src/runs/background/fleet-view.ts +515 -0
- package/dist/extensions/pi-subagents/src/runs/background/notify.ts +161 -44
- package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +1 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-id-resolver.ts +3 -2
- package/dist/extensions/pi-subagents/src/runs/background/run-status.ts +167 -6
- package/dist/extensions/pi-subagents/src/runs/background/scheduled-runs.ts +514 -0
- package/dist/extensions/pi-subagents/src/runs/background/stale-run-reconciler.ts +28 -1
- package/dist/extensions/pi-subagents/src/runs/background/subagent-runner.ts +873 -204
- package/dist/extensions/pi-subagents/src/runs/background/wait.ts +353 -0
- package/dist/extensions/pi-subagents/src/runs/foreground/chain-execution.ts +123 -27
- package/dist/extensions/pi-subagents/src/runs/foreground/execution.ts +174 -70
- package/dist/extensions/pi-subagents/src/runs/foreground/subagent-executor.ts +569 -81
- package/dist/extensions/pi-subagents/src/runs/shared/acceptance.ts +45 -22
- package/dist/extensions/pi-subagents/src/runs/shared/completion-guard.ts +3 -1
- package/dist/extensions/pi-subagents/src/runs/shared/dynamic-fanout.ts +2 -2
- package/dist/extensions/pi-subagents/src/runs/shared/model-fallback.ts +172 -21
- package/dist/extensions/pi-subagents/src/runs/shared/model-scope.ts +128 -0
- package/dist/extensions/pi-subagents/src/runs/shared/nested-events.ts +89 -0
- package/dist/extensions/pi-subagents/src/runs/shared/parallel-utils.ts +50 -1
- package/dist/extensions/pi-subagents/src/runs/shared/pi-args.ts +35 -4
- package/dist/extensions/pi-subagents/src/runs/shared/pi-spawn.ts +63 -45
- package/dist/extensions/pi-subagents/src/runs/shared/single-output.ts +2 -0
- package/dist/extensions/pi-subagents/src/runs/shared/subagent-prompt-runtime.ts +125 -5
- package/dist/extensions/pi-subagents/src/runs/shared/tool-budget.ts +74 -0
- package/dist/extensions/pi-subagents/src/runs/shared/turn-budget.ts +52 -0
- package/dist/extensions/pi-subagents/src/runs/shared/worktree.ts +28 -5
- package/dist/extensions/pi-subagents/src/shared/artifacts.ts +16 -1
- package/dist/extensions/pi-subagents/src/shared/atomic-json.ts +15 -2
- package/dist/extensions/pi-subagents/src/shared/child-transcript.ts +212 -0
- package/dist/extensions/pi-subagents/src/shared/fork-context.ts +133 -22
- package/dist/extensions/pi-subagents/src/shared/settings.ts +3 -1
- package/dist/extensions/pi-subagents/src/shared/types.ts +197 -4
- package/dist/extensions/pi-subagents/src/shared/utils.ts +108 -17
- package/dist/extensions/pi-subagents/src/slash/prompt-workflows.ts +330 -0
- package/dist/extensions/pi-subagents/src/slash/slash-commands.ts +135 -4
- package/dist/extensions/pi-subagents/src/tui/render.ts +32 -12
- package/dist/extensions/pi-subagents/test/e2e/real-session-subagent.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/integration/async-execution.test.ts +2772 -0
- package/dist/extensions/pi-subagents/test/integration/async-job-tracker.test.ts +1107 -0
- package/dist/extensions/pi-subagents/test/integration/async-status.test.ts +463 -0
- package/dist/extensions/pi-subagents/test/integration/chain-clarify.test.ts +274 -0
- package/dist/extensions/pi-subagents/test/integration/chain-execution.test.ts +1535 -0
- package/dist/extensions/pi-subagents/test/integration/detect-error.test.ts +242 -0
- package/dist/extensions/pi-subagents/test/integration/doctor-executor.test.ts +128 -0
- package/dist/extensions/pi-subagents/test/integration/error-handling.test.ts +252 -0
- package/dist/extensions/pi-subagents/test/integration/foreground-result-size.test.ts +188 -0
- package/dist/extensions/pi-subagents/test/integration/fork-context-execution.test.ts +1306 -0
- package/dist/extensions/pi-subagents/test/integration/intercom-result-delivery.test.ts +1033 -0
- package/dist/extensions/pi-subagents/test/integration/parallel-execution.test.ts +397 -0
- package/dist/extensions/pi-subagents/test/integration/render-fork-badge.test.ts +583 -0
- package/dist/extensions/pi-subagents/test/integration/render-widget.test.ts +691 -0
- package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +892 -0
- package/dist/extensions/pi-subagents/test/integration/session-tokens.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/integration/single-execution.test.ts +1824 -0
- package/dist/extensions/pi-subagents/test/integration/slash-commands.test.ts +1284 -0
- package/dist/extensions/pi-subagents/test/integration/slash-live-state.test.ts +127 -0
- package/dist/extensions/pi-subagents/test/integration/template-resolution.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/integration/top-level-async.test.ts +37 -0
- package/dist/extensions/pi-subagents/test/support/helpers.ts +189 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi-script.mjs +335 -0
- package/dist/extensions/pi-subagents/test/support/mock-pi.ts +129 -0
- package/dist/extensions/pi-subagents/test/support/real-session-child-cli.mjs +184 -0
- package/dist/extensions/pi-subagents/test/support/real-session-runner.ts +285 -0
- package/dist/extensions/pi-subagents/test/support/register-loader.mjs +15 -0
- package/dist/extensions/pi-subagents/test/support/ts-loader.mjs +108 -0
- package/dist/extensions/pi-subagents/test/unit/acceptance.test.ts +421 -0
- package/dist/extensions/pi-subagents/test/unit/agent-disabled.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/agent-eject-disable.test.ts +343 -0
- package/dist/extensions/pi-subagents/test/unit/agent-frontmatter.test.ts +1199 -0
- package/dist/extensions/pi-subagents/test/unit/agent-management.test.ts +556 -0
- package/dist/extensions/pi-subagents/test/unit/agent-memory.test.ts +420 -0
- package/dist/extensions/pi-subagents/test/unit/agent-overrides.test.ts +579 -0
- package/dist/extensions/pi-subagents/test/unit/agent-scope.test.ts +20 -0
- package/dist/extensions/pi-subagents/test/unit/agent-selection.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/artifacts.test.ts +24 -0
- package/dist/extensions/pi-subagents/test/unit/async-execution.test.ts +86 -0
- package/dist/extensions/pi-subagents/test/unit/async-interrupt-action.test.ts +221 -0
- package/dist/extensions/pi-subagents/test/unit/async-permission-session.test.ts +98 -0
- package/dist/extensions/pi-subagents/test/unit/async-resume.test.ts +285 -0
- package/dist/extensions/pi-subagents/test/unit/atomic-json.test.ts +112 -0
- package/dist/extensions/pi-subagents/test/unit/chain-append.test.ts +206 -0
- package/dist/extensions/pi-subagents/test/unit/chain-root-attachment.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/chain-serializer.test.ts +229 -0
- package/dist/extensions/pi-subagents/test/unit/child-transcript.test.ts +225 -0
- package/dist/extensions/pi-subagents/test/unit/close-grace-timer.test.ts +101 -0
- package/dist/extensions/pi-subagents/test/unit/completion-batcher.test.ts +228 -0
- package/dist/extensions/pi-subagents/test/unit/completion-dedupe.test.ts +36 -0
- package/dist/extensions/pi-subagents/test/unit/completion-guard.test.ts +152 -0
- package/dist/extensions/pi-subagents/test/unit/config-dir-runtime.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/control-channel.test.ts +327 -0
- package/dist/extensions/pi-subagents/test/unit/control-notices.test.ts +161 -0
- package/dist/extensions/pi-subagents/test/unit/doctor.test.ts +159 -0
- package/dist/extensions/pi-subagents/test/unit/dynamic-fanout.test.ts +217 -0
- package/dist/extensions/pi-subagents/test/unit/extra-agent-dirs.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/file-coalescer.test.ts +68 -0
- package/dist/extensions/pi-subagents/test/unit/foreground-tool-call-compaction.test.ts +78 -0
- package/dist/extensions/pi-subagents/test/unit/fork-context.test.ts +415 -0
- package/dist/extensions/pi-subagents/test/unit/get-final-output.test.ts +126 -0
- package/dist/extensions/pi-subagents/test/unit/index-child-registration.test.ts +271 -0
- package/dist/extensions/pi-subagents/test/unit/intercom-bridge.test.ts +173 -0
- package/dist/extensions/pi-subagents/test/unit/jsonl-writer.test.ts +119 -0
- package/dist/extensions/pi-subagents/test/unit/live-async-resume.test.ts +170 -0
- package/dist/extensions/pi-subagents/test/unit/model-fallback.test.ts +346 -0
- package/dist/extensions/pi-subagents/test/unit/model-info.test.ts +62 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope-settings.test.ts +87 -0
- package/dist/extensions/pi-subagents/test/unit/model-scope.test.ts +145 -0
- package/dist/extensions/pi-subagents/test/unit/native-supervisor-channel.test.ts +162 -0
- package/dist/extensions/pi-subagents/test/unit/nested-control.test.ts +470 -0
- package/dist/extensions/pi-subagents/test/unit/nested-events.test.ts +329 -0
- package/dist/extensions/pi-subagents/test/unit/notify.test.ts +319 -0
- package/dist/extensions/pi-subagents/test/unit/package-manifest.test.ts +70 -0
- package/dist/extensions/pi-subagents/test/unit/parallel-utils.test.ts +247 -0
- package/dist/extensions/pi-subagents/test/unit/path-handling.test.ts +113 -0
- package/dist/extensions/pi-subagents/test/unit/path-resolution.test.ts +94 -0
- package/dist/extensions/pi-subagents/test/unit/pi-args.test.ts +803 -0
- package/dist/extensions/pi-subagents/test/unit/pi-coding-agent-dir.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/pi-spawn.test.ts +238 -0
- package/dist/extensions/pi-subagents/test/unit/proactive-skills.test.ts +118 -0
- package/dist/extensions/pi-subagents/test/unit/profiles.test.ts +299 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-template-bridge.test.ts +408 -0
- package/dist/extensions/pi-subagents/test/unit/prompt-workflows.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/recursion-guard.test.ts +257 -0
- package/dist/extensions/pi-subagents/test/unit/render-helpers.test.ts +209 -0
- package/dist/extensions/pi-subagents/test/unit/result-intercom.test.ts +226 -0
- package/dist/extensions/pi-subagents/test/unit/rpc.test.ts +294 -0
- package/dist/extensions/pi-subagents/test/unit/run-id-resolver.test.ts +147 -0
- package/dist/extensions/pi-subagents/test/unit/run-status.test.ts +997 -0
- package/dist/extensions/pi-subagents/test/unit/scheduled-runs.test.ts +478 -0
- package/dist/extensions/pi-subagents/test/unit/schemas.test.ts +562 -0
- package/dist/extensions/pi-subagents/test/unit/single-output.test.ts +213 -0
- package/dist/extensions/pi-subagents/test/unit/skills-fallback.test.ts +438 -0
- package/dist/extensions/pi-subagents/test/unit/slash-bridge.test.ts +54 -0
- package/dist/extensions/pi-subagents/test/unit/slash-chain-groups.test.ts +464 -0
- package/dist/extensions/pi-subagents/test/unit/stale-run-reconciler.test.ts +255 -0
- package/dist/extensions/pi-subagents/test/unit/status-format.test.ts +19 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-control.test.ts +245 -0
- package/dist/extensions/pi-subagents/test/unit/subagent-prompt-runtime.test.ts +517 -0
- package/dist/extensions/pi-subagents/test/unit/temp-paths.test.ts +72 -0
- package/dist/extensions/pi-subagents/test/unit/tool-budget.test.ts +52 -0
- package/dist/extensions/pi-subagents/test/unit/tool-description.test.ts +237 -0
- package/dist/extensions/pi-subagents/test/unit/total-cost.test.ts +60 -0
- package/dist/extensions/pi-subagents/test/unit/ts-loader.test.ts +33 -0
- package/dist/extensions/pi-subagents/test/unit/turn-budget.test.ts +202 -0
- package/dist/extensions/pi-subagents/test/unit/types-fork-preamble.test.ts +23 -0
- package/dist/extensions/pi-subagents/test/unit/wait.test.ts +398 -0
- package/dist/extensions/pi-subagents/test/unit/widget-nested-render.test.ts +88 -0
- package/dist/extensions/pi-subagents/test/unit/windows-hide-spawn.test.ts +26 -0
- package/dist/extensions/pi-subagents/test/unit/workflow-graph.test.ts +134 -0
- package/dist/extensions/pi-subagents/test/unit/worktree.test.ts +479 -0
- package/dist/extensions/pi-web-agent/package.json +2 -2
- package/dist/extensions/pi-web-agent/src/backends/settings-reader.ts +2 -2
- package/dist/extensions/pi-web-agent/src/changelog-notice.ts +5 -2
- package/dist/extensions/pi-web-agent/src/commands/web-agent-config.ts +1 -1
- package/dist/extensions/pi-web-agent/src/extension.ts +1 -1
- package/dist/extensions/pi-web-agent/src/presentation/config-store.ts +6 -4
- package/dist/extensions/question/helpers.ts +3 -3
- package/dist/extensions/question/index.ts +2 -2
- package/dist/extensions/question/navigation.ts +2 -2
- package/dist/extensions/question/package.json +2 -2
- package/dist/extensions/question/question-list.ts +2 -2
- package/dist/extensions/question/tests/question-list.test.ts +2 -2
- package/dist/extensions/question/tui-adapter.ts +2 -2
- package/dist/extensions/question/types.ts +2 -2
- package/dist/extensions/question/ui-protocol.ts +2 -2
- package/dist/extensions/rtk.ts +2 -2
- package/dist/extensions/tokenin-onboarding.ts +2 -2
- package/dist/extensions/undo.ts +1 -1
- package/dist/extensions/web-agent-onboarding.ts +3 -3
- package/dist/extensions/workflow/adapter.ts +16 -8
- package/dist/extensions/workflow/extension.ts +1 -1
- package/dist/extensions/workflow/package.json +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/assistant-message.d.ts.map +1 -1
- package/dist/modes/interactive/components/assistant-message.js +2 -0
- package/dist/modes/interactive/components/assistant-message.js.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts +1 -1
- package/dist/modes/interactive/components/first-time-setup.d.ts.map +1 -1
- package/dist/modes/interactive/components/first-time-setup.js +19 -5
- package/dist/modes/interactive/components/first-time-setup.js.map +1 -1
- package/dist/skills/caveman/SKILL.md +49 -0
- package/dist/skills/handoff-text/SKILL.md +13 -0
- package/dist/utils/thinking-tags.d.ts +7 -0
- package/dist/utils/thinking-tags.d.ts.map +1 -0
- package/dist/utils/thinking-tags.js +63 -0
- package/dist/utils/thinking-tags.js.map +1 -0
- package/docs/docs.json +4 -0
- package/docs/extensions.md +2 -0
- package/docs/settings.md +15 -0
- package/docs/shared-host-extensions.md +109 -0
- package/package.json +2 -2
|
@@ -0,0 +1,1284 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { beforeEach, describe, it } from "node:test";
|
|
6
|
+
|
|
7
|
+
import { ASYNC_DIR } from "../../src/shared/types.ts";
|
|
8
|
+
|
|
9
|
+
const SLASH_RESULT_TYPE = "subagent-slash-result";
|
|
10
|
+
const SLASH_SUBAGENT_REQUEST_EVENT = "subagent:slash:request";
|
|
11
|
+
const SLASH_SUBAGENT_STARTED_EVENT = "subagent:slash:started";
|
|
12
|
+
const SLASH_SUBAGENT_RESPONSE_EVENT = "subagent:slash:response";
|
|
13
|
+
|
|
14
|
+
interface EventBus {
|
|
15
|
+
on(event: string, handler: (data: unknown) => void): () => void;
|
|
16
|
+
emit(event: string, data: unknown): void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type RegisteredSlashCommand = { handler(args: string, ctx: unknown): Promise<void>; getArgumentCompletions?: (prefix: string) => unknown };
|
|
20
|
+
|
|
21
|
+
interface RegisterSlashCommandsModule {
|
|
22
|
+
registerSlashCommands?: (
|
|
23
|
+
pi: {
|
|
24
|
+
events: EventBus;
|
|
25
|
+
registerCommand(
|
|
26
|
+
name: string,
|
|
27
|
+
spec: RegisteredSlashCommand,
|
|
28
|
+
): void;
|
|
29
|
+
registerShortcut(key: string, spec: { handler(ctx: unknown): Promise<void> }): void;
|
|
30
|
+
sendMessage(message: unknown): void;
|
|
31
|
+
setModel?(model: unknown): Promise<boolean>;
|
|
32
|
+
},
|
|
33
|
+
state: {
|
|
34
|
+
baseCwd: string;
|
|
35
|
+
currentSessionId: string | null;
|
|
36
|
+
asyncJobs: Map<string, unknown>;
|
|
37
|
+
cleanupTimers: Map<string, ReturnType<typeof setTimeout>>;
|
|
38
|
+
lastUiContext: unknown;
|
|
39
|
+
poller: NodeJS.Timeout | null;
|
|
40
|
+
completionSeen: Map<string, number>;
|
|
41
|
+
watcher: unknown;
|
|
42
|
+
watcherRestartTimer: ReturnType<typeof setTimeout> | null;
|
|
43
|
+
resultFileCoalescer: { schedule(file: string, delayMs?: number): boolean; clear(): void };
|
|
44
|
+
},
|
|
45
|
+
) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface SlashLiveStateModule {
|
|
49
|
+
clearSlashSnapshots?: typeof import("../../src/slash/slash-live-state.ts").clearSlashSnapshots;
|
|
50
|
+
getSlashRenderableSnapshot?: typeof import("../../src/slash/slash-live-state.ts").getSlashRenderableSnapshot;
|
|
51
|
+
resolveSlashMessageDetails?: typeof import("../../src/slash/slash-live-state.ts").resolveSlashMessageDetails;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let registerSlashCommands: RegisterSlashCommandsModule["registerSlashCommands"];
|
|
55
|
+
let clearSlashSnapshots: SlashLiveStateModule["clearSlashSnapshots"];
|
|
56
|
+
let getSlashRenderableSnapshot: SlashLiveStateModule["getSlashRenderableSnapshot"];
|
|
57
|
+
let resolveSlashMessageDetails: SlashLiveStateModule["resolveSlashMessageDetails"];
|
|
58
|
+
let available = true;
|
|
59
|
+
try {
|
|
60
|
+
({ registerSlashCommands } = await import("../../src/slash/slash-commands.ts") as RegisterSlashCommandsModule);
|
|
61
|
+
({ clearSlashSnapshots, getSlashRenderableSnapshot, resolveSlashMessageDetails } = await import("../../src/slash/slash-live-state.ts") as SlashLiveStateModule);
|
|
62
|
+
} catch {
|
|
63
|
+
available = false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function createEventBus(): EventBus {
|
|
67
|
+
const handlers = new Map<string, Array<(data: unknown) => void>>();
|
|
68
|
+
return {
|
|
69
|
+
on(event, handler) {
|
|
70
|
+
const existing = handlers.get(event) ?? [];
|
|
71
|
+
existing.push(handler);
|
|
72
|
+
handlers.set(event, existing);
|
|
73
|
+
return () => {
|
|
74
|
+
const current = handlers.get(event) ?? [];
|
|
75
|
+
handlers.set(event, current.filter((entry) => entry !== handler));
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
emit(event, data) {
|
|
79
|
+
for (const handler of handlers.get(event) ?? []) {
|
|
80
|
+
handler(data);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function createState(cwd: string) {
|
|
87
|
+
return {
|
|
88
|
+
baseCwd: cwd,
|
|
89
|
+
currentSessionId: null,
|
|
90
|
+
asyncJobs: new Map(),
|
|
91
|
+
cleanupTimers: new Map(),
|
|
92
|
+
lastUiContext: null,
|
|
93
|
+
poller: null,
|
|
94
|
+
completionSeen: new Map(),
|
|
95
|
+
watcher: null,
|
|
96
|
+
watcherRestartTimer: null,
|
|
97
|
+
resultFileCoalescer: {
|
|
98
|
+
schedule: () => false,
|
|
99
|
+
clear: () => {},
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function withIsolatedHome<T>(fn: () => Promise<T>): Promise<T> {
|
|
105
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), "pi-slash-home-"));
|
|
106
|
+
const previousHome = process.env.HOME;
|
|
107
|
+
const previousUserProfile = process.env.USERPROFILE;
|
|
108
|
+
process.env.HOME = home;
|
|
109
|
+
process.env.USERPROFILE = home;
|
|
110
|
+
try {
|
|
111
|
+
return await fn();
|
|
112
|
+
} finally {
|
|
113
|
+
if (previousHome === undefined) delete process.env.HOME;
|
|
114
|
+
else process.env.HOME = previousHome;
|
|
115
|
+
if (previousUserProfile === undefined) delete process.env.USERPROFILE;
|
|
116
|
+
else process.env.USERPROFILE = previousUserProfile;
|
|
117
|
+
fs.rmSync(home, { recursive: true, force: true });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
function createCommandContext(
|
|
123
|
+
overrides: Partial<{
|
|
124
|
+
cwd: string;
|
|
125
|
+
hasUI: boolean;
|
|
126
|
+
custom: (...args: unknown[]) => Promise<unknown>;
|
|
127
|
+
notify: (message: string, type?: string) => void;
|
|
128
|
+
confirm: (title: string, message: string) => Promise<boolean>;
|
|
129
|
+
setStatus: (key: string, text: string | undefined) => void;
|
|
130
|
+
setToolsExpanded: (expanded: boolean) => void;
|
|
131
|
+
sessionManager: unknown;
|
|
132
|
+
modelRegistry: { getAvailable: () => Array<{ provider: string; id: string }>; find?: (provider: string, id: string) => unknown };
|
|
133
|
+
}> = {},
|
|
134
|
+
) {
|
|
135
|
+
return {
|
|
136
|
+
cwd: overrides.cwd ?? process.cwd(),
|
|
137
|
+
hasUI: overrides.hasUI ?? false,
|
|
138
|
+
ui: {
|
|
139
|
+
notify: overrides.notify ?? ((_message: string) => {}),
|
|
140
|
+
confirm: overrides.confirm ?? (async () => false),
|
|
141
|
+
setStatus: overrides.setStatus ?? ((_key: string, _text: string | undefined) => {}),
|
|
142
|
+
setToolsExpanded: overrides.setToolsExpanded ?? ((_expanded: boolean) => {}),
|
|
143
|
+
onTerminalInput: () => () => {},
|
|
144
|
+
custom: overrides.custom ?? (async () => undefined),
|
|
145
|
+
},
|
|
146
|
+
modelRegistry: overrides.modelRegistry ?? { getAvailable: () => [], find: () => undefined },
|
|
147
|
+
sessionManager: overrides.sessionManager ?? {
|
|
148
|
+
getSessionFile: () => null,
|
|
149
|
+
getSessionId: () => "session-test",
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function withTempProject<T>(prefix: string, fn: (root: string) => Promise<T>): Promise<T> {
|
|
155
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
156
|
+
fs.mkdirSync(path.join(root, ".selesai", "agents"), { recursive: true });
|
|
157
|
+
fs.mkdirSync(path.join(root, ".selesai", "chains"), { recursive: true });
|
|
158
|
+
try {
|
|
159
|
+
return await fn(root);
|
|
160
|
+
} finally {
|
|
161
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function writeProjectChain(root: string, fileName: string, content: string): void {
|
|
166
|
+
fs.writeFileSync(path.join(root, ".selesai", "chains", fileName), content, "utf-8");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function captureSlashCommandParams(
|
|
170
|
+
commandName: string,
|
|
171
|
+
args: string,
|
|
172
|
+
cwd: string,
|
|
173
|
+
setup?: () => void,
|
|
174
|
+
): Promise<{ params: unknown; notifications: string[] }> {
|
|
175
|
+
return withIsolatedHome(async () => {
|
|
176
|
+
setup?.();
|
|
177
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
178
|
+
const events = createEventBus();
|
|
179
|
+
let requestedParams: unknown;
|
|
180
|
+
const notifications: string[] = [];
|
|
181
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
182
|
+
const payload = data as { requestId: string; params?: unknown };
|
|
183
|
+
requestedParams = payload.params;
|
|
184
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
|
|
185
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
186
|
+
requestId: payload.requestId,
|
|
187
|
+
result: {
|
|
188
|
+
content: [{ type: "text", text: `${commandName} finished` }],
|
|
189
|
+
details: { mode: "chain", results: [] },
|
|
190
|
+
},
|
|
191
|
+
isError: false,
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const pi = {
|
|
196
|
+
events,
|
|
197
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) {
|
|
198
|
+
commands.set(name, spec);
|
|
199
|
+
},
|
|
200
|
+
registerShortcut() {},
|
|
201
|
+
sendMessage(_message: unknown) {},
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
registerSlashCommands!(pi, createState(cwd));
|
|
205
|
+
await commands.get(commandName)!.handler(args, createCommandContext({
|
|
206
|
+
cwd,
|
|
207
|
+
notify: (message) => {
|
|
208
|
+
notifications.push(message);
|
|
209
|
+
},
|
|
210
|
+
}));
|
|
211
|
+
return { params: requestedParams, notifications };
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
describe("slash command custom message delivery", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
216
|
+
beforeEach(() => {
|
|
217
|
+
clearSlashSnapshots?.();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("/run accepts an agent without a task", async () => {
|
|
221
|
+
const sent: unknown[] = [];
|
|
222
|
+
const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
|
|
223
|
+
const events = createEventBus();
|
|
224
|
+
let requestedParams: unknown;
|
|
225
|
+
let requestedCtx: unknown;
|
|
226
|
+
const sessionManager = {
|
|
227
|
+
flushed: false,
|
|
228
|
+
rewrites: 0,
|
|
229
|
+
getSessionFile: () => "session.jsonl",
|
|
230
|
+
_rewriteFile() {
|
|
231
|
+
this.rewrites++;
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
235
|
+
const payload = data as { requestId: string; params?: unknown; ctx?: unknown };
|
|
236
|
+
requestedParams = payload.params;
|
|
237
|
+
requestedCtx = payload.ctx;
|
|
238
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
|
|
239
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
240
|
+
requestId: payload.requestId,
|
|
241
|
+
result: {
|
|
242
|
+
content: [{ type: "text", text: "Commit finished" }],
|
|
243
|
+
details: { mode: "single", results: [] },
|
|
244
|
+
},
|
|
245
|
+
isError: false,
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const pi = {
|
|
250
|
+
events,
|
|
251
|
+
registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
|
|
252
|
+
commands.set(name, spec);
|
|
253
|
+
},
|
|
254
|
+
registerShortcut() {},
|
|
255
|
+
sendMessage(message: unknown) {
|
|
256
|
+
sent.push(message);
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const ctx = createCommandContext({ sessionManager });
|
|
261
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
262
|
+
await commands.get("run")!.handler("scout", ctx);
|
|
263
|
+
|
|
264
|
+
assert.deepEqual(requestedParams, { agent: "scout", task: "", clarify: false, agentScope: "both" });
|
|
265
|
+
assert.equal(requestedCtx, ctx);
|
|
266
|
+
assert.equal(sent.length, 2);
|
|
267
|
+
assert.equal((sent[0] as { display?: boolean }).display, true);
|
|
268
|
+
assert.equal((sent[0] as { content?: string }).content, "Running subagent...");
|
|
269
|
+
assert.equal((sent[1] as { display?: boolean }).display, true);
|
|
270
|
+
assert.match((sent[1] as { content?: string }).content ?? "", /Commit finished/);
|
|
271
|
+
assert.equal(sessionManager.rewrites, 2);
|
|
272
|
+
assert.equal(sessionManager.flushed, true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("/run finalizes the slash snapshot before the last UI redraw on success", async () => {
|
|
276
|
+
const sent: unknown[] = [];
|
|
277
|
+
const log: string[] = [];
|
|
278
|
+
const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
|
|
279
|
+
const events = createEventBus();
|
|
280
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
281
|
+
const requestId = (data as { requestId: string }).requestId;
|
|
282
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
|
|
283
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
284
|
+
requestId,
|
|
285
|
+
result: {
|
|
286
|
+
content: [{ type: "text", text: "Scout finished" }],
|
|
287
|
+
details: { mode: "single", results: [{ sessionFile: "/tmp/child-session.jsonl" }] },
|
|
288
|
+
},
|
|
289
|
+
isError: false,
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const pi = {
|
|
294
|
+
events,
|
|
295
|
+
registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
|
|
296
|
+
commands.set(name, spec);
|
|
297
|
+
},
|
|
298
|
+
registerShortcut() {},
|
|
299
|
+
sendMessage(message: unknown) {
|
|
300
|
+
sent.push(message);
|
|
301
|
+
log.push(`send:${(message as { display?: boolean }).display === false ? "hidden" : "visible"}`);
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
306
|
+
await commands.get("run")!.handler("scout inspect this", createCommandContext({
|
|
307
|
+
hasUI: true,
|
|
308
|
+
setStatus: (_key, text) => {
|
|
309
|
+
log.push(`status:${text ?? "clear"}`);
|
|
310
|
+
},
|
|
311
|
+
}));
|
|
312
|
+
|
|
313
|
+
assert.equal(sent.length, 2);
|
|
314
|
+
assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
|
|
315
|
+
assert.equal((sent[0] as { display?: boolean }).display, true);
|
|
316
|
+
assert.equal((sent[0] as { content?: string }).content, "inspect this");
|
|
317
|
+
assert.equal((sent[1] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
|
|
318
|
+
assert.equal((sent[1] as { display?: boolean }).display, false);
|
|
319
|
+
assert.match((sent[1] as { content?: string }).content ?? "", /Scout finished/);
|
|
320
|
+
assert.match((sent[1] as { content?: string }).content ?? "", /Child session exports\n\n- `\/tmp\/child-session\.jsonl`/);
|
|
321
|
+
assert.deepEqual(log, ["send:visible", "status:running...", "send:hidden", "status:clear"]);
|
|
322
|
+
|
|
323
|
+
const visibleDetails = resolveSlashMessageDetails!((sent[0] as { details?: unknown }).details);
|
|
324
|
+
assert.ok(visibleDetails);
|
|
325
|
+
const visibleSnapshot = getSlashRenderableSnapshot!(visibleDetails!);
|
|
326
|
+
assert.equal((visibleSnapshot.result.content[0] as { text?: string }).text, "Scout finished");
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it("/run collapses tool detail before showing the initial live card", async () => {
|
|
330
|
+
const log: string[] = [];
|
|
331
|
+
const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
|
|
332
|
+
const events = createEventBus();
|
|
333
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
334
|
+
const requestId = (data as { requestId: string }).requestId;
|
|
335
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
|
|
336
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
337
|
+
requestId,
|
|
338
|
+
result: { content: [{ type: "text", text: "done" }], details: { mode: "single", results: [] } },
|
|
339
|
+
isError: false,
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
const pi = {
|
|
344
|
+
events,
|
|
345
|
+
registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
|
|
346
|
+
commands.set(name, spec);
|
|
347
|
+
},
|
|
348
|
+
registerShortcut() {},
|
|
349
|
+
sendMessage() {
|
|
350
|
+
log.push("send");
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
355
|
+
await commands.get("run")!.handler("scout inspect this", createCommandContext({
|
|
356
|
+
hasUI: true,
|
|
357
|
+
setToolsExpanded: (expanded) => log.push(`expanded:${String(expanded)}`),
|
|
358
|
+
}));
|
|
359
|
+
|
|
360
|
+
assert.deepEqual(log.slice(0, 2), ["expanded:false", "send"]);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("/run finalizes the slash snapshot before the last UI redraw on error", async () => {
|
|
364
|
+
const sent: unknown[] = [];
|
|
365
|
+
const log: string[] = [];
|
|
366
|
+
const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
|
|
367
|
+
const events = createEventBus();
|
|
368
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
369
|
+
const requestId = (data as { requestId: string }).requestId;
|
|
370
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId });
|
|
371
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
372
|
+
requestId,
|
|
373
|
+
result: {
|
|
374
|
+
content: [{ type: "text", text: "Subagent failed" }],
|
|
375
|
+
details: { mode: "single", results: [] },
|
|
376
|
+
},
|
|
377
|
+
isError: true,
|
|
378
|
+
errorText: "Subagent failed",
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
const pi = {
|
|
383
|
+
events,
|
|
384
|
+
registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
|
|
385
|
+
commands.set(name, spec);
|
|
386
|
+
},
|
|
387
|
+
registerShortcut() {},
|
|
388
|
+
sendMessage(message: unknown) {
|
|
389
|
+
sent.push(message);
|
|
390
|
+
log.push(`send:${(message as { display?: boolean }).display === false ? "hidden" : "visible"}`);
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
395
|
+
await commands.get("run")!.handler("scout inspect this", createCommandContext({
|
|
396
|
+
hasUI: true,
|
|
397
|
+
setStatus: (_key, text) => {
|
|
398
|
+
log.push(`status:${text ?? "clear"}`);
|
|
399
|
+
},
|
|
400
|
+
}));
|
|
401
|
+
|
|
402
|
+
assert.equal(sent.length, 2);
|
|
403
|
+
assert.equal((sent[0] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
|
|
404
|
+
assert.equal((sent[0] as { display?: boolean }).display, true);
|
|
405
|
+
assert.equal((sent[0] as { content?: string }).content, "inspect this");
|
|
406
|
+
assert.equal((sent[1] as { customType?: string; display?: boolean }).customType, SLASH_RESULT_TYPE);
|
|
407
|
+
assert.equal((sent[1] as { display?: boolean }).display, false);
|
|
408
|
+
assert.match((sent[1] as { content?: string }).content ?? "", /Subagent failed/);
|
|
409
|
+
assert.deepEqual(log, ["send:visible", "status:running...", "send:hidden", "status:clear"]);
|
|
410
|
+
|
|
411
|
+
const visibleDetails = resolveSlashMessageDetails!((sent[0] as { details?: unknown }).details);
|
|
412
|
+
assert.ok(visibleDetails);
|
|
413
|
+
const visibleSnapshot = getSlashRenderableSnapshot!(visibleDetails!);
|
|
414
|
+
assert.equal((visibleSnapshot.result.content[0] as { text?: string }).text, "Subagent failed");
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
it("/parallel forwards inline output behavior config", async () => {
|
|
418
|
+
const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
|
|
419
|
+
const events = createEventBus();
|
|
420
|
+
let requestedParams: unknown;
|
|
421
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
422
|
+
const payload = data as { requestId: string; params?: unknown };
|
|
423
|
+
requestedParams = payload.params;
|
|
424
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
|
|
425
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
426
|
+
requestId: payload.requestId,
|
|
427
|
+
result: {
|
|
428
|
+
content: [{ type: "text", text: "parallel finished" }],
|
|
429
|
+
details: { mode: "parallel", results: [] },
|
|
430
|
+
},
|
|
431
|
+
isError: false,
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const pi = {
|
|
436
|
+
events,
|
|
437
|
+
registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
|
|
438
|
+
commands.set(name, spec);
|
|
439
|
+
},
|
|
440
|
+
registerShortcut() {},
|
|
441
|
+
sendMessage(_message: unknown) {},
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
445
|
+
await commands.get("parallel")!.handler("scout[output=x.md,outputMode=file-only,reads=a.md+b.md,progress] -- Review", createCommandContext());
|
|
446
|
+
|
|
447
|
+
assert.deepEqual(requestedParams, {
|
|
448
|
+
tasks: [{ agent: "scout", task: "Review", output: "x.md", outputMode: "file-only", reads: ["a.md", "b.md"], progress: true }],
|
|
449
|
+
clarify: false,
|
|
450
|
+
agentScope: "both",
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
it("/parallel no longer hard-blocks runs above the old 8-task limit before the executor responds", async () => {
|
|
455
|
+
const sent: unknown[] = [];
|
|
456
|
+
const commands = new Map<string, { handler(args: string, ctx: unknown): Promise<void> }>();
|
|
457
|
+
const events = createEventBus();
|
|
458
|
+
let requestedTasks = 0;
|
|
459
|
+
events.on(SLASH_SUBAGENT_REQUEST_EVENT, (data) => {
|
|
460
|
+
const payload = data as { requestId: string; params?: { tasks?: unknown[] } };
|
|
461
|
+
requestedTasks = payload.params?.tasks?.length ?? 0;
|
|
462
|
+
events.emit(SLASH_SUBAGENT_STARTED_EVENT, { requestId: payload.requestId });
|
|
463
|
+
events.emit(SLASH_SUBAGENT_RESPONSE_EVENT, {
|
|
464
|
+
requestId: payload.requestId,
|
|
465
|
+
result: {
|
|
466
|
+
content: [{ type: "text", text: "parallel finished" }],
|
|
467
|
+
details: { mode: "parallel", results: [] },
|
|
468
|
+
},
|
|
469
|
+
isError: false,
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const pi = {
|
|
474
|
+
events,
|
|
475
|
+
registerCommand(name: string, spec: { handler(args: string, ctx: unknown): Promise<void> }) {
|
|
476
|
+
commands.set(name, spec);
|
|
477
|
+
},
|
|
478
|
+
registerShortcut() {},
|
|
479
|
+
sendMessage(message: unknown) {
|
|
480
|
+
sent.push(message);
|
|
481
|
+
},
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
485
|
+
const args = Array.from({ length: 9 }, (_, index) => `scout \"task ${index + 1}\"`).join(" -> ");
|
|
486
|
+
await commands.get("parallel")!.handler(args, createCommandContext());
|
|
487
|
+
|
|
488
|
+
assert.equal(requestedTasks, 9);
|
|
489
|
+
assert.equal(sent.length, 2);
|
|
490
|
+
assert.match((sent[1] as { content?: string }).content ?? "", /parallel finished/);
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
describe("saved chain slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
495
|
+
beforeEach(() => {
|
|
496
|
+
clearSlashSnapshots?.();
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("/run and /chain accept dotted packaged runtime agent names", async () => {
|
|
500
|
+
await withTempProject("pi-packaged-agent-slash-", async (root) => {
|
|
501
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", "code-analysis.scout.md"), `---
|
|
502
|
+
name: scout
|
|
503
|
+
package: code-analysis
|
|
504
|
+
description: Fast recon
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
Inspect
|
|
508
|
+
`, "utf-8");
|
|
509
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", "documentation.writer.md"), `---
|
|
510
|
+
name: writer
|
|
511
|
+
package: documentation
|
|
512
|
+
description: Writer
|
|
513
|
+
---
|
|
514
|
+
|
|
515
|
+
Write
|
|
516
|
+
`, "utf-8");
|
|
517
|
+
|
|
518
|
+
const run = await captureSlashCommandParams("run", "code-analysis.scout Investigate", root);
|
|
519
|
+
assert.deepEqual(run.params, { agent: "code-analysis.scout", task: "Investigate", clarify: false, agentScope: "both" });
|
|
520
|
+
|
|
521
|
+
const chain = await captureSlashCommandParams("chain", "code-analysis.scout \"Scan\" -> documentation.writer", root);
|
|
522
|
+
assert.deepEqual((chain.params as { chain?: Array<{ agent?: string; task?: string }> }).chain?.map(({ agent, task }) => ({ agent, task })), [
|
|
523
|
+
{ agent: "code-analysis.scout", task: "Scan" },
|
|
524
|
+
{ agent: "documentation.writer", task: undefined },
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
await withIsolatedHome(async () => {
|
|
528
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
529
|
+
const pi = {
|
|
530
|
+
events: createEventBus(),
|
|
531
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
532
|
+
registerShortcut() {},
|
|
533
|
+
sendMessage(_message: unknown) {},
|
|
534
|
+
};
|
|
535
|
+
registerSlashCommands!(pi, createState(root));
|
|
536
|
+
const runCompletions = commands.get("run")!.getArgumentCompletions!("code-") as Array<{ value: string; label: string }>;
|
|
537
|
+
assert.deepEqual(runCompletions.map((completion) => completion.value), ["code-analysis.scout"]);
|
|
538
|
+
const chainCompletions = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout \"Scan\" -> doc") as Array<{ value: string; label: string }>;
|
|
539
|
+
assert.deepEqual(chainCompletions.map((completion) => completion.value), ["code-analysis.scout \"Scan\" -> documentation.writer"]);
|
|
540
|
+
// Regression: bare group-ish syntax inside a `--` shared task is plain text, not
|
|
541
|
+
// a group separator, so it must not resume agent completion past the task.
|
|
542
|
+
const pipeInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do x | doc");
|
|
543
|
+
assert.equal(pipeInTask, null);
|
|
544
|
+
const openParenInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do (doc");
|
|
545
|
+
assert.equal(openParenInTask, null);
|
|
546
|
+
const closeParenInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do ) doc");
|
|
547
|
+
assert.equal(closeParenInTask, null);
|
|
548
|
+
const balancedParenInTask = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout -- do (x) doc");
|
|
549
|
+
assert.equal(balancedParenInTask, null);
|
|
550
|
+
// Inside an actual parallel group, `|` still separates tasks and completes agents.
|
|
551
|
+
const groupCompletions = commands.get("chain")!.getArgumentCompletions!("code-analysis.scout \"Scan\" -> (documentation.writer \"w\" | code") as Array<{ value: string; label: string }>;
|
|
552
|
+
assert.deepEqual(groupCompletions.map((completion) => completion.value), ["code-analysis.scout \"Scan\" -> (documentation.writer \"w\" | code-analysis.scout"]);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("/run-chain launches a saved chain with a shared task", async () => {
|
|
558
|
+
await withTempProject("pi-run-chain-success-", async (root) => {
|
|
559
|
+
writeProjectChain(root, "review-flow.chain.md", `---
|
|
560
|
+
name: review-flow
|
|
561
|
+
description: Review flow
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## scout
|
|
565
|
+
|
|
566
|
+
Scan {task}
|
|
567
|
+
|
|
568
|
+
## reviewer
|
|
569
|
+
|
|
570
|
+
Review {previous}
|
|
571
|
+
`);
|
|
572
|
+
|
|
573
|
+
const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Audit the auth flow", root);
|
|
574
|
+
const runParams = params as {
|
|
575
|
+
chain?: Array<{ agent?: string; task?: string }>;
|
|
576
|
+
task?: string;
|
|
577
|
+
clarify?: boolean;
|
|
578
|
+
agentScope?: string;
|
|
579
|
+
async?: unknown;
|
|
580
|
+
context?: unknown;
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
assert.deepEqual(runParams.chain?.map(({ agent, task }) => ({ agent, task })), [
|
|
584
|
+
{ agent: "scout", task: "Scan {task}" },
|
|
585
|
+
{ agent: "reviewer", task: "Review {previous}" },
|
|
586
|
+
]);
|
|
587
|
+
assert.equal(runParams.task, "Audit the auth flow");
|
|
588
|
+
assert.equal(runParams.clarify, false);
|
|
589
|
+
assert.equal(runParams.agentScope, "both");
|
|
590
|
+
assert.equal(runParams.async, undefined);
|
|
591
|
+
assert.equal(runParams.context, undefined);
|
|
592
|
+
});
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("/run-chain launches a saved JSON chain with dynamic fanout", async () => {
|
|
596
|
+
await withTempProject("pi-run-chain-json-dynamic-", async (root) => {
|
|
597
|
+
writeProjectChain(root, "dynamic-review.chain.json", JSON.stringify({
|
|
598
|
+
name: "dynamic-review",
|
|
599
|
+
description: "Dynamic review flow",
|
|
600
|
+
chain: [
|
|
601
|
+
{ agent: "scout", task: "Return targets", as: "targets", outputSchema: { type: "object" } },
|
|
602
|
+
{
|
|
603
|
+
expand: { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 },
|
|
604
|
+
parallel: { agent: "reviewer", task: "Review {target.path}", outputSchema: { type: "object" } },
|
|
605
|
+
collect: { as: "reviews" },
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
}));
|
|
609
|
+
|
|
610
|
+
const { params } = await captureSlashCommandParams("run-chain", "dynamic-review -- Audit", root);
|
|
611
|
+
const runParams = params as { chain?: Array<Record<string, unknown>>; task?: string; clarify?: boolean; agentScope?: string };
|
|
612
|
+
|
|
613
|
+
assert.equal(runParams.task, "Audit");
|
|
614
|
+
assert.equal(runParams.clarify, false);
|
|
615
|
+
assert.equal(runParams.agentScope, "both");
|
|
616
|
+
assert.equal(runParams.chain?.[0]?.agent, "scout");
|
|
617
|
+
assert.deepEqual(runParams.chain?.[1]?.expand, { from: { output: "targets", path: "/items" }, item: "target", key: "/path", maxItems: 4 });
|
|
618
|
+
assert.deepEqual(runParams.chain?.[1]?.collect, { as: "reviews" });
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it("/run-chain preserves saved JSON chain acceptance contracts", async () => {
|
|
623
|
+
await withTempProject("pi-run-chain-json-acceptance-", async (root) => {
|
|
624
|
+
writeProjectChain(root, "verified-flow.chain.json", JSON.stringify({
|
|
625
|
+
name: "verified-flow",
|
|
626
|
+
description: "Verified flow",
|
|
627
|
+
chain: [
|
|
628
|
+
{
|
|
629
|
+
agent: "worker",
|
|
630
|
+
task: "Implement fix",
|
|
631
|
+
acceptance: {
|
|
632
|
+
level: "verified",
|
|
633
|
+
verify: [{ id: "tests", command: "npm test" }],
|
|
634
|
+
},
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
}));
|
|
638
|
+
|
|
639
|
+
const { params } = await captureSlashCommandParams("run-chain", "verified-flow -- Audit", root);
|
|
640
|
+
assert.deepEqual((params as { chain?: Array<{ acceptance?: unknown }> }).chain?.[0]?.acceptance, {
|
|
641
|
+
level: "verified",
|
|
642
|
+
verify: [{ id: "tests", command: "npm test" }],
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
it("/run-chain launches and completes packaged saved chains by dotted runtime name", async () => {
|
|
648
|
+
await withTempProject("pi-run-chain-packaged-", async (root) => {
|
|
649
|
+
writeProjectChain(root, "code-analysis.review-flow.chain.md", `---
|
|
650
|
+
name: review-flow
|
|
651
|
+
package: code-analysis
|
|
652
|
+
description: Review flow
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## code-analysis.scout
|
|
656
|
+
|
|
657
|
+
Scan {task}
|
|
658
|
+
`);
|
|
659
|
+
|
|
660
|
+
const { params } = await captureSlashCommandParams("run-chain", "code-analysis.review-flow -- Audit", root);
|
|
661
|
+
assert.equal((params as { task?: string }).task, "Audit");
|
|
662
|
+
assert.deepEqual((params as { chain?: Array<{ agent?: string; task?: string }> }).chain?.map(({ agent, task }) => ({ agent, task })), [
|
|
663
|
+
{ agent: "code-analysis.scout", task: "Scan {task}" },
|
|
664
|
+
]);
|
|
665
|
+
|
|
666
|
+
await withIsolatedHome(async () => {
|
|
667
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
668
|
+
const pi = {
|
|
669
|
+
events: createEventBus(),
|
|
670
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
671
|
+
registerShortcut() {},
|
|
672
|
+
sendMessage(_message: unknown) {},
|
|
673
|
+
};
|
|
674
|
+
registerSlashCommands!(pi, createState(root));
|
|
675
|
+
const completions = commands.get("run-chain")!.getArgumentCompletions!("code-") as Array<{ value: string; label: string }>;
|
|
676
|
+
assert.deepEqual(completions.map((completion) => completion.value), ["code-analysis.review-flow"]);
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
it("/run-chain reports an unknown saved chain without launching", async () => {
|
|
682
|
+
await withTempProject("pi-run-chain-unknown-", async (root) => {
|
|
683
|
+
const { params, notifications } = await captureSlashCommandParams("run-chain", "missing -- Do work", root);
|
|
684
|
+
|
|
685
|
+
assert.equal(params, undefined);
|
|
686
|
+
assert.deepEqual(notifications, ["Unknown chain: missing"]);
|
|
687
|
+
});
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
it("/run-chain suggests saved chain names", async () => {
|
|
691
|
+
await withTempProject("pi-run-chain-completions-", async (root) => {
|
|
692
|
+
writeProjectChain(root, "review-flow.chain.md", `---
|
|
693
|
+
name: review-flow
|
|
694
|
+
description: Review flow
|
|
695
|
+
---
|
|
696
|
+
|
|
697
|
+
## scout
|
|
698
|
+
|
|
699
|
+
Scan
|
|
700
|
+
`);
|
|
701
|
+
writeProjectChain(root, "release-flow.chain.md", `---
|
|
702
|
+
name: release-flow
|
|
703
|
+
description: Release flow
|
|
704
|
+
---
|
|
705
|
+
|
|
706
|
+
## planner
|
|
707
|
+
|
|
708
|
+
Plan
|
|
709
|
+
`);
|
|
710
|
+
writeProjectChain(root, "triage.chain.md", `---
|
|
711
|
+
name: triage
|
|
712
|
+
description: Triage flow
|
|
713
|
+
---
|
|
714
|
+
|
|
715
|
+
## scout
|
|
716
|
+
|
|
717
|
+
Triage
|
|
718
|
+
`);
|
|
719
|
+
|
|
720
|
+
await withIsolatedHome(async () => {
|
|
721
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
722
|
+
const pi = {
|
|
723
|
+
events: createEventBus(),
|
|
724
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) {
|
|
725
|
+
commands.set(name, spec);
|
|
726
|
+
},
|
|
727
|
+
registerShortcut() {},
|
|
728
|
+
sendMessage(_message: unknown) {},
|
|
729
|
+
};
|
|
730
|
+
|
|
731
|
+
registerSlashCommands!(pi, createState(root));
|
|
732
|
+
const completions = commands.get("run-chain")!.getArgumentCompletions!("re") as Array<{ value: string; label: string }>;
|
|
733
|
+
assert.deepEqual(completions.map((completion) => completion.value).sort(), ["release-flow", "review-flow"]);
|
|
734
|
+
assert.deepEqual(completions.map((completion) => completion.label).sort(), ["release-flow", "review-flow"]);
|
|
735
|
+
assert.equal(commands.get("run-chain")!.getArgumentCompletions!("review-flow -- "), null);
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
it("/run-chain maps --bg to async execution", async () => {
|
|
741
|
+
await withTempProject("pi-run-chain-bg-", async (root) => {
|
|
742
|
+
writeProjectChain(root, "review-flow.chain.md", `---
|
|
743
|
+
name: review-flow
|
|
744
|
+
description: Review flow
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## scout
|
|
748
|
+
|
|
749
|
+
Scan
|
|
750
|
+
`);
|
|
751
|
+
|
|
752
|
+
const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Audit --bg", root);
|
|
753
|
+
|
|
754
|
+
assert.equal((params as { async?: unknown }).async, true);
|
|
755
|
+
assert.equal((params as { context?: unknown }).context, undefined);
|
|
756
|
+
});
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it("/run-chain maps --fork to forked context", async () => {
|
|
760
|
+
await withTempProject("pi-run-chain-fork-", async (root) => {
|
|
761
|
+
writeProjectChain(root, "review-flow.chain.md", `---
|
|
762
|
+
name: review-flow
|
|
763
|
+
description: Review flow
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
## scout
|
|
767
|
+
|
|
768
|
+
Scan
|
|
769
|
+
`);
|
|
770
|
+
|
|
771
|
+
const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Audit --fork", root);
|
|
772
|
+
|
|
773
|
+
assert.equal((params as { context?: unknown }).context, "fork");
|
|
774
|
+
assert.equal((params as { async?: unknown }).async, undefined);
|
|
775
|
+
});
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
it("/run-chain prefers a project saved chain over a same-named user chain", async () => {
|
|
779
|
+
await withTempProject("pi-run-chain-priority-", async (root) => {
|
|
780
|
+
writeProjectChain(root, "review-flow.chain.md", `---
|
|
781
|
+
name: review-flow
|
|
782
|
+
description: Project review flow
|
|
783
|
+
---
|
|
784
|
+
|
|
785
|
+
## scout
|
|
786
|
+
|
|
787
|
+
Project chain task
|
|
788
|
+
`);
|
|
789
|
+
|
|
790
|
+
const { params } = await captureSlashCommandParams("run-chain", "review-flow -- Shared task", root, () => {
|
|
791
|
+
const userChainsDir = path.join(os.homedir(), ".selesai", "agent", "chains");
|
|
792
|
+
fs.mkdirSync(userChainsDir, { recursive: true });
|
|
793
|
+
fs.writeFileSync(path.join(userChainsDir, "review-flow.chain.md"), `---
|
|
794
|
+
name: review-flow
|
|
795
|
+
description: User review flow
|
|
796
|
+
---
|
|
797
|
+
|
|
798
|
+
## scout
|
|
799
|
+
|
|
800
|
+
User chain task
|
|
801
|
+
`, "utf-8");
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
assert.equal((params as { chain?: Array<{ task?: string }> }).chain?.[0]?.task, "Project chain task");
|
|
805
|
+
});
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
it("/run-chain resolves saved outputSchema files at the command boundary", async () => {
|
|
809
|
+
await withTempProject("pi-run-chain-schema-", async (root) => {
|
|
810
|
+
const schemasDir = path.join(root, ".selesai", "chains", "schemas");
|
|
811
|
+
fs.mkdirSync(schemasDir, { recursive: true });
|
|
812
|
+
fs.writeFileSync(path.join(schemasDir, "finding.schema.json"), JSON.stringify({ type: "object", properties: { ok: { type: "boolean" } } }), "utf-8");
|
|
813
|
+
writeProjectChain(root, "schema-flow.chain.md", `---
|
|
814
|
+
name: schema-flow
|
|
815
|
+
description: Schema flow
|
|
816
|
+
---
|
|
817
|
+
|
|
818
|
+
## scout
|
|
819
|
+
outputSchema: ./schemas/finding.schema.json
|
|
820
|
+
|
|
821
|
+
Gather context
|
|
822
|
+
`);
|
|
823
|
+
|
|
824
|
+
const { params } = await captureSlashCommandParams("run-chain", "schema-flow -- Shared task", root);
|
|
825
|
+
|
|
826
|
+
assert.deepEqual((params as { chain?: Array<{ outputSchema?: unknown }> }).chain?.[0]?.outputSchema, {
|
|
827
|
+
type: "object",
|
|
828
|
+
properties: { ok: { type: "boolean" } },
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it("/run-chain preserves saved step behavior fields", async () => {
|
|
834
|
+
await withTempProject("pi-run-chain-fields-", async (root) => {
|
|
835
|
+
writeProjectChain(root, "field-flow.chain.md", `---
|
|
836
|
+
name: field-flow
|
|
837
|
+
description: Field flow
|
|
838
|
+
---
|
|
839
|
+
|
|
840
|
+
## scout
|
|
841
|
+
output: context.md
|
|
842
|
+
outputMode: file-only
|
|
843
|
+
reads: input.md, notes.md
|
|
844
|
+
model: openai/gpt-5.5
|
|
845
|
+
skills: research, audit
|
|
846
|
+
progress: true
|
|
847
|
+
|
|
848
|
+
Gather context
|
|
849
|
+
`);
|
|
850
|
+
|
|
851
|
+
const { params } = await captureSlashCommandParams("run-chain", "field-flow -- Shared task", root);
|
|
852
|
+
|
|
853
|
+
assert.deepEqual((params as { chain?: unknown[] }).chain?.[0], {
|
|
854
|
+
agent: "scout",
|
|
855
|
+
task: "Gather context",
|
|
856
|
+
output: "context.md",
|
|
857
|
+
outputMode: "file-only",
|
|
858
|
+
reads: ["input.md", "notes.md"],
|
|
859
|
+
progress: true,
|
|
860
|
+
skill: ["research", "audit"],
|
|
861
|
+
model: "openai/gpt-5.5",
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it("/chain parses a parenthesized parallel group into a { parallel: [...] } step", async () => {
|
|
867
|
+
await withTempProject("pi-chain-group-slash-", async (root) => {
|
|
868
|
+
for (const name of ["scout", "reviewer", "writer"]) {
|
|
869
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const { params, notifications } = await captureSlashCommandParams("chain", 'scout "scan" -> (reviewer "A" | reviewer "B") -> writer "fix"', root);
|
|
873
|
+
assert.deepEqual(notifications, []);
|
|
874
|
+
const built = params as { chain?: Array<Record<string, unknown>>; task?: string };
|
|
875
|
+
assert.equal(built.task, "scan");
|
|
876
|
+
assert.equal(built.chain?.length, 3);
|
|
877
|
+
assert.equal(built.chain?.[0]?.agent, "scout");
|
|
878
|
+
const parallel = built.chain?.[1]?.parallel as Array<{ agent: string; task: string }>;
|
|
879
|
+
assert.ok(Array.isArray(parallel), "second step should be a parallel group");
|
|
880
|
+
assert.deepEqual(parallel.map(({ agent, task }) => ({ agent, task })), [
|
|
881
|
+
{ agent: "reviewer", task: "A" },
|
|
882
|
+
{ agent: "reviewer", task: "B" },
|
|
883
|
+
]);
|
|
884
|
+
assert.equal(built.chain?.[2]?.agent, "writer");
|
|
885
|
+
});
|
|
886
|
+
});
|
|
887
|
+
|
|
888
|
+
it("/chain reports parallel-group errors as notifications and does not launch", async () => {
|
|
889
|
+
await withTempProject("pi-chain-group-error-", async (root) => {
|
|
890
|
+
for (const name of ["scout", "reviewer"]) {
|
|
891
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const { params, notifications } = await captureSlashCommandParams("chain", 'scout "scan" -> (reviewer "A")', root);
|
|
895
|
+
assert.equal(params, undefined);
|
|
896
|
+
assert.equal(notifications.length, 1);
|
|
897
|
+
assert.match(notifications[0] ?? "", /at least two/i);
|
|
898
|
+
});
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
it("/chain carries inline metadata and group options through to params", async () => {
|
|
902
|
+
await withTempProject("pi-chain-group-meta-", async (root) => {
|
|
903
|
+
for (const name of ["scout", "reviewer", "writer"]) {
|
|
904
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const { params, notifications } = await captureSlashCommandParams(
|
|
908
|
+
"chain",
|
|
909
|
+
'scout[as=ctx,phase=recon] "scan" -> (reviewer "A" | writer "B")[concurrency=2,failFast]',
|
|
910
|
+
root,
|
|
911
|
+
);
|
|
912
|
+
assert.deepEqual(notifications, []);
|
|
913
|
+
const built = params as { chain?: Array<Record<string, unknown>> };
|
|
914
|
+
assert.equal(built.chain?.[0]?.as, "ctx");
|
|
915
|
+
assert.equal(built.chain?.[0]?.phase, "recon");
|
|
916
|
+
const group = built.chain?.[1] as Record<string, unknown>;
|
|
917
|
+
assert.equal((group.parallel as unknown[]).length, 2);
|
|
918
|
+
assert.equal(group.concurrency, 2);
|
|
919
|
+
assert.equal(group.failFast, true);
|
|
920
|
+
});
|
|
921
|
+
});
|
|
922
|
+
|
|
923
|
+
it("/chain tab-completion works inside parallel groups", async () => {
|
|
924
|
+
await withTempProject("pi-chain-group-complete-", async (root) => {
|
|
925
|
+
for (const name of ["scout", "reviewer", "writer"]) {
|
|
926
|
+
fs.writeFileSync(path.join(root, ".selesai", "agents", `${name}.md`), `---\nname: ${name}\ndescription: ${name}\n---\n\nBody\n`, "utf-8");
|
|
927
|
+
}
|
|
928
|
+
await withIsolatedHome(async () => {
|
|
929
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
930
|
+
const pi = {
|
|
931
|
+
events: createEventBus(),
|
|
932
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
933
|
+
registerShortcut() {},
|
|
934
|
+
sendMessage(_message: unknown) {},
|
|
935
|
+
};
|
|
936
|
+
registerSlashCommands!(pi, createState(root));
|
|
937
|
+
const complete = (prefix: string) =>
|
|
938
|
+
(commands.get("chain")!.getArgumentCompletions!(prefix) as Array<{ value: string }> | null)?.map((c) => c.value) ?? null;
|
|
939
|
+
|
|
940
|
+
// after `(`
|
|
941
|
+
assert.deepEqual(complete('scout "scan" -> (rev'), ['scout "scan" -> (reviewer']);
|
|
942
|
+
// after `|`
|
|
943
|
+
assert.deepEqual(complete('scout "scan" -> (reviewer "A" | wr'), ['scout "scan" -> (reviewer "A" | writer']);
|
|
944
|
+
// after a bare `|` a space is inserted before every suggested agent
|
|
945
|
+
const barePipe = complete('scout "scan" -> (reviewer "A" |');
|
|
946
|
+
assert.ok(barePipe && barePipe.length > 0);
|
|
947
|
+
assert.ok(barePipe.every((v) => v.startsWith('scout "scan" -> (reviewer "A" | ')));
|
|
948
|
+
assert.ok(barePipe.includes('scout "scan" -> (reviewer "A" | writer'));
|
|
949
|
+
// inside an open quote: no agent completion
|
|
950
|
+
assert.equal(complete('scout "scan'), null);
|
|
951
|
+
});
|
|
952
|
+
});
|
|
953
|
+
});
|
|
954
|
+
});
|
|
955
|
+
|
|
956
|
+
|
|
957
|
+
describe("subagents-models slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
958
|
+
beforeEach(() => {
|
|
959
|
+
clearSlashSnapshots?.();
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it("routes to the models tool action", async () => {
|
|
963
|
+
const { params } = await captureSlashCommandParams("subagents-models", "", process.cwd());
|
|
964
|
+
assert.deepEqual(params, { action: "models" });
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
it("passes an optional builtin filter", async () => {
|
|
968
|
+
const { params } = await captureSlashCommandParams("subagents-models", "scout", process.cwd());
|
|
969
|
+
assert.deepEqual(params, { action: "models", agent: "scout" });
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
it("rejects invalid builtin filters without launching", async () => {
|
|
973
|
+
const { params, notifications } = await captureSlashCommandParams("subagents-models", "not-a-builtin", process.cwd());
|
|
974
|
+
assert.equal(params, undefined);
|
|
975
|
+
assert.deepEqual(notifications, ["Unknown builtin agent: not-a-builtin"]);
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
it("suggests builtin agent names", async () => {
|
|
979
|
+
await withIsolatedHome(async () => {
|
|
980
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
981
|
+
const pi = {
|
|
982
|
+
events: createEventBus(),
|
|
983
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) {
|
|
984
|
+
commands.set(name, spec);
|
|
985
|
+
},
|
|
986
|
+
registerShortcut() {},
|
|
987
|
+
sendMessage(_message: unknown) {},
|
|
988
|
+
};
|
|
989
|
+
|
|
990
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
991
|
+
const completions = commands.get("subagents-models")!.getArgumentCompletions!("sc") as Array<{ value: string; label: string }>;
|
|
992
|
+
assert.deepEqual(completions.map((completion) => completion.value), ["scout"]);
|
|
993
|
+
});
|
|
994
|
+
});
|
|
995
|
+
});
|
|
996
|
+
|
|
997
|
+
describe("subagent cost slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
998
|
+
it("reports parent and child usage from the current session branch", async () => {
|
|
999
|
+
const sent: unknown[] = [];
|
|
1000
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1001
|
+
const pi = {
|
|
1002
|
+
events: createEventBus(),
|
|
1003
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) {
|
|
1004
|
+
commands.set(name, spec);
|
|
1005
|
+
},
|
|
1006
|
+
registerShortcut() {},
|
|
1007
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1008
|
+
};
|
|
1009
|
+
const parentUsage = {
|
|
1010
|
+
input: 100,
|
|
1011
|
+
output: 50,
|
|
1012
|
+
cacheRead: 10,
|
|
1013
|
+
cacheWrite: 5,
|
|
1014
|
+
cost: { input: 0.001, output: 0.002, cacheRead: 0, cacheWrite: 0, total: 0.003 },
|
|
1015
|
+
};
|
|
1016
|
+
const childUsage = { input: 20, output: 10, cacheRead: 2, cacheWrite: 1, cost: 0.004, turns: 1 };
|
|
1017
|
+
const slashChildUsage = { input: 30, output: 15, cacheRead: 0, cacheWrite: 0, cost: 0.005, turns: 2 };
|
|
1018
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1019
|
+
await commands.get("subagent-cost")!.handler("", createCommandContext({
|
|
1020
|
+
sessionManager: {
|
|
1021
|
+
getBranch: () => [
|
|
1022
|
+
{ type: "message", message: { role: "assistant", usage: parentUsage } },
|
|
1023
|
+
{
|
|
1024
|
+
type: "message",
|
|
1025
|
+
message: {
|
|
1026
|
+
role: "toolResult",
|
|
1027
|
+
toolName: "subagent",
|
|
1028
|
+
details: {
|
|
1029
|
+
mode: "single",
|
|
1030
|
+
results: [{ agent: "worker", task: "fix", exitCode: 0, messages: [], usage: childUsage, sessionFile: "/tmp/worker.jsonl" }],
|
|
1031
|
+
},
|
|
1032
|
+
},
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
type: "custom_message",
|
|
1036
|
+
customType: SLASH_RESULT_TYPE,
|
|
1037
|
+
details: {
|
|
1038
|
+
requestId: "slash-1",
|
|
1039
|
+
result: {
|
|
1040
|
+
content: [{ type: "text", text: "done" }],
|
|
1041
|
+
details: {
|
|
1042
|
+
mode: "single",
|
|
1043
|
+
results: [{ agent: "reviewer", task: "review", exitCode: 0, messages: [], usage: slashChildUsage }],
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
],
|
|
1049
|
+
},
|
|
1050
|
+
}));
|
|
1051
|
+
|
|
1052
|
+
const output = String((sent[0] as { content?: unknown }).content ?? "");
|
|
1053
|
+
assert.match(output, /Parent: ↑100 ↓50 \$0\.0030/);
|
|
1054
|
+
assert.match(output, /Child 1 \(worker\): ↑20 ↓10 \$0\.0040/);
|
|
1055
|
+
assert.match(output, /Session: \/tmp\/worker\.jsonl/);
|
|
1056
|
+
assert.match(output, /Child 2 \(reviewer\): ↑30 ↓15 \$0\.0050/);
|
|
1057
|
+
assert.match(output, /Children: ↑50 ↓25 \$0\.0090/);
|
|
1058
|
+
assert.match(output, /Total: ↑150 ↓75 \$0\.0120/);
|
|
1059
|
+
});
|
|
1060
|
+
});
|
|
1061
|
+
|
|
1062
|
+
describe("subagent profiles slash commands", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
1063
|
+
it("lists saved profiles", async () => {
|
|
1064
|
+
await withIsolatedHome(async () => {
|
|
1065
|
+
const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
|
|
1066
|
+
fs.mkdirSync(profilesDir, { recursive: true });
|
|
1067
|
+
fs.writeFileSync(path.join(profilesDir, "openai-codex.quota.json"), JSON.stringify({ subagents: { agentOverrides: {} } }));
|
|
1068
|
+
fs.writeFileSync(path.join(profilesDir, "openai-codex.quality.json"), JSON.stringify({ subagents: { agentOverrides: {} } }));
|
|
1069
|
+
const sent: unknown[] = [];
|
|
1070
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1071
|
+
const pi = {
|
|
1072
|
+
events: createEventBus(),
|
|
1073
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) {
|
|
1074
|
+
commands.set(name, spec);
|
|
1075
|
+
},
|
|
1076
|
+
registerShortcut() {},
|
|
1077
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1078
|
+
};
|
|
1079
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1080
|
+
await commands.get("subagents-profiles")!.handler("", createCommandContext());
|
|
1081
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /openai-codex\.quota/);
|
|
1082
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /openai-codex\.quality/);
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
it("loads a saved profile into user settings", async () => {
|
|
1087
|
+
await withIsolatedHome(async () => {
|
|
1088
|
+
const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
|
|
1089
|
+
fs.mkdirSync(profilesDir, { recursive: true });
|
|
1090
|
+
fs.writeFileSync(path.join(profilesDir, "openai-codex.quota.json"), JSON.stringify({
|
|
1091
|
+
subagents: { agentOverrides: {
|
|
1092
|
+
scout: { model: "openai-codex/gpt-5.3-codex-spark" },
|
|
1093
|
+
worker: { model: "openai-codex/gpt-5.4" },
|
|
1094
|
+
} },
|
|
1095
|
+
}, null, 2));
|
|
1096
|
+
const sent: unknown[] = [];
|
|
1097
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1098
|
+
const pi = {
|
|
1099
|
+
events: createEventBus(),
|
|
1100
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
1101
|
+
registerShortcut() {},
|
|
1102
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1103
|
+
};
|
|
1104
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1105
|
+
await commands.get("subagents-load-profile")!.handler("openai-codex.quota", createCommandContext());
|
|
1106
|
+
const settings = JSON.parse(fs.readFileSync(path.join(process.env.HOME!, ".selesai", "agent", "settings.json"), "utf-8"));
|
|
1107
|
+
assert.equal(settings.subagents.agentOverrides.scout.model, "openai-codex/gpt-5.3-codex-spark");
|
|
1108
|
+
assert.equal(settings.subagents.agentOverrides.worker.model, "openai-codex/gpt-5.4");
|
|
1109
|
+
assert.doesNotMatch(String((sent[0] as { content?: unknown }).content ?? ""), /run \/reload/);
|
|
1110
|
+
});
|
|
1111
|
+
});
|
|
1112
|
+
|
|
1113
|
+
it("can switch the current session model to the loaded profile worker model", async () => {
|
|
1114
|
+
await withIsolatedHome(async () => {
|
|
1115
|
+
const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
|
|
1116
|
+
fs.mkdirSync(profilesDir, { recursive: true });
|
|
1117
|
+
fs.writeFileSync(path.join(profilesDir, "openai-codex.quota.json"), JSON.stringify({
|
|
1118
|
+
subagents: { agentOverrides: { worker: { model: "gpt-5.4:high" } } },
|
|
1119
|
+
}, null, 2));
|
|
1120
|
+
const sent: unknown[] = [];
|
|
1121
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1122
|
+
let setModelArg: unknown;
|
|
1123
|
+
const resolvedModel = { provider: "openai-codex", id: "gpt-5.4" };
|
|
1124
|
+
const pi = {
|
|
1125
|
+
events: createEventBus(),
|
|
1126
|
+
async setModel(model: unknown) { setModelArg = model; return true; },
|
|
1127
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
1128
|
+
registerShortcut() {},
|
|
1129
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1130
|
+
};
|
|
1131
|
+
registerSlashCommands!(pi as never, createState(process.cwd()));
|
|
1132
|
+
await commands.get("subagents-load-profile")!.handler("openai-codex.quota", createCommandContext({
|
|
1133
|
+
confirm: async () => true,
|
|
1134
|
+
modelRegistry: {
|
|
1135
|
+
getAvailable: () => [{ provider: "openai-codex", id: "gpt-5.4" }],
|
|
1136
|
+
find: (provider, id) => provider === "openai-codex" && id === "gpt-5.4" ? resolvedModel : undefined,
|
|
1137
|
+
},
|
|
1138
|
+
}) as never);
|
|
1139
|
+
assert.equal(setModelArg, resolvedModel);
|
|
1140
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Current session model switched to: openai-codex\/gpt-5.4/);
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
it("refreshes a provider model catalog", async () => {
|
|
1145
|
+
await withIsolatedHome(async () => {
|
|
1146
|
+
const sent: unknown[] = [];
|
|
1147
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1148
|
+
const pi = {
|
|
1149
|
+
events: createEventBus(),
|
|
1150
|
+
exec: async () => ({ stdout: "OK\n", stderr: "", code: 0, killed: false }),
|
|
1151
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
1152
|
+
registerShortcut() {},
|
|
1153
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1154
|
+
};
|
|
1155
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1156
|
+
await commands.get("subagents-refresh-provider-models")!.handler("openai-codex", createCommandContext({
|
|
1157
|
+
cwd: process.cwd(),
|
|
1158
|
+
modelRegistry: {
|
|
1159
|
+
getAvailable: () => [
|
|
1160
|
+
{ provider: "openai-codex", id: "gpt-5.3-codex-spark", reasoning: true },
|
|
1161
|
+
{ provider: "openai-codex", id: "gpt-5.4-mini", reasoning: true },
|
|
1162
|
+
],
|
|
1163
|
+
},
|
|
1164
|
+
}) as never);
|
|
1165
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Provider: openai-codex/);
|
|
1166
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Warning: 2 models were classified with name heuristics fallback\./);
|
|
1167
|
+
assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents", "providers", "openai-codex.models.json")), true);
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
it("generates provider profiles", async () => {
|
|
1172
|
+
await withIsolatedHome(async () => {
|
|
1173
|
+
const sent: unknown[] = [];
|
|
1174
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1175
|
+
const pi = {
|
|
1176
|
+
events: createEventBus(),
|
|
1177
|
+
exec: async () => ({ stdout: "OK\n", stderr: "", code: 0, killed: false }),
|
|
1178
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
1179
|
+
registerShortcut() {},
|
|
1180
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1181
|
+
};
|
|
1182
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1183
|
+
await commands.get("subagents-generate-profiles")!.handler("openai-codex", createCommandContext({
|
|
1184
|
+
modelRegistry: {
|
|
1185
|
+
getAvailable: () => [
|
|
1186
|
+
{ provider: "openai-codex", id: "gpt-5.3-codex-spark", reasoning: true },
|
|
1187
|
+
{ provider: "openai-codex", id: "gpt-5.4-mini", reasoning: true },
|
|
1188
|
+
{ provider: "openai-codex", id: "gpt-5.4", reasoning: true },
|
|
1189
|
+
{ provider: "openai-codex", id: "gpt-5.5", reasoning: true },
|
|
1190
|
+
],
|
|
1191
|
+
},
|
|
1192
|
+
}) as never);
|
|
1193
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Generated subagent profiles/);
|
|
1194
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /Warning: generated profiles depend on heuristic-only classification for 4 selected models\./);
|
|
1195
|
+
assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents", "openai-codex.quota.json")), true);
|
|
1196
|
+
assert.equal(fs.existsSync(path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents", "openai-codex.quality.json")), true);
|
|
1197
|
+
});
|
|
1198
|
+
});
|
|
1199
|
+
|
|
1200
|
+
it("checks a profile", async () => {
|
|
1201
|
+
await withIsolatedHome(async () => {
|
|
1202
|
+
const profilesDir = path.join(process.env.HOME!, ".selesai", "agent", "profiles", "pi-subagents");
|
|
1203
|
+
fs.mkdirSync(profilesDir, { recursive: true });
|
|
1204
|
+
fs.writeFileSync(path.join(profilesDir, "demo.json"), JSON.stringify({
|
|
1205
|
+
subagents: { agentOverrides: { scout: { model: "openai-codex/gpt-5.3-codex-spark" } } },
|
|
1206
|
+
}, null, 2));
|
|
1207
|
+
const sent: unknown[] = [];
|
|
1208
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1209
|
+
const pi = {
|
|
1210
|
+
events: createEventBus(),
|
|
1211
|
+
exec: async () => ({ stdout: "OK\n", stderr: "", code: 0, killed: false }),
|
|
1212
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
1213
|
+
registerShortcut() {},
|
|
1214
|
+
sendMessage(message: unknown) { sent.push(message); },
|
|
1215
|
+
};
|
|
1216
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1217
|
+
await commands.get("subagents-check-profile")!.handler("demo", createCommandContext({
|
|
1218
|
+
modelRegistry: { getAvailable: () => [{ provider: "openai-codex", id: "gpt-5.3-codex-spark" }] },
|
|
1219
|
+
}) as never);
|
|
1220
|
+
assert.match(String((sent[0] as { content?: unknown }).content ?? ""), /probe ok/);
|
|
1221
|
+
});
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it("suggests provider names for refresh and generate commands", async () => {
|
|
1225
|
+
await withIsolatedHome(async () => {
|
|
1226
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1227
|
+
const pi = {
|
|
1228
|
+
events: createEventBus(),
|
|
1229
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) { commands.set(name, spec); },
|
|
1230
|
+
registerShortcut() {},
|
|
1231
|
+
sendMessage(_message: unknown) {},
|
|
1232
|
+
};
|
|
1233
|
+
const state = createState(process.cwd());
|
|
1234
|
+
state.lastUiContext = createCommandContext({
|
|
1235
|
+
modelRegistry: {
|
|
1236
|
+
getAvailable: () => [
|
|
1237
|
+
{ provider: "openai-codex", id: "gpt-5.4" },
|
|
1238
|
+
{ provider: "openai", id: "gpt-5" },
|
|
1239
|
+
{ provider: "anthropic", id: "claude-sonnet-4" },
|
|
1240
|
+
],
|
|
1241
|
+
},
|
|
1242
|
+
}) as never;
|
|
1243
|
+
registerSlashCommands!(pi, state);
|
|
1244
|
+
const refresh = commands.get("subagents-refresh-provider-models")!.getArgumentCompletions!("open") as Array<{ value: string; label: string }>;
|
|
1245
|
+
const generate = commands.get("subagents-generate-profiles")!.getArgumentCompletions!("an") as Array<{ value: string; label: string }>;
|
|
1246
|
+
assert.deepEqual(refresh.map((entry) => entry.value), ["openai", "openai-codex"]);
|
|
1247
|
+
assert.deepEqual(generate.map((entry) => entry.value), ["anthropic"]);
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
});
|
|
1251
|
+
|
|
1252
|
+
describe("subagents-doctor slash command", { skip: !available ? "slash-commands.ts not importable" : undefined }, () => {
|
|
1253
|
+
beforeEach(() => {
|
|
1254
|
+
clearSlashSnapshots?.();
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
it("routes to the doctor tool action", async () => {
|
|
1258
|
+
const { params } = await captureSlashCommandParams("subagents-doctor", "", process.cwd());
|
|
1259
|
+
assert.deepEqual(params, { action: "doctor" });
|
|
1260
|
+
});
|
|
1261
|
+
|
|
1262
|
+
it("routes fleet to the read-only status view", async () => {
|
|
1263
|
+
const { params } = await captureSlashCommandParams("subagents-fleet", "", process.cwd());
|
|
1264
|
+
assert.deepEqual(params, { action: "status", view: "fleet" });
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1267
|
+
it("does not register the removed subagents-status overlay command", async () => {
|
|
1268
|
+
await withIsolatedHome(async () => {
|
|
1269
|
+
const commands = new Map<string, RegisteredSlashCommand>();
|
|
1270
|
+
const pi = {
|
|
1271
|
+
events: createEventBus(),
|
|
1272
|
+
registerCommand(name: string, spec: RegisteredSlashCommand) {
|
|
1273
|
+
commands.set(name, spec);
|
|
1274
|
+
},
|
|
1275
|
+
registerShortcut() {},
|
|
1276
|
+
sendMessage(_message: unknown) {},
|
|
1277
|
+
};
|
|
1278
|
+
|
|
1279
|
+
registerSlashCommands!(pi, createState(process.cwd()));
|
|
1280
|
+
assert.equal(commands.has("subagents-status"), false);
|
|
1281
|
+
});
|
|
1282
|
+
});
|
|
1283
|
+
|
|
1284
|
+
});
|