@krtclcdy/tersio 2.24.0 → 2.24.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/README.md +1 -3
- package/cli/banner.js +2 -3
- package/cli/currency.js +3 -7
- package/cli/dashboard.js +167 -3
- package/cli/doctor-fix.js +15 -12
- package/cli/doctor.js +1 -12
- package/cli/hosts.js +7 -11
- package/cli/install.js +113 -71
- package/cli/interactive.js +36 -2
- package/cli/layer.js +1 -3
- package/cli/manifest.js +1 -4
- package/cli/profile.js +9 -7
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +1 -6
- package/cli/settings.js +3 -8
- package/cli/uninstall.js +43 -24
- package/cli/update.js +7 -8
- package/cli/usage.js +15 -5
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +6 -5
- package/extensions/ai-addons-updater/index.ts +7 -6
- package/extensions/caveman-session/index.js +14 -29
- package/extensions/caveman-session/index.ts +15 -30
- package/extensions/combo-toggle/index.js +10 -14
- package/extensions/combo-toggle/index.ts +10 -15
- package/extensions/lib/utils.js +4 -0
- package/extensions/lib/utils.ts +3 -0
- package/extensions/rtk-session/index.js +27 -31
- package/extensions/rtk-session/index.ts +25 -29
- package/extensions/shared/carbon.js +3 -8
- package/extensions/shared/carbon.ts +4 -8
- package/extensions/shared/host.js +1 -11
- package/extensions/shared/host.ts +1 -11
- package/extensions/shared/plugin-settings.js +4 -14
- package/extensions/shared/plugin-settings.ts +4 -14
- package/extensions/shared/pricing.js +4 -6
- package/extensions/shared/pricing.ts +4 -6
- package/extensions/shared/rtk-gain.js +2 -7
- package/extensions/shared/rtk-gain.ts +2 -7
- package/extensions/shared/session-state.js +30 -18
- package/extensions/shared/session-state.ts +33 -16
- package/extensions/shared/usage-ledger.js +138 -17
- package/extensions/shared/usage-ledger.ts +143 -19
- package/extensions/shared/usage-store.js +175 -23
- package/extensions/shared/usage-store.ts +171 -22
- package/extensions/tersio-commands/index.js +1 -5
- package/extensions/tersio-commands/index.ts +1 -5
- package/package.json +6 -7
- package/tersio.js +1 -3
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// Port of the EcoLogits v0.8.2 LLM impact model (genai-impact/ecologits,
|
|
4
|
-
// MPL-2.0 — see ECO_NOTICE.md): the single-stream (batch-1) ceiling divided by
|
|
5
|
-
// serving_concurrency for continuous-batching amortization. Model params come
|
|
6
|
-
// from EcoLogits' registry, else a documented class borrow; unknown ids fall
|
|
7
|
-
// to the gpt-4o default. Render everything from here as ~est., never merged
|
|
8
|
-
// with measured figures.
|
|
1
|
+
// EcoLogits v0.8.2 port; always render as ~est, never merged with measured.
|
|
9
2
|
|
|
10
3
|
export interface ModelCarbon {
|
|
11
4
|
activeB: number;
|
|
@@ -39,6 +32,9 @@ const GRIDS: Record<string, number> = {
|
|
|
39
32
|
// Serving amortization: single-stream ceiling ÷ concurrency ≈ served impact.
|
|
40
33
|
export const SERVING_CONCURRENCY = 32;
|
|
41
34
|
|
|
35
|
+
/** EcoLogits release this port tracks, so a UI can name it instead of guessing. */
|
|
36
|
+
export const ECOLOGITS_VERSION = '0.8.2';
|
|
37
|
+
|
|
42
38
|
interface RegistryEntry {
|
|
43
39
|
match?: string;
|
|
44
40
|
exact?: string;
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// OMP is a fork of pi, and six surfaces differ: tool schemas (OMP injects
|
|
4
|
-
// `pi.zod`, pi takes any object it forwards as JSON Schema), `setLabel`
|
|
5
|
-
// (pi's takes an entryId, so an extension name must never reach it),
|
|
6
|
-
// `session_branch` (pi reports that switch as `session_tree`), select options
|
|
7
|
-
// (pi takes plain strings), the status-bar theme (pi has no ctx.ui.theme), and
|
|
8
|
-
// prompt injection (pi appends to its sections rather than replacing them).
|
|
9
|
-
//
|
|
10
|
-
// The host is read from the API shape, never a name or an env marker: OMP is
|
|
11
|
-
// the fork that injects zod, and pi does not, including through the SDK.
|
|
1
|
+
// One source for both hosts; OMP differs in six APIs, detected by shape not name.
|
|
12
2
|
import { asPromptArray } from './session-state.js';
|
|
13
3
|
export function isPiHost(pi) {
|
|
14
4
|
return !pi.zod;
|
|
@@ -1,14 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// OMP is a fork of pi, and six surfaces differ: tool schemas (OMP injects
|
|
4
|
-
// `pi.zod`, pi takes any object it forwards as JSON Schema), `setLabel`
|
|
5
|
-
// (pi's takes an entryId, so an extension name must never reach it),
|
|
6
|
-
// `session_branch` (pi reports that switch as `session_tree`), select options
|
|
7
|
-
// (pi takes plain strings), the status-bar theme (pi has no ctx.ui.theme), and
|
|
8
|
-
// prompt injection (pi appends to its sections rather than replacing them).
|
|
9
|
-
//
|
|
10
|
-
// The host is read from the API shape, never a name or an env marker: OMP is
|
|
11
|
-
// the fork that injects zod, and pi does not, including through the SDK.
|
|
1
|
+
// One source for both hosts; OMP differs in six APIs, detected by shape not name.
|
|
12
2
|
|
|
13
3
|
import { asPromptArray } from './session-state.ts';
|
|
14
4
|
import type { ExtensionApi, ExtensionCtx, PromptInjection, SystemPromptEvent, ToolParams, UiApi } from './types.ts';
|
|
@@ -1,25 +1,15 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// One file, under Tersio's own home (~/.tersio/settings.json), serves every
|
|
4
|
-
// host: OMP has no reason to keep its own copy and pi never had a store. The
|
|
5
|
-
// profile CLI writes the same file, so the extensions, `tersio settings`, and
|
|
6
|
-
// both hosts always agree.
|
|
7
|
-
//
|
|
8
|
-
// Tolerant: any parse failure yields {} so every caller falls back to its own
|
|
9
|
-
// default.
|
|
1
|
+
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
10
2
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
11
3
|
import os from 'node:os';
|
|
12
4
|
import path from 'node:path';
|
|
13
5
|
import { tersioHome } from '../lib/utils.js';
|
|
14
6
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
15
|
-
//
|
|
16
|
-
// home, so OMP and pi read the same values. This module owns the path.
|
|
7
|
+
// The one store both hosts read; this module owns its path.
|
|
17
8
|
export function tersioSettingsFile() {
|
|
18
9
|
return path.join(tersioHome(), 'settings.json');
|
|
19
10
|
}
|
|
20
|
-
// Older installs
|
|
21
|
-
//
|
|
22
|
-
// switching the store does not reset a user's defaults.
|
|
11
|
+
// Older installs kept these as OMP plugin settings; still read so switching
|
|
12
|
+
// the store does not reset a user's defaults.
|
|
23
13
|
function legacyOmpSettings() {
|
|
24
14
|
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
25
15
|
for (const p of [
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// One file, under Tersio's own home (~/.tersio/settings.json), serves every
|
|
4
|
-
// host: OMP has no reason to keep its own copy and pi never had a store. The
|
|
5
|
-
// profile CLI writes the same file, so the extensions, `tersio settings`, and
|
|
6
|
-
// both hosts always agree.
|
|
7
|
-
//
|
|
8
|
-
// Tolerant: any parse failure yields {} so every caller falls back to its own
|
|
9
|
-
// default.
|
|
1
|
+
// One settings file for every host; parse failure yields {} with per-caller defaults.
|
|
10
2
|
|
|
11
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
12
4
|
import os from 'node:os';
|
|
@@ -15,15 +7,13 @@ import { tersioHome } from '../lib/utils.ts';
|
|
|
15
7
|
|
|
16
8
|
export const PLUGIN_NAME = '@krtclcdy/tersio';
|
|
17
9
|
|
|
18
|
-
//
|
|
19
|
-
// home, so OMP and pi read the same values. This module owns the path.
|
|
10
|
+
// The one store both hosts read; this module owns its path.
|
|
20
11
|
export function tersioSettingsFile(): string {
|
|
21
12
|
return path.join(tersioHome(), 'settings.json');
|
|
22
13
|
}
|
|
23
14
|
|
|
24
|
-
// Older installs
|
|
25
|
-
//
|
|
26
|
-
// switching the store does not reset a user's defaults.
|
|
15
|
+
// Older installs kept these as OMP plugin settings; still read so switching
|
|
16
|
+
// the store does not reset a user's defaults.
|
|
27
17
|
function legacyOmpSettings(): Record<string, unknown> | null {
|
|
28
18
|
const configHome = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');
|
|
29
19
|
for (const p of [
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM.
|
|
2
|
-
//
|
|
3
|
-
// the Dashboard labels an unpriced model honestly. No static table: prices rot.
|
|
1
|
+
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM. Exact id
|
|
2
|
+
// → provider-prefix strip → default, reported with known:false when unpriced.
|
|
4
3
|
import fs from 'node:fs';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { tersioDataPath } from '../lib/utils.js';
|
|
@@ -70,9 +69,8 @@ export function loadLivePrices() {
|
|
|
70
69
|
function num(v) {
|
|
71
70
|
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
72
71
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
// a free stealth model never reports the default price.
|
|
72
|
+
// Transcripts and the feed spell one model differently; aliases stop a free
|
|
73
|
+
// model reporting the default price.
|
|
76
74
|
export const MODEL_ALIASES = [
|
|
77
75
|
{ id: 'space-bunny', feed: 'stealth/space-bunny-alpha' },
|
|
78
76
|
];
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM.
|
|
2
|
-
//
|
|
3
|
-
// the Dashboard labels an unpriced model honestly. No static table: prices rot.
|
|
1
|
+
// extensions/shared/pricing.ts — dynamic model pricing from LiteLLM. Exact id
|
|
2
|
+
// → provider-prefix strip → default, reported with known:false when unpriced.
|
|
4
3
|
import fs from 'node:fs';
|
|
5
4
|
import path from 'node:path';
|
|
6
5
|
import { tersioDataPath } from '../lib/utils.ts';
|
|
@@ -88,9 +87,8 @@ function num(v: unknown): number | null {
|
|
|
88
87
|
return typeof v === 'number' && Number.isFinite(v) && v >= 0 ? v : null;
|
|
89
88
|
}
|
|
90
89
|
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
// a free stealth model never reports the default price.
|
|
90
|
+
// Transcripts and the feed spell one model differently; aliases stop a free
|
|
91
|
+
// model reporting the default price.
|
|
94
92
|
export const MODEL_ALIASES: ReadonlyArray<{ id: string; feed: string }> = [
|
|
95
93
|
{ id: 'space-bunny', feed: 'stealth/space-bunny-alpha' },
|
|
96
94
|
];
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// history.db with the sqlite3 CLI, so the dashboard shows measured figures and
|
|
3
|
-
// never estimates. Missing DB or CLI returns empty. Caveman and Ponytail are
|
|
4
|
-
// bench-measured in BENCHMARK.md instead: instruction-following is not metered.
|
|
1
|
+
// Savings read from rtk history, never estimated; modes bench in BENCHMARK.md.
|
|
5
2
|
import { execFileSync } from 'node:child_process';
|
|
6
3
|
import fs from 'node:fs';
|
|
7
4
|
import os from 'node:os';
|
|
@@ -10,9 +7,7 @@ const EMPTY = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0, byComman
|
|
|
10
7
|
// The table shows one clipped cell, so bound the command before it crosses the
|
|
11
8
|
// sqlite3 CLI and lands in every /data.json poll.
|
|
12
9
|
const MAX_CMD_CHARS = 200;
|
|
13
|
-
//
|
|
14
|
-
// several lines and derail every row after it. The stored command keeps its
|
|
15
|
-
// newlines; only the read view flattens.
|
|
10
|
+
// A multi-line stored command would derail every row after it; flatten on read.
|
|
16
11
|
const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13), ' '), char(9), ' ')`;
|
|
17
12
|
export function rtkDbPath() {
|
|
18
13
|
const override = process.env.TERSIO_RTK_DB;
|
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
//
|
|
2
|
-
// history.db with the sqlite3 CLI, so the dashboard shows measured figures and
|
|
3
|
-
// never estimates. Missing DB or CLI returns empty. Caveman and Ponytail are
|
|
4
|
-
// bench-measured in BENCHMARK.md instead: instruction-following is not metered.
|
|
1
|
+
// Savings read from rtk history, never estimated; modes bench in BENCHMARK.md.
|
|
5
2
|
import { execFileSync } from 'node:child_process';
|
|
6
3
|
import fs from 'node:fs';
|
|
7
4
|
import os from 'node:os';
|
|
@@ -30,9 +27,7 @@ const EMPTY: RtkGain = { commands: 0, saved: 0, input: 0, avgPct: 0, totalMs: 0,
|
|
|
30
27
|
// sqlite3 CLI and lands in every /data.json poll.
|
|
31
28
|
const MAX_CMD_CHARS = 200;
|
|
32
29
|
|
|
33
|
-
//
|
|
34
|
-
// several lines and derail every row after it. The stored command keeps its
|
|
35
|
-
// newlines; only the read view flattens.
|
|
30
|
+
// A multi-line stored command would derail every row after it; flatten on read.
|
|
36
31
|
const CMD_ONE_LINE = `replace(replace(replace(rtk_cmd, char(10), ' '), char(13), ' '), char(9), ' ')`;
|
|
37
32
|
|
|
38
33
|
export function rtkDbPath(): string {
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { readPluginSettings } from './plugin-settings.js';
|
|
1
2
|
const BRIDGE_KEY = Symbol.for('tersio/combo-session-state');
|
|
3
|
+
// A verbatim sentence from OMP's subagent prompt, so it can drift; settings.json overrides it.
|
|
2
4
|
export const OMP_SUBAGENT_MARKER = 'You are operating on a piece of work assigned to you by the main agent.';
|
|
3
5
|
export const COMBO_LEVELS = Object.freeze({
|
|
4
6
|
off: Object.freeze({ level: 'off', caveman: 'off', rtk: 'off', ponytail: 'off' }),
|
|
@@ -51,12 +53,12 @@ function bridge() {
|
|
|
51
53
|
if (existing?.state)
|
|
52
54
|
return existing;
|
|
53
55
|
const initial = normalizedState((existing || COMBO_LEVELS.off));
|
|
54
|
-
return (globalThis[BRIDGE_KEY] = { state: initial, listeners: new
|
|
56
|
+
return (globalThis[BRIDGE_KEY] = { state: initial, listeners: new Map() });
|
|
55
57
|
}
|
|
56
58
|
function publish(state) {
|
|
57
59
|
const shared = bridge();
|
|
58
60
|
shared.state = state;
|
|
59
|
-
for (const listener of shared.listeners)
|
|
61
|
+
for (const listener of shared.listeners.values())
|
|
60
62
|
listener(state);
|
|
61
63
|
return state;
|
|
62
64
|
}
|
|
@@ -70,11 +72,19 @@ export function systemPromptIncludes(systemPrompt, marker) {
|
|
|
70
72
|
return asPromptArray(systemPrompt).some((prompt) => typeof prompt === 'string' && prompt.includes(marker));
|
|
71
73
|
}
|
|
72
74
|
export function isOmpSubagentPrompt(systemPrompt) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
+
const raw = readPluginSettings().subagentMarkers;
|
|
76
|
+
const configured = Array.isArray(raw) ? raw.filter((m) => typeof m === 'string' && m.trim() !== '') : [];
|
|
77
|
+
// A malformed setting falls back rather than matching every turn.
|
|
78
|
+
const markers = configured.length ? configured : [OMP_SUBAGENT_MARKER];
|
|
79
|
+
return markers.some((marker) => systemPromptIncludes(systemPrompt, marker));
|
|
80
|
+
}
|
|
81
|
+
// ultra maps to max, lite/full to nearest, rather than erroring.
|
|
82
|
+
const COMBO_ALIASES = { ultra: 'max', lite: 'medium', full: 'balanced' };
|
|
75
83
|
export function normalizeComboLevel(value) {
|
|
76
84
|
const level = String(value || '').trim().toLowerCase();
|
|
77
|
-
|
|
85
|
+
if (isPresetLevel(level))
|
|
86
|
+
return level;
|
|
87
|
+
return COMBO_ALIASES[level] ?? null;
|
|
78
88
|
}
|
|
79
89
|
// OMP themes status text through ctx.ui.theme.fg; pi has no ctx.ui.theme, so
|
|
80
90
|
// the same string renders plain there.
|
|
@@ -87,13 +97,19 @@ export function themeStatus(ui, indicator, label, highlight = false) {
|
|
|
87
97
|
export function paintStatusBar(ui, key, emoji, label, isActive) {
|
|
88
98
|
ui?.setStatus?.(key, themeStatus(ui, emoji, label, isActive));
|
|
89
99
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
100
|
+
export function statusUi(ctx) {
|
|
101
|
+
try {
|
|
102
|
+
const ui = ctx?.ui;
|
|
103
|
+
return ui?.setStatus ? ui : undefined;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Which context a status bar paints through. Never remember a UI-less event:
|
|
110
|
+
// that mutes every later bare syncStatus(), freezing the bar on a stale preset.
|
|
95
111
|
export function paintableCtx(remembered, next) {
|
|
96
|
-
return next
|
|
112
|
+
return statusUi(next) ? next : remembered;
|
|
97
113
|
}
|
|
98
114
|
// Last-wins scan for a custom session entry; skips entries whose value fails
|
|
99
115
|
// to parse so a corrupt write never shadows an older valid one.
|
|
@@ -162,13 +178,9 @@ export function reconcileSharedComboEntries(entries) {
|
|
|
162
178
|
export function activeModesSummary(state) {
|
|
163
179
|
return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
|
|
164
180
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (typeof listener === 'function')
|
|
169
|
-
bridge().listeners.add(listener);
|
|
170
|
-
else
|
|
171
|
-
bridge().listeners.clear();
|
|
181
|
+
// Keyed registration drops stale closures; hosts offer no teardown hook.
|
|
182
|
+
export function setSharedComboListener(key, listener) {
|
|
183
|
+
bridge().listeners.set(key, listener);
|
|
172
184
|
}
|
|
173
185
|
export function resetSharedComboState() {
|
|
174
186
|
return setSharedComboLevel('off');
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { readPluginSettings } from './plugin-settings.ts';
|
|
1
2
|
import type { ComboLevel, ComboState, ExtensionCtx, SessionEntry, UiApi } from './types.ts';
|
|
2
3
|
|
|
3
4
|
const BRIDGE_KEY = Symbol.for('tersio/combo-session-state');
|
|
4
5
|
|
|
6
|
+
// A verbatim sentence from OMP's subagent prompt, so it can drift; settings.json overrides it.
|
|
5
7
|
export const OMP_SUBAGENT_MARKER = 'You are operating on a piece of work assigned to you by the main agent.';
|
|
6
8
|
|
|
7
9
|
export const COMBO_LEVELS: Record<string, Readonly<ComboState>> = Object.freeze({
|
|
@@ -28,7 +30,7 @@ const MODE_ENTRY_TYPES: Record<string, ModeName> = {
|
|
|
28
30
|
|
|
29
31
|
interface Bridge {
|
|
30
32
|
state: Readonly<ComboState>;
|
|
31
|
-
listeners:
|
|
33
|
+
listeners: Map<string, (state: Readonly<ComboState>) => void>;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export function normalizeMode(name: ModeName, value: unknown): string | null {
|
|
@@ -67,13 +69,13 @@ function bridge(): Bridge {
|
|
|
67
69
|
const existing = (globalThis as Record<symbol, unknown>)[BRIDGE_KEY] as Bridge | undefined;
|
|
68
70
|
if (existing?.state) return existing;
|
|
69
71
|
const initial = normalizedState((existing || COMBO_LEVELS.off) as Partial<Modes>);
|
|
70
|
-
return ((globalThis as Record<symbol, unknown>)[BRIDGE_KEY] = { state: initial, listeners: new
|
|
72
|
+
return ((globalThis as Record<symbol, unknown>)[BRIDGE_KEY] = { state: initial, listeners: new Map() });
|
|
71
73
|
}
|
|
72
74
|
|
|
73
75
|
function publish(state: Readonly<ComboState>): Readonly<ComboState> {
|
|
74
76
|
const shared = bridge();
|
|
75
77
|
shared.state = state;
|
|
76
|
-
for (const listener of shared.listeners) listener(state);
|
|
78
|
+
for (const listener of shared.listeners.values()) listener(state);
|
|
77
79
|
return state;
|
|
78
80
|
}
|
|
79
81
|
|
|
@@ -90,12 +92,20 @@ export function systemPromptIncludes(systemPrompt: string | string[], marker: st
|
|
|
90
92
|
}
|
|
91
93
|
|
|
92
94
|
export function isOmpSubagentPrompt(systemPrompt: string | string[]): boolean {
|
|
93
|
-
|
|
95
|
+
const raw = readPluginSettings().subagentMarkers;
|
|
96
|
+
const configured = Array.isArray(raw) ? raw.filter((m): m is string => typeof m === 'string' && m.trim() !== '') : [];
|
|
97
|
+
// A malformed setting falls back rather than matching every turn.
|
|
98
|
+
const markers = configured.length ? configured : [OMP_SUBAGENT_MARKER];
|
|
99
|
+
return markers.some((marker) => systemPromptIncludes(systemPrompt, marker));
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
// ultra maps to max, lite/full to nearest, rather than erroring.
|
|
103
|
+
const COMBO_ALIASES: Record<string, ComboLevel> = { ultra: 'max', lite: 'medium', full: 'balanced' };
|
|
104
|
+
|
|
96
105
|
export function normalizeComboLevel(value: unknown): ComboLevel | null {
|
|
97
106
|
const level = String(value || '').trim().toLowerCase();
|
|
98
|
-
|
|
107
|
+
if (isPresetLevel(level)) return level;
|
|
108
|
+
return COMBO_ALIASES[level] ?? null;
|
|
99
109
|
}
|
|
100
110
|
|
|
101
111
|
// OMP themes status text through ctx.ui.theme.fg; pi has no ctx.ui.theme, so
|
|
@@ -110,13 +120,22 @@ export function paintStatusBar(ui: UiApi | undefined, key: string, emoji: string
|
|
|
110
120
|
ui?.setStatus?.(key, themeStatus(ui, emoji, label, isActive));
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
// Stale ctx access throws, so never let it abort a mode restore.
|
|
124
|
+
export type StatusUi = UiApi & { setStatus: NonNullable<UiApi['setStatus']> };
|
|
125
|
+
|
|
126
|
+
export function statusUi(ctx: ExtensionCtx | undefined): StatusUi | undefined {
|
|
127
|
+
try {
|
|
128
|
+
const ui = ctx?.ui;
|
|
129
|
+
return ui?.setStatus ? (ui as StatusUi) : undefined;
|
|
130
|
+
} catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Which context a status bar paints through. Never remember a UI-less event:
|
|
136
|
+
// that mutes every later bare syncStatus(), freezing the bar on a stale preset.
|
|
118
137
|
export function paintableCtx(remembered: ExtensionCtx | undefined, next: ExtensionCtx | undefined): ExtensionCtx | undefined {
|
|
119
|
-
return next
|
|
138
|
+
return statusUi(next) ? next : remembered;
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
// Last-wins scan for a custom session entry; skips entries whose value fails
|
|
@@ -186,11 +205,9 @@ export function activeModesSummary(state: { caveman: string; rtk: string; ponyta
|
|
|
186
205
|
return `caveman=${state.caveman.toUpperCase()}, rtk=${state.rtk.toUpperCase()}, ponytail=${state.ponytail.toUpperCase()}`;
|
|
187
206
|
}
|
|
188
207
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
if (typeof listener === 'function') bridge().listeners.add(listener);
|
|
193
|
-
else bridge().listeners.clear();
|
|
208
|
+
// Keyed registration drops stale closures; hosts offer no teardown hook.
|
|
209
|
+
export function setSharedComboListener(key: string, listener: (state: Readonly<ComboState>) => void): void {
|
|
210
|
+
bridge().listeners.set(key, listener);
|
|
194
211
|
}
|
|
195
212
|
|
|
196
213
|
export function resetSharedComboState(): Readonly<ComboState> {
|