@krtclcdy/tersio 2.24.0 → 2.25.0
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 +28 -24
- package/cli/banner.js +7 -5
- package/cli/common.js +14 -13
- package/cli/currency.js +2 -7
- package/cli/dashboard.js +220 -13
- package/cli/doctor-fix.js +99 -29
- package/cli/doctor.js +32 -35
- package/cli/hosts.js +23 -26
- package/cli/install.js +228 -115
- package/cli/interactive.js +65 -21
- package/cli/manifest.js +8 -4
- package/cli/profile.js +49 -16
- package/cli/reset.js +3 -7
- package/cli/rtk-wiring.js +29 -32
- package/cli/settings.js +40 -16
- package/cli/uninstall.js +79 -29
- package/cli/update.js +27 -20
- package/cli/usage.js +54 -19
- package/dashboard/dist/index.html +18 -18
- package/extensions/ai-addons-updater/index.js +75 -20
- package/extensions/ai-addons-updater/index.ts +67 -23
- package/extensions/caveman-session/index.js +33 -75
- package/extensions/caveman-session/index.ts +35 -77
- package/extensions/combo-toggle/index.js +40 -62
- package/extensions/combo-toggle/index.ts +38 -72
- package/extensions/lib/utils.js +64 -15
- package/extensions/lib/utils.ts +62 -15
- package/extensions/opencode/index.js +104 -0
- package/extensions/opencode/server.js +190 -0
- package/extensions/opencode/server.ts +200 -0
- package/extensions/rtk-session/index.js +38 -57
- package/extensions/rtk-session/index.ts +36 -55
- package/extensions/shared/carbon.js +4 -10
- package/extensions/shared/carbon.ts +5 -10
- package/extensions/shared/host.js +95 -56
- package/extensions/shared/host.ts +106 -60
- package/extensions/shared/plugin-settings.js +6 -18
- package/extensions/shared/plugin-settings.ts +6 -18
- package/extensions/shared/pricing.js +79 -27
- package/extensions/shared/pricing.ts +80 -28
- package/extensions/shared/rtk-gain.js +4 -11
- package/extensions/shared/rtk-gain.ts +4 -11
- package/extensions/shared/session-state.js +28 -39
- package/extensions/shared/session-state.ts +29 -38
- package/extensions/shared/status.js +38 -0
- package/extensions/shared/status.ts +45 -0
- package/extensions/shared/types.js +1 -2
- package/extensions/shared/types.ts +2 -2
- package/extensions/shared/usage-ledger.js +312 -57
- package/extensions/shared/usage-ledger.ts +321 -61
- package/extensions/shared/usage-store.js +354 -38
- package/extensions/shared/usage-store.ts +360 -43
- package/extensions/tersio-commands/index.js +5 -14
- package/extensions/tersio-commands/index.ts +5 -14
- package/package.json +12 -7
- package/tersio.js +4 -6
- package/cli/agent-hosts.js +0 -42
- package/cli/agents.js +0 -266
- package/cli/layer.js +0 -24
- package/cli/omp-layer.js +0 -51
- package/cli/pi-layer.js +0 -37
- package/cli/pi-wiring.js +0 -92
- package/extensions/shared/omp-prompt.js +0 -12
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// OpenCode plugin entry: maps its plugin API onto the shared ExtensionApi the mode extensions consume.
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import type { Plugin } from '@opencode/plugin';
|
|
4
|
+
import cavemanSessionExtension from '../caveman-session/index.ts';
|
|
5
|
+
import rtkSessionExtension from '../rtk-session/index.ts';
|
|
6
|
+
import comboToggleExtension from '../combo-toggle/index.ts';
|
|
7
|
+
import tersioCommandsExtension from '../tersio-commands/index.ts';
|
|
8
|
+
import { getSharedComboState } from '../shared/session-state.ts';
|
|
9
|
+
import { formatStatus } from '../shared/status.ts';
|
|
10
|
+
import type { ExtensionApi, ExtensionCtx, InputEvent, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
11
|
+
|
|
12
|
+
interface CommandConfig {
|
|
13
|
+
description: string;
|
|
14
|
+
handler: (args: string, ctx: ExtensionCtx) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ToolConfig {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
parameters: unknown;
|
|
21
|
+
execute: (
|
|
22
|
+
toolCallId: string,
|
|
23
|
+
params: { args: string[] },
|
|
24
|
+
signal: AbortSignal | undefined,
|
|
25
|
+
onUpdate: ((data: unknown) => void) | undefined,
|
|
26
|
+
ctx: ExtensionCtx | undefined,
|
|
27
|
+
) => Promise<{ isError: boolean; content: { type: string; text: string }[]; details: Record<string, unknown> }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Plain object, not Plugin.define: define is an identity wrapper, and a value
|
|
31
|
+
// import would force every install to vendor the whole @opencode dependency tree.
|
|
32
|
+
const plugin: Plugin.Plugin = {
|
|
33
|
+
id: 'tersio',
|
|
34
|
+
async setup(ctx) {
|
|
35
|
+
// Entries are scoped per session; an unseen ID restores on its first hook.
|
|
36
|
+
const sessions = new Map<string, SessionEntry[]>();
|
|
37
|
+
const started = new Map<string, Promise<void>>();
|
|
38
|
+
let activeSid: string | undefined;
|
|
39
|
+
const loadEntries = async (sid: string): Promise<SessionEntry[]> => {
|
|
40
|
+
const own = ((await ctx.storage.get(`entries:${sid}`)) as SessionEntry[] | undefined) ?? [];
|
|
41
|
+
if (own.length) return own;
|
|
42
|
+
// Pre-scoped installs kept one global list; adopt it once, then diverge.
|
|
43
|
+
const legacy = ((await ctx.storage.get('entries')) as SessionEntry[] | undefined) ?? [];
|
|
44
|
+
return [...legacy];
|
|
45
|
+
};
|
|
46
|
+
const sessionEntriesOf = (sid: string): SessionEntry[] => {
|
|
47
|
+
let list = sessions.get(sid);
|
|
48
|
+
if (!list) {
|
|
49
|
+
list = [];
|
|
50
|
+
sessions.set(sid, list);
|
|
51
|
+
}
|
|
52
|
+
return list;
|
|
53
|
+
};
|
|
54
|
+
const persist = (sid: string): void => {
|
|
55
|
+
void ctx.storage.set(`entries:${sid}`, sessionEntriesOf(sid) as unknown as never).catch(() => {});
|
|
56
|
+
};
|
|
57
|
+
const stubCtx = (sid?: string): ExtensionCtx => ({
|
|
58
|
+
hasUI: false,
|
|
59
|
+
cwd: ctx.location.directory,
|
|
60
|
+
sessionManager: sid === undefined
|
|
61
|
+
? { getBranch: () => [], getEntries: () => [] }
|
|
62
|
+
: {
|
|
63
|
+
getBranch: () => sessionEntriesOf(sid),
|
|
64
|
+
getEntries: () => sessionEntriesOf(sid),
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// Starts are chained so appends land in the calling session.
|
|
68
|
+
let startQueue = Promise.resolve();
|
|
69
|
+
const runSessionStart = (sid: string): void => {
|
|
70
|
+
const prev = started.get(sid);
|
|
71
|
+
if (prev) return;
|
|
72
|
+
const run = startQueue.then(async () => {
|
|
73
|
+
for (const row of await loadEntries(sid)) sessionEntriesOf(sid).push(row);
|
|
74
|
+
activeSid = sid;
|
|
75
|
+
try {
|
|
76
|
+
for (const handler of lifecycle.get('session_start') ?? []) await handler({}, stubCtx(sid));
|
|
77
|
+
} finally {
|
|
78
|
+
persist(sid);
|
|
79
|
+
activeSid = undefined;
|
|
80
|
+
}
|
|
81
|
+
}).catch(() => {});
|
|
82
|
+
started.set(sid, run);
|
|
83
|
+
startQueue = run;
|
|
84
|
+
};
|
|
85
|
+
const ensureSession = (sid: string): Promise<void> => {
|
|
86
|
+
runSessionStart(sid);
|
|
87
|
+
return started.get(sid) ?? Promise.resolve();
|
|
88
|
+
};
|
|
89
|
+
const commands = new Map<string, CommandConfig>();
|
|
90
|
+
const tools = new Map<string, ToolConfig>();
|
|
91
|
+
const beforeStart: Array<(event: SystemPromptEvent, ctx: ExtensionCtx) => unknown> = [];
|
|
92
|
+
const inputHandlers: Array<(event: InputEvent) => unknown> = [];
|
|
93
|
+
const lifecycle = new Map<string, Array<(event: unknown, ctx: ExtensionCtx) => unknown>>();
|
|
94
|
+
|
|
95
|
+
const host: ExtensionApi = {
|
|
96
|
+
hostId: 'opencode',
|
|
97
|
+
setLabel() {},
|
|
98
|
+
registerCommand(name, config) { commands.set(name, config); },
|
|
99
|
+
registerTool(tool) { tools.set(tool.name, tool as ToolConfig); },
|
|
100
|
+
appendEntry(type, data) {
|
|
101
|
+
if (activeSid === undefined) return;
|
|
102
|
+
sessionEntriesOf(activeSid).push({ type: 'custom', customType: type, data });
|
|
103
|
+
persist(activeSid);
|
|
104
|
+
},
|
|
105
|
+
exec: (cmd, args, opts) => new Promise((resolve) => {
|
|
106
|
+
execFile(cmd, args, { signal: opts?.signal, cwd: opts?.cwd }, (error, stdout, stderr) => {
|
|
107
|
+
const code = error && typeof (error as { code?: unknown }).code === 'number' ? (error as { code: number }).code : 0;
|
|
108
|
+
resolve({ stdout: stdout ?? '', stderr: stderr ?? '', code, killed: !!(error as { killed?: boolean } | null)?.killed });
|
|
109
|
+
});
|
|
110
|
+
}),
|
|
111
|
+
cwd: ctx.location.directory,
|
|
112
|
+
on: (event, handler) => {
|
|
113
|
+
if (event === 'before_agent_start') { beforeStart.push(handler as (event: SystemPromptEvent, ctx: ExtensionCtx) => unknown); return; }
|
|
114
|
+
if (event === 'input') { inputHandlers.push(handler as (event: InputEvent) => unknown); return; }
|
|
115
|
+
const list = lifecycle.get(event) ?? [];
|
|
116
|
+
list.push(handler as (event: unknown, ctx: ExtensionCtx) => unknown);
|
|
117
|
+
lifecycle.set(event, list);
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
for (const extension of [cavemanSessionExtension, rtkSessionExtension, comboToggleExtension, tersioCommandsExtension]) extension(host);
|
|
122
|
+
|
|
123
|
+
// The first hook of an unseen session runs its session_start handlers.
|
|
124
|
+
const sidOf = (value: unknown): string | undefined =>
|
|
125
|
+
typeof value === 'string' && value ? value : undefined;
|
|
126
|
+
|
|
127
|
+
await ctx.command.transform((editor) => {
|
|
128
|
+
for (const [name, config] of commands) {
|
|
129
|
+
editor.add({
|
|
130
|
+
name,
|
|
131
|
+
description: config.description,
|
|
132
|
+
execute: async (input) => {
|
|
133
|
+
const sid = sidOf((input as { sessionID?: unknown }).sessionID);
|
|
134
|
+
if (sid) await ensureSession(sid);
|
|
135
|
+
const text = typeof input.prompt?.text === 'string' ? input.prompt.text : '';
|
|
136
|
+
const args = text.includes(' ') ? text.slice(text.indexOf(' ') + 1).trim() : '';
|
|
137
|
+
// The shared handlers report through ui.notify, which has no OpenCode
|
|
138
|
+
// surface. Collect it and send it back so a command never answers blank.
|
|
139
|
+
const notes: string[] = [];
|
|
140
|
+
const cmdCtx: ExtensionCtx = {
|
|
141
|
+
...stubCtx(sid),
|
|
142
|
+
hasUI: true,
|
|
143
|
+
ui: { notify: (message: string) => { notes.push(message); } },
|
|
144
|
+
};
|
|
145
|
+
const prev = activeSid;
|
|
146
|
+
activeSid = sid;
|
|
147
|
+
try {
|
|
148
|
+
await config.handler(args, cmdCtx);
|
|
149
|
+
} finally {
|
|
150
|
+
activeSid = prev;
|
|
151
|
+
}
|
|
152
|
+
const reply = notes.length ? notes.join('\n') : formatStatus(getSharedComboState());
|
|
153
|
+
await ctx.session.prompt({ sessionID: input.sessionID, text: reply });
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
await ctx.tool.transform((editor) => {
|
|
160
|
+
for (const tool of tools.values()) {
|
|
161
|
+
editor.add({
|
|
162
|
+
name: tool.name,
|
|
163
|
+
description: tool.description,
|
|
164
|
+
input: tool.parameters as { type: 'object'; properties: Record<string, unknown>; required?: string[] },
|
|
165
|
+
execute: async (input) => {
|
|
166
|
+
const args = Array.isArray((input as { args?: unknown }).args) ? ((input as { args: string[] }).args) : [];
|
|
167
|
+
const result = await tool.execute('t', { args }, undefined, undefined, stubCtx());
|
|
168
|
+
return { content: result.content.map((part) => part.text).join('\n') };
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
await ctx.session.hook('context', async (event) => {
|
|
175
|
+
const sid = sidOf((event as { sessionID?: unknown }).sessionID);
|
|
176
|
+
if (sid) await ensureSession(sid);
|
|
177
|
+
const systemParts = event.system.map((part) => (part.type === 'text' ? part.text : '')).filter(Boolean);
|
|
178
|
+
for (const handler of beforeStart) {
|
|
179
|
+
const result = await handler({ systemPrompt: [...systemParts] } as SystemPromptEvent, stubCtx(sid));
|
|
180
|
+
if (!result || typeof result !== 'object' || !('systemPrompt' in result)) continue;
|
|
181
|
+
const parts = typeof (result as { systemPrompt: unknown }).systemPrompt === 'string'
|
|
182
|
+
? [(result as { systemPrompt: string }).systemPrompt]
|
|
183
|
+
: Array.isArray((result as { systemPrompt: unknown }).systemPrompt)
|
|
184
|
+
? (result as { systemPrompt: string[] }).systemPrompt
|
|
185
|
+
: [];
|
|
186
|
+
for (const part of parts) {
|
|
187
|
+
if (!systemParts.includes(part)) event.system.push({ type: 'text', text: part });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
await ctx.session.hook('prompt', async (event) => {
|
|
193
|
+
const sid = sidOf((event as { sessionID?: unknown }).sessionID);
|
|
194
|
+
if (sid) await ensureSession(sid);
|
|
195
|
+
for (const handler of inputHandlers) void handler({ text: event.prompt.text, source: 'interactive' });
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export default plugin;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.js';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.js';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.js';
|
|
4
|
+
import { resolveRtkBinary } from '../lib/utils.js';
|
|
5
|
+
import { announceStatus } from '../shared/status.js';
|
|
4
6
|
const DEFAULT_ENABLED = false;
|
|
5
7
|
function asBoolean(value) {
|
|
6
8
|
if (typeof value === 'boolean')
|
|
@@ -23,37 +25,23 @@ function setRtkProcessEnabled(enabled) {
|
|
|
23
25
|
const RTK_PROMPT = `RTK guidance active. RTK automatically rewrites eligible Bash calls through the installed rtk hook. Prefer rtk for noisy shell output, but use exact raw output for state changes, checksums, patches, and diagnostics that need full bytes.`;
|
|
24
26
|
export default function rtkSessionExtension(pi) {
|
|
25
27
|
let enabled = DEFAULT_ENABLED;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
function syncStatus(ctx) {
|
|
29
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
30
|
-
const c = lastCtx;
|
|
31
|
-
if (!c?.ui?.setStatus)
|
|
32
|
-
return;
|
|
33
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
34
|
-
if (isComboPresetActive() || !enabled) {
|
|
35
|
-
c.ui.setStatus('rtk', undefined);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
paintStatusBar(c.ui, 'rtk', '⚡', 'rtk: ON', isActive);
|
|
28
|
+
function fail(text) {
|
|
29
|
+
return { isError: true, content: [{ type: 'text', text }], details: { enabled } };
|
|
39
30
|
}
|
|
40
31
|
function setEnabled(next, ctx) {
|
|
41
32
|
enabled = Boolean(next);
|
|
42
33
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
43
34
|
setSharedComboMode('rtk', enabled);
|
|
44
35
|
setRtkProcessEnabled(enabled);
|
|
45
|
-
|
|
46
|
-
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
36
|
+
announceStatus(ctx);
|
|
47
37
|
}
|
|
48
38
|
setExtensionLabel(pi, 'RTK session toggle');
|
|
49
|
-
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
50
|
-
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
39
|
+
// Live mirror: a /tersio or /combo switch publishes shared state — adopt it at once so the next turn and the rtk_run gate see it, no reload.
|
|
51
40
|
function syncFromShared(state) {
|
|
52
41
|
enabled = state.rtk === 'on';
|
|
53
42
|
setRtkProcessEnabled(enabled);
|
|
54
|
-
syncStatus();
|
|
55
43
|
}
|
|
56
|
-
setSharedComboListener(syncFromShared);
|
|
44
|
+
setSharedComboListener('rtk', syncFromShared);
|
|
57
45
|
pi.registerCommand?.('rtk', {
|
|
58
46
|
description: 'Toggle RTK compact shell-output guidance for this session',
|
|
59
47
|
handler: async (args, ctx) => {
|
|
@@ -82,49 +70,41 @@ export default function rtkSessionExtension(pi) {
|
|
|
82
70
|
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
83
71
|
}),
|
|
84
72
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
85
|
-
|
|
73
|
+
// A subagent's own flag sits at its start-up default, so honour shared state too.
|
|
74
|
+
if (!(enabled || getSharedComboState().rtk === 'on'))
|
|
75
|
+
return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
|
|
76
|
+
if (typeof pi.exec !== 'function')
|
|
77
|
+
return fail('This host cannot run rtk.');
|
|
78
|
+
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
79
|
+
try {
|
|
80
|
+
// A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
|
|
81
|
+
const result = await pi.exec(resolveRtkBinary() ?? 'rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
82
|
+
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
86
83
|
return {
|
|
87
|
-
isError:
|
|
88
|
-
content: [{ type: 'text', text:
|
|
89
|
-
details: { enabled },
|
|
84
|
+
isError: result.code !== 0,
|
|
85
|
+
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
86
|
+
details: { code: result.code, killed: result.killed, enabled },
|
|
90
87
|
};
|
|
91
88
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
isError: result.code !== 0,
|
|
97
|
-
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
98
|
-
details: { code: result.code, killed: result.killed, enabled },
|
|
99
|
-
};
|
|
89
|
+
catch (e) {
|
|
90
|
+
return fail(`rtk could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
91
|
+
}
|
|
100
92
|
},
|
|
101
93
|
});
|
|
102
|
-
onHostEvent(pi, 'input', async (event) => {
|
|
103
|
-
if (event?.source === 'extension')
|
|
104
|
-
return;
|
|
105
|
-
const t = normalizeInputCommand(event?.text);
|
|
106
|
-
if (t === 'rtk on' || t === 'use rtk')
|
|
107
|
-
setEnabled(true);
|
|
108
|
-
if (t === 'rtk off' || t === 'stop rtk')
|
|
109
|
-
setEnabled(false);
|
|
110
|
-
});
|
|
111
94
|
function restoreEnabled(ctx) {
|
|
112
95
|
const entries = sessionEntries(ctx);
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
//
|
|
117
|
-
reconcileSharedComboEntries(entries);
|
|
118
|
-
// Persisted session state wins; a fresh session falls back to the
|
|
119
|
-
// installer/user-configured default (off unless configured).
|
|
96
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
97
|
+
if (hasModeState(entries))
|
|
98
|
+
reconcileSharedComboEntries(entries);
|
|
99
|
+
// Persisted session state wins; a fresh session falls back to the installer/user-configured default (off unless configured).
|
|
120
100
|
const persisted = resolveEnabled(entries);
|
|
121
|
-
enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
|
|
101
|
+
enabled = typeof persisted === 'boolean' ? persisted : enabled || readRtkDefault();
|
|
122
102
|
setRtkProcessEnabled(enabled);
|
|
123
|
-
|
|
103
|
+
announceStatus(ctx);
|
|
124
104
|
}
|
|
105
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
125
106
|
pi.on('session_start', async (_event, ctx) => {
|
|
126
107
|
restoreEnabled(ctx);
|
|
127
|
-
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
128
108
|
});
|
|
129
109
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
130
110
|
restoreEnabled(ctx);
|
|
@@ -132,18 +112,19 @@ export default function rtkSessionExtension(pi) {
|
|
|
132
112
|
pi.on('session_tree', async (_event, ctx) => {
|
|
133
113
|
restoreEnabled(ctx);
|
|
134
114
|
});
|
|
135
|
-
pi
|
|
136
|
-
|
|
137
|
-
syncStatus(ctx);
|
|
115
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
116
|
+
restoreEnabled(ctx);
|
|
138
117
|
});
|
|
139
|
-
pi
|
|
140
|
-
|
|
141
|
-
syncStatus(ctx);
|
|
118
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
119
|
+
restoreEnabled(ctx);
|
|
142
120
|
});
|
|
143
121
|
pi.on('before_agent_start', async (event) => {
|
|
144
122
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
145
123
|
if (!active)
|
|
146
124
|
return;
|
|
125
|
+
// Skip when already present, so a host that re-presents the prompt cannot stack it.
|
|
126
|
+
if (systemPromptIncludes(event.systemPrompt, RTK_PROMPT))
|
|
127
|
+
return;
|
|
147
128
|
return injectPromptText(pi, event, RTK_PROMPT);
|
|
148
129
|
});
|
|
149
130
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getSharedComboState, hasModeState, isOmpSubagentPrompt, lastCustomValue, reconcileSharedComboEntries, sessionEntries, setSharedComboListener, setSharedComboMode, systemPromptIncludes } from '../shared/session-state.ts';
|
|
2
2
|
import { injectPromptText, onHostEvent, setExtensionLabel, stringArrayToolParams } from '../shared/host.ts';
|
|
3
3
|
import { readRtkDefault } from '../shared/plugin-settings.ts';
|
|
4
|
-
import
|
|
4
|
+
import { resolveRtkBinary } from '../lib/utils.ts';
|
|
5
|
+
import { announceStatus } from '../shared/status.ts';
|
|
6
|
+
import type { ExtensionApi, ExtensionCtx, SessionEntry, SystemPromptEvent } from '../shared/types.ts';
|
|
5
7
|
|
|
6
8
|
const DEFAULT_ENABLED = false;
|
|
7
9
|
|
|
@@ -25,40 +27,28 @@ const RTK_PROMPT = `RTK guidance active. RTK automatically rewrites eligible Bas
|
|
|
25
27
|
|
|
26
28
|
export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
27
29
|
let enabled = DEFAULT_ENABLED;
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
function syncStatus(ctx?: ExtensionCtx): void {
|
|
32
|
-
lastCtx = paintableCtx(lastCtx, ctx);
|
|
33
|
-
const c = lastCtx;
|
|
34
|
-
if (!c?.ui?.setStatus) return;
|
|
35
|
-
// Combo owns the bar when any preset is active; keep ours empty to avoid duplication.
|
|
36
|
-
if (isComboPresetActive() || !enabled) {
|
|
37
|
-
c.ui.setStatus('rtk', undefined);
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
paintStatusBar(c.ui, 'rtk', '⚡', 'rtk: ON', isActive);
|
|
30
|
+
|
|
31
|
+
function fail(text: string) {
|
|
32
|
+
return { isError: true as const, content: [{ type: 'text' as const, text }], details: { enabled } };
|
|
41
33
|
}
|
|
42
34
|
|
|
35
|
+
|
|
43
36
|
function setEnabled(next: unknown, ctx?: ExtensionCtx): void {
|
|
44
37
|
enabled = Boolean(next);
|
|
45
38
|
pi.appendEntry?.('rtk-mode', { enabled });
|
|
46
39
|
setSharedComboMode('rtk', enabled);
|
|
47
40
|
setRtkProcessEnabled(enabled);
|
|
48
|
-
|
|
49
|
-
ctx?.ui?.notify?.(enabled ? `RTK on — compact shell output for this session. Active: ${active}.` : `RTK off. Active: ${active}.`, 'info');
|
|
41
|
+
announceStatus(ctx);
|
|
50
42
|
}
|
|
51
43
|
|
|
52
44
|
setExtensionLabel(pi, 'RTK session toggle');
|
|
53
45
|
|
|
54
|
-
// Live mirror: a /tersio or /combo switch publishes shared state — adopt
|
|
55
|
-
// it at once so the next turn and the rtk_run gate see it, no reload.
|
|
46
|
+
// Live mirror: a /tersio or /combo switch publishes shared state — adopt it at once so the next turn and the rtk_run gate see it, no reload.
|
|
56
47
|
function syncFromShared(state: { rtk: string }): void {
|
|
57
48
|
enabled = state.rtk === 'on';
|
|
58
49
|
setRtkProcessEnabled(enabled);
|
|
59
|
-
syncStatus();
|
|
60
50
|
}
|
|
61
|
-
setSharedComboListener(syncFromShared);
|
|
51
|
+
setSharedComboListener('rtk', syncFromShared);
|
|
62
52
|
pi.registerCommand?.('rtk', {
|
|
63
53
|
description: 'Toggle RTK compact shell-output guidance for this session',
|
|
64
54
|
handler: async (args, ctx) => {
|
|
@@ -88,48 +78,39 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
88
78
|
description: "Arguments passed to rtk, e.g. ['git','status'] or ['read','src/index.ts']",
|
|
89
79
|
}),
|
|
90
80
|
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
91
|
-
|
|
81
|
+
// A subagent's own flag sits at its start-up default, so honour shared state too.
|
|
82
|
+
if (!(enabled || getSharedComboState().rtk === 'on')) return fail('RTK mode is off. Run /rtk on for this session, or use bash explicitly.');
|
|
83
|
+
if (typeof pi.exec !== 'function') return fail('This host cannot run rtk.');
|
|
84
|
+
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
85
|
+
try {
|
|
86
|
+
// A GUI-launched host may lack ~/.bun/bin on PATH, so prefer the resolved path.
|
|
87
|
+
const result = await pi.exec(resolveRtkBinary() ?? 'rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
88
|
+
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
92
89
|
return {
|
|
93
|
-
isError:
|
|
94
|
-
content: [{ type: 'text', text:
|
|
95
|
-
details: { enabled },
|
|
90
|
+
isError: result.code !== 0,
|
|
91
|
+
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
92
|
+
details: { code: result.code, killed: result.killed, enabled },
|
|
96
93
|
};
|
|
94
|
+
} catch (e) {
|
|
95
|
+
return fail(`rtk could not run: ${e instanceof Error ? e.message : String(e)}`);
|
|
97
96
|
}
|
|
98
|
-
onUpdate?.({ content: [{ type: 'text', text: `rtk ${params.args.join(' ')}` }], details: { phase: 'start' } });
|
|
99
|
-
const result = await pi.exec!('rtk', params.args, { signal, cwd: ctx?.cwd || pi.cwd });
|
|
100
|
-
const text = [result.stdout, result.stderr].filter(Boolean).join('\n');
|
|
101
|
-
return {
|
|
102
|
-
isError: result.code !== 0,
|
|
103
|
-
content: [{ type: 'text', text: text || `rtk exited ${result.code}` }],
|
|
104
|
-
details: { code: result.code, killed: result.killed, enabled },
|
|
105
|
-
};
|
|
106
97
|
},
|
|
107
98
|
});
|
|
108
99
|
|
|
109
|
-
onHostEvent<InputEvent>(pi, 'input', async (event) => {
|
|
110
|
-
if (event?.source === 'extension') return;
|
|
111
|
-
const t = normalizeInputCommand(event?.text);
|
|
112
|
-
if (t === 'rtk on' || t === 'use rtk') setEnabled(true);
|
|
113
|
-
if (t === 'rtk off' || t === 'stop rtk') setEnabled(false);
|
|
114
|
-
});
|
|
115
100
|
function restoreEnabled(ctx?: ExtensionCtx): void {
|
|
116
101
|
const entries = sessionEntries(ctx);
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
// UI-gated. Deriving it here makes suppression independent of load order.
|
|
121
|
-
reconcileSharedComboEntries(entries);
|
|
122
|
-
// Persisted session state wins; a fresh session falls back to the
|
|
123
|
-
// installer/user-configured default (off unless configured).
|
|
102
|
+
// A subagent's branch has no Tersio entries; reconciling it would wipe shared state.
|
|
103
|
+
if (hasModeState(entries)) reconcileSharedComboEntries(entries);
|
|
104
|
+
// Persisted session state wins; a fresh session falls back to the installer/user-configured default (off unless configured).
|
|
124
105
|
const persisted = resolveEnabled(entries);
|
|
125
|
-
enabled = typeof persisted === 'boolean' ? persisted : readRtkDefault();
|
|
106
|
+
enabled = typeof persisted === 'boolean' ? persisted : enabled || readRtkDefault();
|
|
126
107
|
setRtkProcessEnabled(enabled);
|
|
127
|
-
|
|
108
|
+
announceStatus(ctx);
|
|
128
109
|
}
|
|
129
110
|
|
|
111
|
+
// Start, resume, and branch all land here, so every session type shows it.
|
|
130
112
|
pi.on('session_start', async (_event, ctx) => {
|
|
131
113
|
restoreEnabled(ctx);
|
|
132
|
-
ctx?.ui?.notify?.(`RTK loaded: ${enabled ? 'on' : 'off'}`, 'info');
|
|
133
114
|
});
|
|
134
115
|
|
|
135
116
|
onHostEvent(pi, 'session_branch', async (_event, ctx) => {
|
|
@@ -140,19 +121,19 @@ export default function rtkSessionExtension(pi: ExtensionApi): void {
|
|
|
140
121
|
restoreEnabled(ctx);
|
|
141
122
|
});
|
|
142
123
|
|
|
143
|
-
pi
|
|
144
|
-
|
|
145
|
-
syncStatus(ctx);
|
|
124
|
+
onHostEvent(pi, 'session_switch', async (_event, ctx) => {
|
|
125
|
+
restoreEnabled(ctx);
|
|
146
126
|
});
|
|
147
127
|
|
|
148
|
-
pi
|
|
149
|
-
|
|
150
|
-
syncStatus(ctx);
|
|
128
|
+
onHostEvent(pi, 'session_compact', async (_event, ctx) => {
|
|
129
|
+
restoreEnabled(ctx);
|
|
151
130
|
});
|
|
152
131
|
|
|
153
132
|
pi.on<SystemPromptEvent>('before_agent_start', async (event) => {
|
|
154
133
|
const active = isOmpSubagentPrompt(event.systemPrompt) ? getSharedComboState().rtk === 'on' : enabled;
|
|
155
134
|
if (!active) return;
|
|
135
|
+
// Skip when already present, so a host that re-presents the prompt cannot stack it.
|
|
136
|
+
if (systemPromptIncludes(event.systemPrompt, RTK_PROMPT)) return;
|
|
156
137
|
return injectPromptText(pi, event, RTK_PROMPT);
|
|
157
138
|
});
|
|
158
139
|
}
|
|
@@ -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
|
// EcoLogits 0.8.2 constants — kWh, kgCO2eq, seconds, GB.
|
|
10
3
|
const Q_BITS = 4;
|
|
11
4
|
const E_ALPHA = 8.91e-8;
|
|
@@ -28,8 +21,9 @@ const GRIDS = {
|
|
|
28
21
|
};
|
|
29
22
|
// Serving amortization: single-stream ceiling ÷ concurrency ≈ served impact.
|
|
30
23
|
export const SERVING_CONCURRENCY = 32;
|
|
31
|
-
|
|
32
|
-
|
|
24
|
+
/** EcoLogits release this port tracks, so a UI can name it instead of guessing. */
|
|
25
|
+
export const ECOLOGITS_VERSION = '0.8.2';
|
|
26
|
+
// Registry values verbatim from EcoLogits' model_repository; borrows cite the public param disclosure they stand in for (MoE active/total).
|
|
33
27
|
const TABLE = [
|
|
34
28
|
{ match: 'opus', active: 625.0, total: 2000, provider: 'anthropic', source: 'registry' },
|
|
35
29
|
{ match: 'sonnet', active: 137.5, total: 440, provider: 'anthropic', source: 'registry' },
|
|
@@ -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;
|
|
@@ -48,8 +44,7 @@ interface RegistryEntry {
|
|
|
48
44
|
source: 'registry' | 'borrowed';
|
|
49
45
|
}
|
|
50
46
|
|
|
51
|
-
// Registry values verbatim from EcoLogits' model_repository; borrows cite
|
|
52
|
-
// the public param disclosure they stand in for (MoE active/total).
|
|
47
|
+
// Registry values verbatim from EcoLogits' model_repository; borrows cite the public param disclosure they stand in for (MoE active/total).
|
|
53
48
|
const TABLE: RegistryEntry[] = [
|
|
54
49
|
{ match: 'opus', active: 625.0, total: 2000, provider: 'anthropic', source: 'registry' },
|
|
55
50
|
{ match: 'sonnet', active: 137.5, total: 440, provider: 'anthropic', source: 'registry' },
|