mirror of
https://github.com/blamouche/obsidian-any-ai-code.git
synced 2026-07-22 06:53:38 +00:00
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:
parent
459451b225
commit
073efef75c
14 changed files with 1631 additions and 751 deletions
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -1 +1 @@
|
|||
0.2.8
|
||||
0.2.9
|
||||
45
README.md
45
README.md
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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[] = [];
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
64
session-utils.ts
Normal 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;
|
||||
}
|
||||
125
styles.css
125
styles.css
|
|
@ -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%;
|
||||
|
|
|
|||
86
tests/session-utils.test.ts
Normal file
86
tests/session-utils.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue