Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
a8e789a
feat(btw): retain native side sessions
MoerAI Aug 23, 2026
cb493d7
fix(btw): enforce retained side runtime policy
MoerAI Aug 23, 2026
66be6cc
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 23, 2026
8735d2b
fix(btw): route tool isolation through session API
MoerAI Aug 23, 2026
549f8ce
fix(btw): enforce tool policy at provider boundary
MoerAI Aug 23, 2026
3a947d9
fix(btw): preserve parent branch context
MoerAI Aug 23, 2026
d537d31
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 23, 2026
ac875f4
fix(btw): enforce policy on prepared requests
MoerAI Aug 23, 2026
5419584
fix(btw): persist initialized retained sessions
MoerAI Aug 23, 2026
cf9cc37
fix(btw): require persisted Main session
MoerAI Aug 23, 2026
bd7ef4d
fix(btw): preflight side isolation state
MoerAI Aug 23, 2026
661518b
fix(btw): guard extension editor dialogs
MoerAI Aug 23, 2026
396ad9e
fix(btw): validate parent session identity
MoerAI Aug 23, 2026
dc08ae8
fix(btw): restore captured Main leaf
MoerAI Aug 23, 2026
2e45079
fix(btw): harden picker and parent identity
MoerAI Aug 23, 2026
d3e2086
fix(btw): serialize picker dispatch
MoerAI Aug 23, 2026
2d6f797
fix(btw): bound catalog metadata reads
MoerAI Aug 23, 2026
a63dec6
fix(btw): preflight live provider auth
MoerAI Aug 23, 2026
a6f0e92
fix(btw): use metadata-only catalog listing
MoerAI Aug 23, 2026
619613c
docs(core): cover retained BTW host changes
MoerAI Aug 23, 2026
4373455
fix(btw): seed external session metadata
MoerAI Aug 23, 2026
eab6a02
fix(btw): preserve configured session storage
MoerAI Aug 23, 2026
174ff94
fix(btw): suppress unavailable tool prompts
MoerAI Aug 23, 2026
b06be7c
fix(btw): revalidate settled parent identity
MoerAI Aug 23, 2026
c852fc9
fix(btw): suppress MCP instructions
MoerAI Aug 23, 2026
175befc
fix(btw): serialize destructive close
MoerAI Aug 23, 2026
44f1770
fix(btw): serialize session actions
MoerAI Aug 23, 2026
ab41efb
fix(btw): serialize Main return
MoerAI Aug 23, 2026
37a4b00
fix(btw): suppress skill guidance
MoerAI Aug 23, 2026
912b537
fix(btw): resolve shortcut command collisions
MoerAI Aug 23, 2026
88d3ef6
fix(btw): suppress tool prompt presets
MoerAI Aug 23, 2026
b5faeed
fix(btw): revalidate active Main for New
MoerAI Aug 23, 2026
3cacd49
fix(btw): bound recovered catalog scans
MoerAI Aug 23, 2026
a99fac8
fix(btw): revalidate Main after auth
MoerAI Aug 24, 2026
dc03fb7
fix(btw): preserve action reservation
MoerAI Aug 24, 2026
6ecb31e
fix(btw): reject stale source after auth
MoerAI Aug 24, 2026
7080b3a
fix(btw): defer parent snapshot
MoerAI Aug 24, 2026
84480c8
fix(btw): wait before picker switch
MoerAI Aug 24, 2026
d6c1563
fix(btw): revalidate switch target
MoerAI Aug 24, 2026
789b58d
fix(btw): guard direct session returns
MoerAI Aug 24, 2026
c9ff2b8
fix(btw): bind session identity through teardown
MoerAI Aug 24, 2026
7fd8913
fix(btw): recover cancelled replacements
MoerAI Aug 24, 2026
be6eb5b
fix(btw): preserve recovery identity
MoerAI Aug 24, 2026
8cbba14
fix(btw): close discarded runtimes
MoerAI Aug 24, 2026
9d59c72
fix(btw): guard post-veto source state
MoerAI Aug 24, 2026
a6e18a8
fix(btw): preserve streaming close
MoerAI Aug 24, 2026
09fb31a
fix(btw): track streaming source generation
MoerAI Aug 24, 2026
81530c1
fix(btw): revalidate after host rebind
MoerAI Aug 24, 2026
696cde6
fix(btw): lock replacement callbacks
MoerAI Aug 24, 2026
ef9ba6c
fix(btw): harden replacement rollback
MoerAI Aug 24, 2026
76325cc
fix(btw): scope replacement privilege
MoerAI Aug 24, 2026
0d6651e
fix(btw): close replacement admission gaps
MoerAI Aug 24, 2026
64077ee
fix(btw): release initial turn close gate
MoerAI Aug 24, 2026
ff0c5cb
fix(btw): bind admission and deletion identity
MoerAI Aug 24, 2026
5cdc370
fix(btw): rollback rejected shortcut claims
MoerAI Aug 24, 2026
1b6e29d
fix(btw): cancel deferred shortcut claims
MoerAI Aug 24, 2026
046f4d3
fix(btw): clear deferred claims before rollback
MoerAI Aug 24, 2026
9238f91
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 24, 2026
29f0690
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 24, 2026
ad4ff63
fix(btw): recover failed retained persistence
MoerAI Aug 24, 2026
72310e7
fix(btw): claim retained persistence files
MoerAI Aug 24, 2026
67ea1af
fix(btw): quarantine failed persistence cleanup
MoerAI Aug 24, 2026
c199960
fix(btw): bind persistence to claimed inode
MoerAI Aug 24, 2026
0316d3c
fix(btw): detach failed candidate persistence
MoerAI Aug 24, 2026
a99ca35
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 24, 2026
3fc81d1
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 24, 2026
22ca1ce
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 25, 2026
8c4c07b
fix(bash-timeout): tolerate missing prompt context
MoerAI Aug 25, 2026
6168d8e
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 25, 2026
2028cb8
test(rpc): model replacement lifecycle in registry fixture
MoerAI Aug 25, 2026
8f4770a
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 25, 2026
62778e3
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 25, 2026
1e5962a
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 25, 2026
ce9a799
Merge remote-tracking branch 'origin/main' into feat/btw-retained-ses…
MoerAI Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

