Run multiple runtimes in parallel via session tabs

Refactor the sidebar panel from a single-process view into a tabbed
multi-session view: each tab is an independent CliSession with its own
PTY process and xterm terminal. A New session / + control opens a session
from any configured runtime; multiple sessions of the same runtime are
allowed and labels are disambiguated. Stop/Restart/Clear and the @file/
@folder buttons act on the active tab; a single ResizeObserver refits only
the visible terminal and re-fits on tab activation.

Automations now spawn their own session tab for the declared runtime (or
the default when none is declared), await CLI readiness, then send the
prompt — replacing the old skip-if-not-running/runtime-mismatch logic.
Add settings autoCloseAutomationSessions and maxConcurrentSessions; the
Automations modal "Run now" button is always enabled.

Extract pure helpers to session-utils.ts (resolveRuntimeForAutomation,
runtimeMatches, nextSessionLabel, canOpenSession) with unit tests. Update
README and the generated example automation. Bump to 0.2.9 (manifest,
versions.json, package.json, prompt-hub version).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
blamouche 2026-05-20 10:12:26 +02:00
parent 459451b225
commit 073efef75c
14 changed files with 1631 additions and 751 deletions

View file

@ -76,3 +76,11 @@
| 2026-05-20 00:18:00 CEST | agent | Updated README intro paragraph to describe the Automations feature (scheduled/manual prompt files + exportable per-run history) alongside runtime/mention features. Docs only — no code or bundle change; manifest left at 0.2.7. Bumped prompt-hub version 0.2.7 → 0.2.8. | `README.md`, `.prompt-hub/version.md`, `.prompt-hub/releases.md` | success | Commit and push to `feature/automation`. |
| 2026-05-20 00:22:00 CEST | agent | Released 0.2.8 (first release shipping Automations). Aligned manifest/versions to 0.2.8, fast-forward merged `feature/automation` into `main` (8efbce2..55a6b72), pushed main, created+pushed annotated tag `0.2.8`. Release workflow run 26128878146 succeeded and created a DRAFT GitHub Release with 5 assets (any-ai-cli-0.2.8.zip, main.js, manifest.json, styles.css, versions.json). Awaiting user decision to publish the draft. | `manifest.json`, `versions.json`, `.prompt-hub/releases.md`, `git merge`, `git tag 0.2.8`, `gh run watch` | success | Publish the draft release if user confirms. |
| 2026-05-20 00:24:00 CEST | agent | User confirmed publish; ran `gh release edit 0.2.8 --draft=false`. Release 0.2.8 is now public at https://github.com/blamouche/obsidian-any-ai-code/releases/tag/0.2.8. | `gh release edit` | success | Release live. |
## 2026-05-20 10:15 — agent (Claude Opus 4.7)
- Action : implémenté les sessions runtime parallèles à onglets (feature majeure).
- Détails : nouveau `session-utils.ts` (fonctions pures + tests), classe `CliSession` + `createSessionTerminal()` dans `main.ts`, refactor complet de `ClaudeCliView` (sessions[], onglets, startSession/closeSession/setActiveSession, onData/onExit/Codex par session), réécriture de `triggerAutomation` (+`activateViewAndStartSession`) pour spawn un nouvel onglet par run, settings `autoCloseAutomationSessions`/`maxConcurrentSessions`, `automations-modal.ts` "Run now" toujours actif, CSS onglets, README + exemple d'automatisation mis à jour.
- Fichiers : main.ts, session-utils.ts (new), tests/session-utils.test.ts (new), automations-modal.ts, styles.css, package.json, manifest.json, versions.json, README.md, .prompt-hub/{version,releases}.md.
- Validation : `npm run lint` OK, `npm test` 70/70 OK, `npm run build` OK, `tsc --noEmit` sans erreur sur les fichiers du projet (erreurs résiduelles uniquement dans node_modules/vitest).
- Version : 0.2.8 -> 0.2.9.
- Statut : success. Étape suivante : commit + push, puis test manuel dans Obsidian (smoke test du plan).

View file

@ -1,5 +1,12 @@
# Releases
## 0.2.9 - 2026-05-20
- **Parallel runtime sessions in tabs.** The sidebar panel now hosts multiple independent sessions, each with its own PTY process and xterm terminal, shown in a tab bar. A `New session` button / `+` control opens a session from any configured runtime; multiple sessions of the same runtime are allowed and disambiguated (`Claude`, `Claude (2)`, …). Switching tabs refits the visible terminal; the per-tab `×` closes a session. `Stop` / `Restart` / `Clear` / `@active file` / `@active folder` now act on the active tab.
- **Automations spawn their own session.** Each automation run (scheduled or manual) opens a fresh session tab for the declared `runtime` (or the default runtime when none is declared), waits for the CLI to boot, then sends the prompt. Replaces the old "skip if no CLI running / runtime mismatch" behaviour. Unknown declared runtimes are skipped and logged.
- New settings: **Auto-close automation sessions** (default on — closes an automation tab when its process exits) and **Max concurrent sessions** (default 8, `0` = unlimited) to guard against runaway spawns. Removed the obsolete "Auto-restart on runtime switch" toggle (the sidebar runtime dropdown is replaced by the tab bar).
- Extracted pure helpers to `session-utils.ts` (`resolveRuntimeForAutomation`, `runtimeMatches`, `nextSessionLabel`, `canOpenSession`) with unit tests; the **Run now** button in the Automations modal is always enabled.
- Updated README and the generated example automation to document the tabbed/parallel model. Aligned `manifest.json`, `versions.json`, and `package.json` to 0.2.9.
## 0.2.8 - 2026-05-20
- **First release shipping the Automations feature.** Merged `feature/automation` into `main`: schedule reusable prompts from a vault folder (per-file `interval`/`cron` frontmatter), fire them into the running CLI automatically or manually from the Automations modal, with a per-run history (capped ring buffer) you can clear or export to a date-stamped markdown file. Includes the "Create example automation" settings button that writes a fully documented hello-world file.
- Fixes folded into this release: send `\r` (not `\n`) so prompts submit; split body/Enter into two writes so Codex submits too; export creates a fresh date-and-time-stamped file each time.

View file

@ -0,0 +1,41 @@
# Tâche : faire tourner plusieurs runtimes en parallèle (sessions à onglets)
Date : 2026-05-20 09:57:05
Slug : parallel-runtimes-tabs
## Objectif
Permettre l'exécution de plusieurs runtimes simultanément via une UX à onglets dans le
panneau latéral. Un onglet = une session indépendante (process PTY + terminal xterm propres).
## Décisions validées avec l'utilisateur
- UX : onglets dans un seul panneau, bouton « + » pour lancer une session.
- Plusieurs sessions du même runtime autorisées.
- Automatisation : ouvre TOUJOURS un nouvel onglet avec le runtime déclaré (ou runtime par
défaut si non déclaré), puis envoie le prompt à cette session.
## Plan d'exécution
1. `session-utils.ts` (fonctions pures) + `tests/session-utils.test.ts` + lint glob.
2. Champs settings `autoCloseAutomationSessions`, `maxConcurrentSessions` + sanitizing.
3. Classe `CliSession` + helper `createSessionTerminal()`.
4. Refactor `ClaudeCliView` : sessions[], startSession/closeSession/setActiveSession + onData/onExit/Codex par session.
5. Reconstruire `onOpen()` : barre d'onglets, menu « + », hôte terminaux, recâblage boutons, autoStart -> 1 session.
6. setActiveSession fit/resize à l'activation + observer unique.
7. Auto-close sessions automatisation + plafond maxConcurrentSessions.
8. `activateViewAndStartSession` + réécriture `triggerAutomation`.
9. `automations-modal.ts` : « Run now » toujours actif.
10. Lignes UI settings + CSS onglets.
## Statut
Terminé (implémentation). Test manuel Obsidian restant côté utilisateur.
## Review
- Implémentation conforme au plan validé : sessions à onglets, plusieurs sessions par
runtime, automatisation -> nouvel onglet (runtime déclaré ou défaut), auto-close + plafond.
- Fonctions de décision isolées dans `session-utils.ts` et couvertes par 15 tests unitaires.
- Pièges xterm gérés : un seul ResizeObserver ciblant la session visible, refit/resize à
l'activation d'onglet (pas de fit sur terminal `display:none`), reset + NO_COLOR Codex par session.
- Validation : lint OK, 70 tests OK, build esbuild OK, `tsc --noEmit` propre sur le code projet.
- Version 0.2.8 -> 0.2.9 (manifest, versions.json, package.json, version.md alignés).
- Limites / suivi : le champ `autoRestartOnRuntimeSwitch` est conservé dans les settings pour
éviter une migration mais n'a plus d'effet (nettoyage possible plus tard). Smoke test manuel
dans Obsidian (ouverture multi-onglets, bascule, fermeture, automatisation) à réaliser.

View file

@ -1 +1 @@
0.2.8
0.2.9

View file

@ -4,16 +4,17 @@
Run any local AI coding CLI — Claude, Codex, or your own — inside a right sidebar terminal in Obsidian.
The plugin embeds a real PTY-backed terminal in the sidebar and lets you declare an unlimited list of CLI runtimes from settings (each with a display name and a launch command). Pick one from a dropdown to start it in your active vault folder, switch between them on the fly, and inject the active file or folder as a mention with one click. You can also schedule reusable prompts: drop markdown files in a vault folder and the plugin fires them into the running CLI on an interval or cron — or run them on demand from the Automations panel, with a per-run history you can export.
The plugin embeds a real PTY-backed terminal in the sidebar and lets you declare an unlimited list of CLI runtimes from settings (each with a display name and a launch command). Open as many sessions as you like — each runs in its own tab with its own process, so several runtimes (or several instances of the same one) run in parallel in a single panel. Launch sessions in your active vault folder, switch tabs on the fly, and inject the active file or folder as a mention with one click. You can also schedule reusable prompts: drop markdown files in a vault folder and the plugin fires each one into its own fresh session tab on an interval or cron — or run them on demand from the Automations panel, with a per-run history you can export.
## Features
- Dedicated sidebar view with an embedded `xterm` terminal.
- **Customizable runtime list** — declare any number of CLI runtimes from settings (Claude and Codex are pre-populated; add Aider, custom wrappers, anything on `PATH`) and switch between them via a sidebar dropdown.
- One-click `@active file` and `@active folder` buttons that insert the current note path (or its parent folder) as a mention in the running CLI's stdin.
- **Automations** — drop markdown files (prompt + frontmatter schedule) in a vault folder and have them fired into the running CLI on an `interval` or `cron`, or run them manually from a modal with a per-run history log.
- Process controls in the toolbar: `Start`, `Stop`, `Restart`, `Clear`.
- Launches the selected runtime in the **current active vault folder** so the AI sees your notes as the working tree.
- **Parallel sessions in tabs** — run multiple runtimes (or multiple instances of the same one) side by side, each in its own tab with its own process. Switch tabs to bring a session to the front; background sessions keep running. A `+` control opens a new session from any configured runtime.
- **Customizable runtime list** — declare any number of CLI runtimes from settings (Claude and Codex are pre-populated; add Aider, custom wrappers, anything on `PATH`).
- One-click `@active file` and `@active folder` buttons that insert the current note path (or its parent folder) as a mention in the active session's stdin.
- **Automations** — drop markdown files (prompt + frontmatter schedule) in a vault folder and have each fired into its own new session tab on an `interval` or `cron`, or run them manually from a modal with a per-run history log.
- Process controls in the toolbar act on the active tab: `New session`, `Stop`, `Restart`, `Clear`.
- Launches each session in the **current active vault folder** so the AI sees your notes as the working tree.
- Resilient PTY stack with multi-tier fallbacks (`node-pty` → Python PTY bridge → direct pipe → `script`) so it works on macOS, Linux, and Windows.
- Visible runtime status (`Status: ...`) and clear error reporting in the panel.
@ -83,17 +84,17 @@ The community-store auto-install and the release zip both ship only the three ca
## Usage
1. Click the bot ribbon icon, or run the command palette entry **`Open panel`**, to reveal the panel on the right.
2. Use the runtime dropdown on the first toolbar row to pick which CLI to launch (Claude, Codex, or any custom entry you added).
3. Click `Start` to launch the selected runtime in the active vault folder.
4. While the CLI is running:
2. Click `New session` (or the `+` at the end of the tab bar) and pick a runtime to launch it in the active vault folder. With auto-start enabled, the default runtime opens automatically as the first session.
3. Open more sessions the same way — each gets its own tab and process, so runtimes run in parallel. Tabs of the same runtime are disambiguated (`Claude`, `Claude (2)`, …).
4. Click a tab to bring its session to the front; the `×` on a tab closes that session (and kills its process). A status dot shows whether each session is running.
5. The toolbar acts on the active tab:
- Click `@active file` or `@active folder` (second toolbar row) to insert the current note path or its parent folder as a mention.
- Click `Restart` to relaunch, `Stop` to terminate, `Clear` to wipe the terminal output.
5. Switching the dropdown to another runtime while a process is running automatically restarts it on the new CLI (configurable in settings).
- Click `Restart` to relaunch the active session's runtime in place, `Stop` to terminate it, `Clear` to wipe its terminal output.
6. Click `Automations` (second toolbar row) to open the Automations modal: run any prompt manually with **Run now**, or browse the **History** tab to see what fired and when.
## Automations
Automations let you store reusable prompts as markdown files in your vault and have the plugin send them to the running CLI either on a schedule (`interval` or `cron`) or manually from the **Automations** modal.
Automations let you store reusable prompts as markdown files in your vault. When an automation fires (on a schedule or manually), the plugin opens a **new session tab** for the target runtime and sends the prompt to it — so scheduled runs execute in parallel without disturbing your other sessions.
### Setup
@ -113,7 +114,7 @@ name: Daily summary # optional, defaults to the filename
enabled: true # optional, defaults to true
interval: 60 # minutes — exclusive with `cron`
# cron: "0 9 * * 1-5" # standard 5-field cron — exclusive with `interval`
runtime: claude # optional — only fire if this runtime id or display name is running
runtime: claude # optional — runtime id or display name to spawn; omit to use the default runtime
appendNewline: true # optional, defaults to true (adds Enter so the CLI executes the prompt)
---
@ -123,27 +124,29 @@ Summarize my notes from the last 24h and propose three priorities for today.
Rules:
- Exactly one of `interval` or `cron` must be set. `interval` is in whole minutes (>= 1). `cron` uses standard 5-field syntax (`cron-parser`).
- `enabled: false` keeps the entry visible in the modal but skips scheduling and disables the **Run now** button.
- `enabled: false` keeps the entry visible in the modal but skips scheduling (you can still trigger it with **Run now**).
- The prompt is everything after the closing `---` (trimmed).
- If the configured `runtime` does not match the currently running CLI, the run is skipped and logged in History.
- If no CLI is running when a scheduled run is due, the run is skipped (the plugin never auto-starts a CLI for you).
- The `runtime` field selects which runtime to spawn, matched by id or display name. If it names a runtime that is not configured, the run is skipped and logged in History.
- If `runtime` is omitted, the automation spawns the **default runtime** (set in plugin settings).
- Each run opens its own session tab. With **Auto-close automation sessions** enabled (default), the tab closes when the process exits so tabs don't accumulate.
### Manual runs and history
The **Automations** toolbar button opens a modal with two tabs:
- **Automations** — list of parsed entries with schedule, last run, next run, status badge, and a **Run now** button per row (disabled when no CLI is running). Parse errors are shown at the top with file paths and reasons.
- **Automations** — list of parsed entries with schedule, last run, next run, status badge, and a **Run now** button per row (always enabled — it opens a new session tab and sends the prompt). Parse errors are shown at the top with file paths and reasons.
- **History** — chronological log of every fired run (or skip / error), capped at 200 entries by default. You can **Clear history** or **Export as markdown** to create a snapshot note in the vault.
## Plugin Settings
General:
- **Default runtime** — which configured runtime is selected when the panel opens (and used by auto-start).
- **Auto-start** — start the default runtime automatically when the panel opens.
- **Auto-restart on runtime switch** — when you change the runtime from the sidebar dropdown while a process is running, restart it immediately to apply the new selection.
- **Default runtime** — which configured runtime opens as the first session on auto-start, and which automations use when they declare no `runtime`.
- **Auto-start** — open the default runtime as a session automatically when the panel opens.
- **Auto-close automation sessions** — when an automation-spawned session's process exits, close its tab automatically so tabs don't pile up (default on).
- **Max concurrent sessions** — cap the number of session tabs that can run at once (`0` = unlimited). Protects against runaway automation spawns.
Runtimes section (the customizable list of CLIs shown in the sidebar dropdown):
Runtimes section (the customizable list of CLIs available from the new-session menu):
- Each entry holds a display name and a launch command. Examples:
- `Claude``claude`

View file

@ -114,8 +114,6 @@ export class AutomationsModal extends Modal {
return;
}
const runnable = this.isAnyViewRunning();
const table = host.createEl("table", { cls: "any-ai-cli-am-table" });
const thead = table.createEl("thead").createEl("tr");
for (const label of ["Name", "Schedule", "Last run", "Next run", "Status", "Actions"]) {
@ -150,10 +148,7 @@ export class AutomationsModal extends Modal {
const actionsCell = tr.createEl("td", { cls: "any-ai-cli-am-actions" });
const runBtn = actionsCell.createEl("button", { text: "Run now" });
runBtn.disabled = !runnable;
if (!runnable) {
runBtn.setAttribute("title", "Start a CLI in the sidebar panel first.");
}
runBtn.setAttribute("title", "Open a new session tab and send this prompt.");
runBtn.addEventListener("click", () => {
void this.plugin.triggerAutomation(entry, "manual");
});
@ -213,14 +208,6 @@ export class AutomationsModal extends Modal {
}
}
private isAnyViewRunning(): boolean {
const leaves = this.app.workspace.getLeavesOfType("claude-cli-view");
return leaves.some((leaf) => {
const view = leaf.view as unknown as { isProcessRunning?: () => boolean };
return typeof view?.isProcessRunning === "function" && view.isProcessRunning();
});
}
private async exportHistory(history: AutomationRunRecord[]): Promise<void> {
const now = new Date();
const lines: string[] = [];

944
main.js

File diff suppressed because it is too large Load diff

1036
main.ts

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"id": "any-ai-cli",
"name": "Any AI CLI",
"version": "0.2.8",
"version": "0.2.9",
"minAppVersion": "1.7.2",
"description": "Run an AI assistant CLI like Claude from a right sidebar terminal panel, with a customizable runtime list.",
"author": "Benoit Lamouche",

View file

@ -1,12 +1,12 @@
{
"name": "any-ai-cli",
"version": "0.1.47",
"version": "0.2.9",
"description": "Run an AI assistant CLI like Claude from a right sidebar terminal panel, with a customizable runtime list.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "node esbuild.config.mjs production",
"lint": "eslint main.ts runtime-utils.ts \"tests/**/*.ts\"",
"lint": "eslint main.ts runtime-utils.ts session-utils.ts \"tests/**/*.ts\"",
"test": "vitest run",
"test:watch": "vitest"
},

64
session-utils.ts Normal file
View file

@ -0,0 +1,64 @@
import type { CliRuntimeConfig } from "./runtime-utils";
/**
* True when `target` (an automation's declared runtime) refers to `runtime`,
* matched either by id or by display name (case-insensitive). Mirrors the rule
* the sidebar view used before sessions existed.
*/
export function runtimeMatches(runtime: CliRuntimeConfig, target: string): boolean {
const wanted = target.trim().toLowerCase();
if (!wanted) {
return false;
}
if (runtime.id.trim().toLowerCase() === wanted) {
return true;
}
return runtime.name.trim().toLowerCase() === wanted;
}
/**
* Resolve which runtime an automation should spawn.
* - If `declared` is set, find the runtime matching it (id or name); returns
* `null` when none matches (the automation is then skipped).
* - If `declared` is empty/null, fall back to the default runtime
* (`defaultId`), then to the first configured runtime.
*/
export function resolveRuntimeForAutomation(
runtimes: CliRuntimeConfig[],
declared: string | null | undefined,
defaultId: string
): CliRuntimeConfig | null {
const wanted = (declared ?? "").trim();
if (wanted) {
return runtimes.find((runtime) => runtimeMatches(runtime, wanted)) ?? null;
}
return runtimes.find((runtime) => runtime.id === defaultId) ?? runtimes[0] ?? null;
}
/**
* Build a tab label for a new session, disambiguating duplicates of the same
* runtime: "Claude", "Claude (2)", "Claude (3)", ...
*/
export function nextSessionLabel(existingLabels: string[], runtimeName: string): string {
const base = runtimeName.trim() || "(Unnamed)";
const taken = new Set(existingLabels);
if (!taken.has(base)) {
return base;
}
let counter = 2;
while (taken.has(`${base} (${counter})`)) {
counter += 1;
}
return `${base} (${counter})`;
}
/**
* Whether another session may be opened given the current count and the
* configured cap. `max <= 0` means unlimited.
*/
export function canOpenSession(currentCount: number, max: number): boolean {
if (!Number.isFinite(max) || max <= 0) {
return true;
}
return currentCount < max;
}

View file

@ -249,6 +249,127 @@
font-size: 12px;
}
/* ----- Session tab bar ----- */
.claude-cli-tabbar {
display: flex;
align-items: center;
gap: 4px;
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: thin;
}
.claude-cli-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
border: 1px solid var(--background-modifier-border);
border-radius: 6px 6px 0 0;
background: var(--background-secondary);
color: var(--text-muted);
font-size: 12px;
cursor: pointer;
white-space: nowrap;
max-width: 200px;
}
.claude-cli-tab:hover {
background: var(--interactive-hover);
color: var(--text-normal);
}
.claude-cli-tab.is-active {
background: var(--background-primary);
color: var(--text-normal);
border-bottom-color: var(--interactive-accent);
}
.claude-cli-tab-label {
overflow: hidden;
text-overflow: ellipsis;
}
.claude-cli-tab-dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex: 0 0 auto;
}
.claude-cli-tab-dot.is-running {
background: var(--color-green, #3fb950);
}
.claude-cli-tab-dot.is-stopped {
background: var(--text-faint);
}
.claude-cli-tab-auto {
display: inline-flex;
align-items: center;
opacity: 0.7;
}
.claude-cli-tab-auto svg {
width: 12px;
height: 12px;
}
.claude-cli-tab-close {
display: inline-flex;
align-items: center;
border-radius: 4px;
opacity: 0.6;
}
.claude-cli-tab-close:hover {
opacity: 1;
background: var(--background-modifier-hover);
}
.claude-cli-tab-close svg {
width: 13px;
height: 13px;
}
.claude-cli-tab-new {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
flex: 0 0 auto;
}
.claude-cli-tab-new:hover {
background: var(--interactive-hover);
color: var(--text-normal);
}
/* ----- Terminals container ----- */
.claude-cli-terminals {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
position: relative;
}
.claude-cli-empty-hint {
color: var(--text-muted);
font-size: 13px;
padding: 16px;
text-align: center;
}
.claude-cli-empty-hint.is-hidden {
display: none;
}
.claude-cli-terminal {
flex: 1;
min-height: 220px;
@ -257,6 +378,10 @@
overflow: hidden;
}
.claude-cli-terminal.is-hidden {
display: none;
}
.claude-cli-terminal .xterm {
position: relative;
height: 100%;

View file

@ -0,0 +1,86 @@
import { describe, expect, it } from "vitest";
import {
canOpenSession,
nextSessionLabel,
resolveRuntimeForAutomation,
runtimeMatches
} from "../session-utils";
import type { CliRuntimeConfig } from "../runtime-utils";
const runtimes: CliRuntimeConfig[] = [
{ id: "claude", name: "Claude", command: "claude" },
{ id: "codex", name: "Codex", command: "codex" },
{ id: "abc123", name: "My Gemini", command: "gemini" }
];
describe("runtimeMatches", () => {
it("matches by id", () => {
expect(runtimeMatches(runtimes[0], "claude")).toBe(true);
});
it("matches by name case-insensitively", () => {
expect(runtimeMatches(runtimes[2], "my gemini")).toBe(true);
});
it("returns false on no match or empty target", () => {
expect(runtimeMatches(runtimes[0], "codex")).toBe(false);
expect(runtimeMatches(runtimes[0], " ")).toBe(false);
});
});
describe("resolveRuntimeForAutomation", () => {
it("resolves a declared runtime by name", () => {
expect(resolveRuntimeForAutomation(runtimes, "Codex", "claude")?.id).toBe("codex");
});
it("resolves a declared runtime by id", () => {
expect(resolveRuntimeForAutomation(runtimes, "abc123", "claude")?.id).toBe("abc123");
});
it("returns null when a declared runtime is unknown", () => {
expect(resolveRuntimeForAutomation(runtimes, "Nope", "claude")).toBeNull();
});
it("falls back to the default runtime when none is declared", () => {
expect(resolveRuntimeForAutomation(runtimes, null, "codex")?.id).toBe("codex");
expect(resolveRuntimeForAutomation(runtimes, "", "abc123")?.id).toBe("abc123");
});
it("falls back to the first runtime when the default id is invalid", () => {
expect(resolveRuntimeForAutomation(runtimes, null, "missing")?.id).toBe("claude");
});
it("returns null when there are no runtimes", () => {
expect(resolveRuntimeForAutomation([], null, "claude")).toBeNull();
});
});
describe("nextSessionLabel", () => {
it("uses the runtime name when free", () => {
expect(nextSessionLabel([], "Claude")).toBe("Claude");
});
it("disambiguates duplicates", () => {
expect(nextSessionLabel(["Claude"], "Claude")).toBe("Claude (2)");
expect(nextSessionLabel(["Claude", "Claude (2)"], "Claude")).toBe("Claude (3)");
});
it("falls back to (Unnamed) for blank names", () => {
expect(nextSessionLabel([], " ")).toBe("(Unnamed)");
});
});
describe("canOpenSession", () => {
it("allows when under the cap", () => {
expect(canOpenSession(2, 8)).toBe(true);
});
it("blocks at the cap", () => {
expect(canOpenSession(8, 8)).toBe(false);
});
it("treats 0 or negative as unlimited", () => {
expect(canOpenSession(100, 0)).toBe(true);
expect(canOpenSession(100, -1)).toBe(true);
});
});

View file

@ -18,5 +18,6 @@
"0.1.45": "1.7.2",
"0.1.46": "1.7.2",
"0.1.47": "1.7.2",
"0.2.8": "1.7.2"
"0.2.8": "1.7.2",
"0.2.9": "1.7.2"
}