### Added

- Retained multi-session `/btw` TUI with native transcripts, a Main/numbered-side/New picker,
configurable `Ctrl+/`, `Ctrl+_`, and `Ctrl+7` switching, idle Escape-Escape return, and
current-side Ctrl+C deletion; non-TUI side queries remain one-shot and parallel.

### Changed

### Removed
Expand Down
34 changes: 34 additions & 0 deletions packages/coding-agent/src/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,39 @@
# changes

## 2026-08-23 - retained BTW sessions and configurable switch routing
Comment thread
MoerAI marked this conversation as resolved.

### What changed

- The builtin BTW TUI path creates separate retained host sessions and uses native session
selection/transcripts instead of the one-shot widget.
- The fresh replacement context applies a retained side's captured model and thinking level before
its first turn. A typed `newSession` policy capability persists the tool ban without crossing the
extension/core boundary, forces later extension startup to keep the active set empty, and blocks
Cursor exec registered-tool lookup. Final payload enforcement strips provider-native tools after
every extension transform.
- Main-side context snapshots follow Main's selected tree leaf rather than the file's physical
tail; retained siblings inherit that persisted leaf, and an oversized newest message is
truncated instead of emptying the snapshot. Creation waits for Main to settle before reloading
the catalog and snapshotting.
- Session tool policy is enforced at `ExtensionRunner`'s final payload boundary, covering normal,
compaction, and cache-keepalive provider requests after all extension transforms.
- Extension command contexts expose typed session listing/inspection. `newSession` can opt into
flushing initialized metadata before returning, which keeps empty retained sides durable without
crossing the extension/core boundary.
- `app.btw.switch` adds Ctrl+/, Ctrl+_, and Ctrl+7 defaults.
- Interactive extension UI exposes active key matching and effective-key lookup for configurable
terminal routing and visible fallback hints.

### Why

- Side conversations need durable identity, host scrolling, reliable return/close behavior, and a
fallback when an editor such as Zed reserves Ctrl+/ before the terminal receives it.

### Expected merge-conflict zones

- MEDIUM: builtin BTW wiring and keybinding definitions.
- LOW: extension UI context and interactive implementation.

## 2026-08-22 - emit agent_idle after settlement-deferred turns resolve

### What changed
Expand Down
10 changes: 10 additions & 0 deletions packages/coding-agent/src/core/agent-session-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import type {
ReplacedSessionContext,
SessionShutdownEvent,
SessionStartEvent,
SessionToolPolicy,
} from "./extensions/index.ts";
import { type ExtensionRunner, emitSessionShutdownEvent } from "./extensions/runner.ts";
import type { CreateAgentSessionResult } from "./sdk.ts";
import { assertSessionCwdExists } from "./session-cwd.ts";
import { SessionManager } from "./session-manager.ts";
import { SESSION_TOOL_POLICY_ENTRY_TYPE } from "./session-tool-policy.ts";

/**
* Result returned by runtime creation.
Expand Down Expand Up @@ -270,6 +272,8 @@ export class AgentSessionRuntime {

async newSession(options?: {
parentSession?: string;
persistInitializedSession?: boolean;
sessionToolPolicy?: SessionToolPolicy;
setup?: (sessionManager: SessionManager) => Promise<void>;
withSession?: (ctx: ReplacedSessionContext) => Promise<void>;
}): Promise<{ cancelled: boolean }> {
Expand All @@ -286,6 +290,9 @@ export class AgentSessionRuntime {
if (options?.parentSession) {
sessionManager.newSession({ parentSession: options.parentSession });
}
if (options?.sessionToolPolicy) {
sessionManager.appendCustomEntry(SESSION_TOOL_POLICY_ENTRY_TYPE, options.sessionToolPolicy);
}

await this.teardownCurrent("new", sessionManager.getSessionFile());
await this.apply(
Expand All @@ -301,6 +308,9 @@ export class AgentSessionRuntime {
await options.setup(this.session.sessionManager);
this.session.agent.state.messages = this.session.sessionManager.buildSessionContext().messages;
}
if (options?.persistInitializedSession) {
this.session.sessionManager.persistInitializedSession();
Comment thread
MoerAI marked this conversation as resolved.
Outdated
Comment thread
MoerAI marked this conversation as resolved.
Outdated
}
await this.finishSessionReplacement(options?.withSession);
return { cancelled: false };
}
Expand Down
14 changes: 13 additions & 1 deletion packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ import {
type SessionHeader,
} from "./session-manager.ts";
import { generateSessionTitle, sessionTitleRetryPolicy, shouldSkipSessionTitle } from "./session-title-generator.ts";
import { isSessionToolUseDisabled } from "./session-tool-policy.ts";
import { SessionWorkBarrier } from "./session-work-barrier.ts";
import type { SettingsManager, SettingsSourceSelection } from "./settings-manager.ts";
import type { SlashCommandInfo } from "./slash-commands.ts";
Expand Down Expand Up @@ -2830,6 +2831,9 @@ export class AgentSession {
* channel regardless of which tools the request advertised.
*/
getRegisteredTool(name: string): AgentTool | undefined {
if (isSessionToolUseDisabled(this.sessionManager.getEntries())) {
return undefined;
}
return this._toolRegistry.get(name);
}

Expand All @@ -2856,9 +2860,10 @@ export class AgentSession {
}

setActiveToolsByName(toolNames: string[]): void {
const effectiveToolNames = isSessionToolUseDisabled(this.sessionManager.getEntries()) ? [] : toolNames;
Comment thread
MoerAI marked this conversation as resolved.
Comment thread
MoerAI marked this conversation as resolved.
Comment thread
MoerAI marked this conversation as resolved.
const tools: AgentTool[] = [];
const validToolNames: string[] = [];
for (const name of toolNames) {
for (const name of effectiveToolNames) {
const tool = this._toolRegistry.get(name);
if (tool) {
tools.push(tool);
Expand Down Expand Up @@ -7988,6 +7993,13 @@ export class AgentSession {
) as ReplacedSessionContext;
context.sendMessage = (message, options) => this.sendCustomMessage(message, options);
context.sendUserMessage = (content, options) => this.sendUserMessage(content, options);
context.setActiveTools = (toolNames) => this.setActiveToolsByName(toolNames);
context.setSessionModel = async (model) => {
if (!this._modelRuntime.hasConfiguredAuth(model.provider)) return false;
await this.setSessionModel(model);
return true;
};
context.setSessionThinkingLevel = (level) => this.setSessionThinkingLevel(level);
return context;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
| 31 | `loop` | `loop/` | Scheduled/cron loop runs driven by a loopfile + `/loop` command — see `loop/AGENTS.md` |
| 32 | `cache-keepalive` | `cache-keepalive/` | Warms the provider prompt cache between turns (`warmPromptCache`, Anthropic-aware TTL) and renders a `cache-keepalive` notice entry |
| 33 | `ttsr` | `ttsr/` | Stream-rule detection (collapse + control-token-leak) with abort→remediate→retry; ported from oh-my-pi — see `ttsr/changes.md` |
| 34 | `btw` | `btw/` | `/btw` side-question command that queries in parallel without touching the main session |
| 34 | `btw` | `btw/` | Retained TUI side sessions with native transcripts, a Main/BTW/New switcher, configurable Ctrl aliases, Escape return, and current-side close; non-TUI keeps the parallel one-shot query |
| 35 | `claude-sdk-oauth` | `claude-sdk-oauth/` | Claude SDK OAuth provider: multi-account OAuth, resume-first session continuity, stream-safe failover — see `claude-sdk-oauth/AGENTS.md` + `changes.md` |
| 36 | `cursor-cli-oauth` | `cursor-cli-oauth/` | Cursor CLI OAuth provider lane: multi-account OAuth, spawn/stream parsing, failover; registers unconditionally and reports executable/auth state through its oauth check — see `cursor-cli-oauth/AGENTS.md` |
| 37 | `config-reload` | `config-reload/` | Hash-gated watcher for trusted global/project config surfaces that defers a full session reload until idle and exposes the `config-watch:*` event protocol; registered after settings-dependent builtins so a reload rebuilds their resolved settings, and before final MCP observation |
Expand Down
44 changes: 44 additions & 0 deletions packages/coding-agent/src/core/extensions/builtin/btw/changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,49 @@
# changes — btw

## 2026-08-23 - Retained native TUI side sessions

### What changed

- In TUI mode, every `/btw <question>` creates a distinct host session with versioned root-parent
metadata, stable `BTW #N: <summary>` naming, a hidden bounded Main-context snapshot, no active
tools, and a native transcript.
- Bare `/btw` opens the native selector with Main, numbered retained sides, and New BTW. The popup
renders the effective switch keys and always shows `/btw`; defaults are Ctrl+/, Ctrl+_, and
Ctrl+7 so editors that reserve Ctrl+/ still have a terminal fallback.
- A configured switch input opens the same picker. Two idle interrupt keys inside one second return
to Main without deleting the side. The configured clear key switches to Main and then deletes
only the visible side.
- Catalog reads revalidate selected paths, skip stale/corrupt rows, recover root scope from side
metadata after reload, and keep failed deletions discoverable.
- The fresh replacement context applies the captured model and thinking level before the first
answer. The typed `newSession` policy capability persists a tool ban without crossing the
extension/core boundary; it forces later MCP/tool-search activation to an empty effective set
and rejects Cursor exec registered-tool lookup. Final provider payload enforcement removes
provider-native tool definitions after every extension transform.
- Parent snapshots use Main's active leaf when Main is visible, so a side never captures an
abandoned physical tail after tree navigation. The selected Main leaf is persisted in side
metadata and inherited by siblings created from a side.
- A newest message larger than the 64k snapshot budget is truncated into the snapshot instead of
dropping all parent context.
- Inline and New BTW creation wait for Main to settle, then reload the catalog before snapshotting
so the leaf and content describe the same completed turn.
- The typed `newSession` contract flushes initialized sides before returning, so an empty New BTW
remains in the disk catalog. Discovery and parent inspection use typed command-context
capabilities instead of importing core session internals.
- Non-TUI modes retain the existing parallel, read-only, one-shot provider query.

### Why

- One custom widget could not represent multiple retained side conversations, use the host
transcript scrollback, survive reload/navigation, or make destructive close unambiguous.

### Expected merge-conflict zones

- MEDIUM: `index.ts` command and session-start wiring.
- LOW: `session-catalog.ts`, `retained-session.ts`, `tui-command.ts`, `input-controls.ts`, and
`session-actions.ts` are extension-local additions.
- LOW: `core/keybindings.ts`, extension UI context types, and interactive UI context wiring.

## 2026-08-13 - Preserve provider-header deletion markers

### What changed
Expand Down
57 changes: 56 additions & 1 deletion packages/coding-agent/src/core/extensions/builtin/btw/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
import { convertToLlm, filterContextExcludedMessages } from "../../../messages.ts";
import { buildSessionContext } from "../../../session-manager.ts";
import type { ExtensionAPI, ExtensionContext } from "../../types.ts";
import { createBtwInputRouter } from "./input-controls.ts";
import { BtwPanel } from "./panel.ts";
import { applyBtwSideSessionPolicy } from "./retained-session.ts";
import {
closeRetainedBtwSide,
deleteBtwSessionFile,
readCurrentBtwSide,
returnToBtwParent,
} from "./session-actions.ts";
import { buildSideQueryContext, getSideQueryPromptContextWindow, runSideQuery } from "./side-query.ts";
import { defaultBtwTuiCommandDependencies, runBtwTuiCommand } from "./tui-command.ts";

const WIDGET_KEY = "btw";
const ESCAPE = "";
Expand Down Expand Up @@ -30,6 +39,20 @@ export default function btwExtension(pi: ExtensionAPI) {
dismiss(ctx, { abort: true });
});

pi.on("session_start", (_event, ctx) => {
applyBtwSideSessionPolicy(pi, ctx);
if (ctx.mode !== "tui" || !ctx.ui.matchesKeybinding) return;
const router = createBtwInputRouter({
isCurrentSide: () => readCurrentBtwSide(ctx.sessionManager) !== undefined,
isIdle: () => ctx.isIdle(),
matchesKeybinding: (data, keybinding) => ctx.ui.matchesKeybinding?.(data, keybinding) ?? false,
dispatch: (command) => {
pi.sendUserMessage(command, { expandPromptTemplates: true });
Comment thread
MoerAI marked this conversation as resolved.
Outdated
},
});
ctx.ui.onTerminalInput((data) => router.handleInput(data));
});

pi.on("session_before_fork", (_event, ctx) => {
dismiss(ctx, { abort: true });
});
Expand All @@ -42,10 +65,42 @@ export default function btwExtension(pi: ExtensionAPI) {
if (active?.settled) dismiss(ctx, { abort: false });
});

pi.registerCommand("btw-main", {
description: "Return from the current retained BTW session to Main",
handler: async (_args, ctx) => {
await returnToBtwParent({
Comment thread
MoerAI marked this conversation as resolved.
Outdated
ctx,
current: readCurrentBtwSide(ctx.sessionManager),
});
},
});

pi.registerCommand("btw-close", {
description: "Delete the current retained BTW session and return to Main",
handler: async (_args, ctx) => {
await closeRetainedBtwSide({
ctx,
current: readCurrentBtwSide(ctx.sessionManager),
deleteSessionFile: deleteBtwSessionFile,
});
},
});

pi.registerCommand("btw", {
description: "Ask a side question in parallel without touching the main session",
description: "Create or switch retained BTW side sessions",
argumentHint: "<question>",
handler: async (args, ctx) => {
if (ctx.mode === "tui" && ctx.hasUI) {
dismiss(ctx, { abort: true });
try {
await runBtwTuiCommand(args, ctx, defaultBtwTuiCommandDependencies);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
ctx.ui.notify(`/btw failed: ${message}`, "error");
}
return;
}

const question = args.trim();
if (!question) {
ctx.ui.notify("Usage: /btw <question>", "warning");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { AppKeybinding } from "../../../keybindings.ts";

export const BTW_SWITCH_KEYBINDING = "app.btw.switch" as const;

export interface BtwInputRouter {
handleInput(data: string): { consume?: boolean; data?: string } | undefined;
}

export function createBtwInputRouter(_input: {
isCurrentSide: () => boolean;
isIdle: () => boolean;
matchesKeybinding: (data: string, binding: AppKeybinding) => boolean;
dispatch: (command: string) => void;
now?: () => number;
}): BtwInputRouter {
const input = _input;
const now = input.now ?? Date.now;
let firstIdleInterruptAt: number | undefined;

function resetInterruptPair(): void {
firstIdleInterruptAt = undefined;
}

return {
handleInput(data) {
if (input.matchesKeybinding(data, BTW_SWITCH_KEYBINDING)) {
resetInterruptPair();
input.dispatch("/btw");
Comment thread
MoerAI marked this conversation as resolved.
Outdated
return { consume: true };
Comment thread
MoerAI marked this conversation as resolved.
}
if (!input.isCurrentSide()) {
resetInterruptPair();
return undefined;
}
if (input.matchesKeybinding(data, "app.clear")) {
resetInterruptPair();
input.dispatch("/btw-close");
Comment thread
MoerAI marked this conversation as resolved.
Outdated
return { consume: true };
Comment thread
MoerAI marked this conversation as resolved.
}
if (!input.matchesKeybinding(data, "app.interrupt")) {
resetInterruptPair();
return undefined;
}
if (!input.isIdle()) {
resetInterruptPair();
return undefined;
}
const pressedAt = now();
if (
firstIdleInterruptAt !== undefined &&
pressedAt >= firstIdleInterruptAt &&
pressedAt - firstIdleInterruptAt <= 1_000
Comment thread
MoerAI marked this conversation as resolved.
) {
resetInterruptPair();
input.dispatch("/btw-main");
return { consume: true };
}
firstIdleInterruptAt = pressedAt;
return undefined;
},
};
}
53 changes: 53 additions & 0 deletions packages/coding-agent/src/core/extensions/builtin/btw/picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import type { BtwSessionCatalog } from "./session-catalog.ts";

export type BtwPickerChoice = { type: "session"; sessionPath: string } | { type: "new"; parentSessionPath: string };

export interface BtwPickerOption {
label: string;
choice: BtwPickerChoice;
}

export async function validateBtwPickerChoice(
choice: BtwPickerChoice,
exists: (sessionPath: string) => Promise<boolean>,
): Promise<boolean> {
const sessionPath = choice.type === "session" ? choice.sessionPath : choice.parentSessionPath;
try {
return await exists(sessionPath);
Comment thread
MoerAI marked this conversation as resolved.
Outdated
} catch {
return false;
}
}

export function buildBtwPickerOptions(catalog: BtwSessionCatalog, currentSessionPath: string): BtwPickerOption[] {
const options: BtwPickerOption[] = [];
if (catalog.main) {
options.push({
label: `Main — ${catalog.main.name?.trim() || "Main session"}`,
choice: {
type: "session",
sessionPath: catalog.main.path,
},
});
}
for (const side of catalog.sides) {
const current = side.path === currentSessionPath ? " (current)" : "";
options.push({
label: `BTW #${side.metadata.ordinal} — ${side.metadata.summary}${current}`,
choice: {
type: "session",
sessionPath: side.path,
},
});
}
if (catalog.main) {
options.push({
label: "New BTW",
choice: {
type: "new",
parentSessionPath: catalog.parentSessionPath,
},
});
}
return options;
}
Loading