talon-agent 4.3.2 → 4.3.3
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/package.json +1 -1
- package/src/backend/claude-sdk/doctor.ts +1 -1
- package/src/backend/codex/doctor.ts +1 -1
- package/src/backend/openai-agents/doctor.ts +1 -1
- package/src/bootstrap.ts +1 -1
- package/src/cli/doctor.ts +1 -1
- package/src/core/agent-runtime/backend-registry.ts +1 -1
- package/src/core/auth/expiry-monitor.ts +1 -1
- package/src/core/{doctor.ts → doctor/index.ts} +11 -11
- package/src/core/{notify.ts → frontend-runtime/admin-notify.ts} +1 -1
- package/src/core/plugin/native-runtimes.ts +1 -1
- package/src/frontend/discord/commands/admin.ts +1 -1
- package/src/frontend/discord/render.ts +1 -1
- package/src/frontend/telegram/actions/index.ts +1 -1
- package/src/frontend/telegram/actions/moderation/chat.ts +63 -0
- package/src/frontend/telegram/actions/moderation/index.ts +74 -0
- package/src/frontend/telegram/actions/moderation/members.ts +77 -0
- package/src/frontend/telegram/actions/moderation/permissions.ts +69 -0
- package/src/frontend/telegram/actions/moderation/topics.ts +48 -0
- package/src/frontend/telegram/actions/moderation/types.ts +16 -0
- package/src/frontend/telegram/admin/background.ts +66 -0
- package/src/frontend/telegram/admin/chunked-reply.ts +20 -0
- package/src/frontend/telegram/admin/health.ts +81 -0
- package/src/frontend/telegram/admin/sessions.ts +105 -0
- package/src/frontend/telegram/admin.ts +56 -261
- package/src/frontend/telegram/callbacks/model/backend.ts +156 -0
- package/src/frontend/telegram/callbacks/model/control.ts +29 -0
- package/src/frontend/telegram/callbacks/model/nav.ts +54 -0
- package/src/frontend/telegram/callbacks/model/select.ts +100 -0
- package/src/frontend/telegram/callbacks/model/types.ts +39 -0
- package/src/frontend/telegram/callbacks/model/views.ts +116 -0
- package/src/frontend/telegram/callbacks/model.ts +61 -331
- package/src/frontend/telegram/commands/admin.ts +108 -77
- package/src/frontend/telegram/commands/settings.ts +65 -43
- package/src/frontend/telegram/commands/whatsapp-pairing.ts +1 -1
- package/src/frontend/telegram/helpers/diagnostics.ts +1 -1
- package/src/frontend/telegram/middleware.ts +166 -201
- package/src/frontend/terminal/builtins/context.ts +184 -0
- package/src/frontend/terminal/builtins/help.ts +35 -0
- package/src/frontend/terminal/builtins/index.ts +23 -0
- package/src/frontend/terminal/builtins/model.ts +198 -0
- package/src/frontend/terminal/builtins/session.ts +101 -0
- package/src/frontend/terminal/builtins/status.ts +202 -0
- package/src/frontend/terminal/command-registry.ts +80 -0
- package/src/frontend/terminal/commands.ts +11 -674
- package/src/frontend/terminal/input/history.ts +64 -0
- package/src/frontend/terminal/input/keys.ts +143 -0
- package/src/frontend/terminal/input/parts.ts +33 -0
- package/src/frontend/terminal/input/paste.ts +53 -0
- package/src/frontend/terminal/input/render.ts +42 -0
- package/src/frontend/terminal/input/state.ts +60 -0
- package/src/frontend/terminal/input.ts +27 -342
- package/src/frontend/whatsapp/connection.ts +1 -1
- package/src/frontend/whatsapp/index.ts +1 -1
- package/src/frontend/whatsapp/pairing-service.ts +1 -1
- package/src/plugins/github/provision.ts +1 -1
- package/src/plugins/mempalace/provision.ts +1 -1
- package/src/plugins/playwright/provision.ts +1 -1
- package/src/frontend/telegram/actions/moderation.ts +0 -255
- /package/src/core/{doctor-types.ts → doctor/types.ts} +0 -0
- /package/src/core/{pairing-broker.ts → frontend-runtime/pairing-broker.ts} +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/** `/model` and `/effort` — the per-chat model settings. */
|
|
2
|
+
|
|
3
|
+
import { getModels } from "../../../core/models/catalog.js";
|
|
4
|
+
import { resolveModelId as resolveModelName } from "../../../core/models/catalog.js";
|
|
5
|
+
import {
|
|
6
|
+
getChatSettings,
|
|
7
|
+
setChatModel,
|
|
8
|
+
setChatEffort,
|
|
9
|
+
} from "../../../storage/chat-settings.js";
|
|
10
|
+
import type { Command, CommandContext } from "../command-registry.js";
|
|
11
|
+
|
|
12
|
+
type BackendRef = CommandContext["backend"];
|
|
13
|
+
|
|
14
|
+
async function showCurrentModel(
|
|
15
|
+
ctx: CommandContext,
|
|
16
|
+
be: BackendRef,
|
|
17
|
+
currentModel: string,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const modelInfo = await be?.models?.getRawModelInfo?.(currentModel);
|
|
20
|
+
const displayName = modelInfo?.displayName ?? currentModel;
|
|
21
|
+
const details = modelInfo
|
|
22
|
+
? [
|
|
23
|
+
modelInfo.providerName,
|
|
24
|
+
modelInfo.free ? "free" : undefined,
|
|
25
|
+
modelInfo.selectable
|
|
26
|
+
? "ready"
|
|
27
|
+
: (modelInfo.unavailableReason ?? "not connected"),
|
|
28
|
+
].filter(Boolean)
|
|
29
|
+
: [];
|
|
30
|
+
ctx.renderer.writeSystem(
|
|
31
|
+
`Model: ${displayName}${details.length ? ` · ${details.join(" · ")}` : ""}`,
|
|
32
|
+
);
|
|
33
|
+
if (modelInfo?.contextWindow) {
|
|
34
|
+
ctx.renderer.writeln(
|
|
35
|
+
` Context window: ${modelInfo.contextWindow.toLocaleString()}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (be?.models?.getProviders) {
|
|
39
|
+
const providers = await be.models?.getProviders();
|
|
40
|
+
const connected = providers.filter((p) => p.connected);
|
|
41
|
+
if (connected.length > 0) {
|
|
42
|
+
ctx.renderer.writeln(
|
|
43
|
+
` Providers: ${connected.map((p) => `${p.name} (${p.modelCount})`).join(", ")}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
ctx.renderer.writeln(
|
|
48
|
+
` Use /model free, /model all, /model providers, or /model <name>.`,
|
|
49
|
+
);
|
|
50
|
+
ctx.reprompt();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function listModels(
|
|
54
|
+
ctx: CommandContext,
|
|
55
|
+
be: BackendRef,
|
|
56
|
+
lowerArgs: string,
|
|
57
|
+
): Promise<void> {
|
|
58
|
+
if (be?.models?.listModels) {
|
|
59
|
+
const filter = lowerArgs === "free" ? "free" : "all";
|
|
60
|
+
const { models, total } = await be.models.listModels(filter);
|
|
61
|
+
const list = models.slice(0, 20);
|
|
62
|
+
ctx.renderer.writeSystem(
|
|
63
|
+
`${filter === "free" ? "Free" : "Available"} models (${total})`,
|
|
64
|
+
);
|
|
65
|
+
for (const model of list) {
|
|
66
|
+
ctx.renderer.writeln(
|
|
67
|
+
` ${model.displayName} · ${model.providerName}${model.contextWindow ? ` · ${model.contextWindow.toLocaleString()} ctx` : ""}${model.free ? " · free" : ""}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (total > list.length) {
|
|
71
|
+
ctx.renderer.writeln(` …and ${total - list.length} more`);
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
const names = getModels()
|
|
75
|
+
.map((m) => m.aliases[0] ?? m.id)
|
|
76
|
+
.join(", ");
|
|
77
|
+
ctx.renderer.writeSystem(`Available: ${names}`);
|
|
78
|
+
}
|
|
79
|
+
ctx.reprompt();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function listProviders(
|
|
83
|
+
ctx: CommandContext,
|
|
84
|
+
be: BackendRef,
|
|
85
|
+
): Promise<void> {
|
|
86
|
+
if (be?.models?.getProviders) {
|
|
87
|
+
const providers = await be.models?.getProviders();
|
|
88
|
+
ctx.renderer.writeSystem(`Providers (${providers.length})`);
|
|
89
|
+
for (const p of providers.slice(0, 20)) {
|
|
90
|
+
ctx.renderer.writeln(
|
|
91
|
+
` ${p.name} · ${p.connected ? "connected" : "not connected"} · ${p.modelCount} models`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
ctx.renderer.writeSystem("Provider listing not supported.");
|
|
96
|
+
}
|
|
97
|
+
ctx.reprompt();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Resolve model query via backend (falling back to the static catalog). */
|
|
101
|
+
async function selectModel(
|
|
102
|
+
ctx: CommandContext,
|
|
103
|
+
be: BackendRef,
|
|
104
|
+
trimmedArgs: string,
|
|
105
|
+
): Promise<void> {
|
|
106
|
+
if (be?.models?.resolveModelInfo) {
|
|
107
|
+
const resolution = await be.models?.resolveModelInfo(trimmedArgs);
|
|
108
|
+
if (resolution.kind === "missing") {
|
|
109
|
+
const msg =
|
|
110
|
+
be.models?.formatModelError?.(trimmedArgs, resolution) ??
|
|
111
|
+
`No model matched "${trimmedArgs}".`;
|
|
112
|
+
ctx.renderer.writeError(msg);
|
|
113
|
+
ctx.reprompt();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (resolution.kind === "ambiguous") {
|
|
117
|
+
const preview = resolution.matches
|
|
118
|
+
.map((m) => `${m.displayName} (${m.providerName})`)
|
|
119
|
+
.join(", ");
|
|
120
|
+
ctx.renderer.writeError(`Ambiguous: "${trimmedArgs}" matches ${preview}`);
|
|
121
|
+
ctx.reprompt();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (!resolution.model.selectable) {
|
|
125
|
+
ctx.renderer.writeError(
|
|
126
|
+
resolution.model.unavailableReason ??
|
|
127
|
+
`${resolution.model.providerName} is not connected.`,
|
|
128
|
+
);
|
|
129
|
+
ctx.reprompt();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
setChatModel(ctx.chatId(), resolution.storedValue);
|
|
133
|
+
ctx.renderer.writeSystem(
|
|
134
|
+
`Model → ${resolution.model.displayName} (${resolution.model.providerName}${resolution.model.free ? " · free" : ""})`,
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
setChatModel(ctx.chatId(), resolveModelName(trimmedArgs));
|
|
138
|
+
ctx.renderer.writeSystem(`Model → ${resolveModelName(trimmedArgs)}`);
|
|
139
|
+
}
|
|
140
|
+
ctx.reprompt();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const modelCommand: Command = {
|
|
144
|
+
name: "model",
|
|
145
|
+
argHint: "[name]",
|
|
146
|
+
description: "Switch model",
|
|
147
|
+
async handler(args, ctx) {
|
|
148
|
+
const currentModel =
|
|
149
|
+
getChatSettings(ctx.chatId()).model ?? ctx.config.model;
|
|
150
|
+
const be = ctx.backend;
|
|
151
|
+
|
|
152
|
+
const trimmedArgs = args.trim();
|
|
153
|
+
const lowerArgs = trimmedArgs.toLowerCase();
|
|
154
|
+
|
|
155
|
+
if (!trimmedArgs) {
|
|
156
|
+
return showCurrentModel(ctx, be, currentModel);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (lowerArgs === "reset" || lowerArgs === "default") {
|
|
160
|
+
setChatModel(ctx.chatId(), undefined);
|
|
161
|
+
ctx.renderer.writeSystem(`Model → ${ctx.config.model}`);
|
|
162
|
+
ctx.reprompt();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (lowerArgs === "free" || lowerArgs === "list" || lowerArgs === "all") {
|
|
167
|
+
return listModels(ctx, be, lowerArgs);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (lowerArgs === "providers") {
|
|
171
|
+
return listProviders(ctx, be);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return selectModel(ctx, be, trimmedArgs);
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
export const effortCommand: Command = {
|
|
179
|
+
name: "effort",
|
|
180
|
+
argHint: "[lvl]",
|
|
181
|
+
description: "Thinking effort (off/low/medium/high/max)",
|
|
182
|
+
async handler(args, ctx) {
|
|
183
|
+
if (!args) {
|
|
184
|
+
ctx.renderer.writeSystem(
|
|
185
|
+
`Effort: ${getChatSettings(ctx.chatId()).effort ?? "adaptive"}`,
|
|
186
|
+
);
|
|
187
|
+
} else {
|
|
188
|
+
setChatEffort(
|
|
189
|
+
ctx.chatId(),
|
|
190
|
+
args === "adaptive"
|
|
191
|
+
? undefined
|
|
192
|
+
: (args as "off" | "low" | "medium" | "high" | "max"),
|
|
193
|
+
);
|
|
194
|
+
ctx.renderer.writeSystem(`Effort → ${args}`);
|
|
195
|
+
}
|
|
196
|
+
ctx.reprompt();
|
|
197
|
+
},
|
|
198
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** `/reset`, `/resume`, `/rename` — session lifecycle. */
|
|
2
|
+
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { formatTimeAgo } from "../renderer.js";
|
|
5
|
+
import { isTerminalChatId } from "../../../util/chat-id.js";
|
|
6
|
+
import { resolveModel as coreResolveModel } from "../../../core/models/catalog.js";
|
|
7
|
+
import {
|
|
8
|
+
getAllSessions,
|
|
9
|
+
getSession,
|
|
10
|
+
setSessionName,
|
|
11
|
+
} from "../../../storage/sessions.js";
|
|
12
|
+
import type { Command } from "../command-registry.js";
|
|
13
|
+
|
|
14
|
+
export const resetCommand: Command = {
|
|
15
|
+
name: "reset",
|
|
16
|
+
description: "Start a fresh session",
|
|
17
|
+
async handler(_args, ctx) {
|
|
18
|
+
ctx.initNewChat();
|
|
19
|
+
ctx.renderer.writeSystem("Session cleared.");
|
|
20
|
+
ctx.reprompt();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const resumeCommand: Command = {
|
|
25
|
+
name: "resume",
|
|
26
|
+
description: "List & resume a past session",
|
|
27
|
+
async handler(_args, ctx) {
|
|
28
|
+
const sessions = getAllSessions()
|
|
29
|
+
.filter(
|
|
30
|
+
(s) =>
|
|
31
|
+
isTerminalChatId(s.chatId) &&
|
|
32
|
+
s.chatId !== ctx.chatId() &&
|
|
33
|
+
s.info.turns > 0,
|
|
34
|
+
)
|
|
35
|
+
.sort((a, b) => b.info.lastActive - a.info.lastActive)
|
|
36
|
+
.slice(0, 10);
|
|
37
|
+
|
|
38
|
+
if (sessions.length === 0) {
|
|
39
|
+
ctx.renderer.writeSystem("No previous sessions to resume.");
|
|
40
|
+
ctx.reprompt();
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
ctx.renderer.writeln();
|
|
45
|
+
ctx.renderer.writeln(` ${pc.bold("Past sessions")}`);
|
|
46
|
+
for (let i = 0; i < sessions.length; i++) {
|
|
47
|
+
const s = sessions[i]!;
|
|
48
|
+
const name = s.info.sessionName
|
|
49
|
+
? `"${s.info.sessionName}"`
|
|
50
|
+
: pc.dim("(unnamed)");
|
|
51
|
+
const turns = `${s.info.turns} turn${s.info.turns !== 1 ? "s" : ""}`;
|
|
52
|
+
const ago = formatTimeAgo(s.info.lastActive);
|
|
53
|
+
const model = s.info.lastModel
|
|
54
|
+
? (coreResolveModel(s.info.lastModel)?.displayName ?? s.info.lastModel)
|
|
55
|
+
: "";
|
|
56
|
+
ctx.renderer.writeln(
|
|
57
|
+
` ${pc.green(String(i + 1))}. ${name} ${pc.dim(`${turns} · ${ago}${model ? ` · ${model}` : ""}`)}`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
ctx.renderer.writeln();
|
|
61
|
+
ctx.renderer.writeln(
|
|
62
|
+
` ${pc.dim("Enter number to resume (Esc to cancel):")}`,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const input = await ctx.waitForInput();
|
|
66
|
+
const num = parseInt(input, 10);
|
|
67
|
+
if (num >= 1 && num <= sessions.length) {
|
|
68
|
+
const selected = sessions[num - 1]!;
|
|
69
|
+
ctx.initNewChat(selected.chatId);
|
|
70
|
+
const name = selected.info.sessionName
|
|
71
|
+
? `"${selected.info.sessionName}"`
|
|
72
|
+
: `(${selected.info.turns} turns)`;
|
|
73
|
+
ctx.renderer.writeSystem(`Resumed: ${name}`);
|
|
74
|
+
} else {
|
|
75
|
+
ctx.renderer.writeSystem("Cancelled.");
|
|
76
|
+
}
|
|
77
|
+
ctx.reprompt();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const renameCommand: Command = {
|
|
82
|
+
name: "rename",
|
|
83
|
+
argHint: "[name]",
|
|
84
|
+
description: "Name the current session",
|
|
85
|
+
async handler(args, ctx) {
|
|
86
|
+
// Ensure session exists in store (auto-creates if needed)
|
|
87
|
+
getSession(ctx.chatId());
|
|
88
|
+
if (!args) {
|
|
89
|
+
const session = getSession(ctx.chatId());
|
|
90
|
+
ctx.renderer.writeSystem(
|
|
91
|
+
session.sessionName
|
|
92
|
+
? `Session name: "${session.sessionName}"`
|
|
93
|
+
: "Session has no name.",
|
|
94
|
+
);
|
|
95
|
+
} else {
|
|
96
|
+
setSessionName(ctx.chatId(), args);
|
|
97
|
+
ctx.renderer.writeSystem(`Session renamed to "${args}"`);
|
|
98
|
+
}
|
|
99
|
+
ctx.reprompt();
|
|
100
|
+
},
|
|
101
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/** `/status` — session stats, enriched from the backend when available. */
|
|
2
|
+
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import {
|
|
5
|
+
buildCacheDisplay,
|
|
6
|
+
buildContextDisplay,
|
|
7
|
+
buildPlanDisplay,
|
|
8
|
+
} from "../../shared/status-context.js";
|
|
9
|
+
import {
|
|
10
|
+
formatDuration,
|
|
11
|
+
formatTokenCount,
|
|
12
|
+
formatUsd,
|
|
13
|
+
} from "../../shared/format.js";
|
|
14
|
+
import { getChatSettings } from "../../../storage/chat-settings.js";
|
|
15
|
+
import { getSessionInfo } from "../../../storage/sessions.js";
|
|
16
|
+
import { getLoadedPlugins } from "../../../core/plugin/index.js";
|
|
17
|
+
import { getPooledBackend } from "../../../core/engine/backend-controller/index.js";
|
|
18
|
+
import type { Command, CommandContext } from "../command-registry.js";
|
|
19
|
+
|
|
20
|
+
type BackendRef = CommandContext["backend"];
|
|
21
|
+
type SessionInfo = ReturnType<typeof getSessionInfo>;
|
|
22
|
+
|
|
23
|
+
type UsageTotals = {
|
|
24
|
+
inputTokens: number;
|
|
25
|
+
outputTokens: number;
|
|
26
|
+
cacheRead: number;
|
|
27
|
+
cacheWrite: number;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Stored totals, replaced by the backend's live snapshot when available. */
|
|
31
|
+
async function liveUsageTotals(
|
|
32
|
+
be: BackendRef,
|
|
33
|
+
info: SessionInfo,
|
|
34
|
+
): Promise<UsageTotals> {
|
|
35
|
+
const u = info.usage;
|
|
36
|
+
let displayInputTokens = u.totalInputTokens;
|
|
37
|
+
let displayOutputTokens = u.totalOutputTokens;
|
|
38
|
+
let displayCacheRead = u.totalCacheRead;
|
|
39
|
+
let displayCacheWrite = u.totalCacheWrite;
|
|
40
|
+
|
|
41
|
+
// Enrich from backend when available
|
|
42
|
+
if (be?.usage?.getSessionSnapshot && info.sessionId) {
|
|
43
|
+
const snap = await be.usage
|
|
44
|
+
?.getSessionSnapshot(info.sessionId)
|
|
45
|
+
.catch(() => undefined);
|
|
46
|
+
if (snap) {
|
|
47
|
+
displayInputTokens = snap.inputTokens ?? displayInputTokens;
|
|
48
|
+
displayOutputTokens = snap.outputTokens ?? displayOutputTokens;
|
|
49
|
+
displayCacheRead = snap.cacheRead ?? displayCacheRead;
|
|
50
|
+
displayCacheWrite = snap.cacheWrite ?? displayCacheWrite;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
inputTokens: displayInputTokens,
|
|
55
|
+
outputTokens: displayOutputTokens,
|
|
56
|
+
cacheRead: displayCacheRead,
|
|
57
|
+
cacheWrite: displayCacheWrite,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The backend's model line, plus its context window if the store had none. */
|
|
62
|
+
async function backendModelSummary(
|
|
63
|
+
be: BackendRef,
|
|
64
|
+
activeModel: string,
|
|
65
|
+
contextWindow: number,
|
|
66
|
+
): Promise<{ line: string; contextWindow: number }> {
|
|
67
|
+
let backendModelLine = "";
|
|
68
|
+
if (be?.models?.getRawModelInfo) {
|
|
69
|
+
const modelInfo = await be.models
|
|
70
|
+
?.getRawModelInfo(activeModel)
|
|
71
|
+
.catch(() => undefined);
|
|
72
|
+
const label = be.label ?? "Backend";
|
|
73
|
+
if (modelInfo) {
|
|
74
|
+
backendModelLine = ` ${pc.bold(label)} ${modelInfo.displayName} · ${modelInfo.providerName}${modelInfo.free ? " · free" : ""}`;
|
|
75
|
+
if (modelInfo.contextWindow) {
|
|
76
|
+
contextWindow ||= modelInfo.contextWindow;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { line: backendModelLine, contextWindow };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function writeSessionAndContext(
|
|
84
|
+
ctx: CommandContext,
|
|
85
|
+
info: SessionInfo,
|
|
86
|
+
totals: UsageTotals,
|
|
87
|
+
cache: ReturnType<typeof buildCacheDisplay>,
|
|
88
|
+
contextWindow: number,
|
|
89
|
+
): void {
|
|
90
|
+
const u = info.usage;
|
|
91
|
+
const nameStr = info.sessionName ? `"${info.sessionName}" · ` : "";
|
|
92
|
+
const context = buildContextDisplay({
|
|
93
|
+
contextTokens: u.contextTokens,
|
|
94
|
+
lastPromptTokens: u.lastPromptTokens,
|
|
95
|
+
contextWindow,
|
|
96
|
+
});
|
|
97
|
+
const contextUsed = context.known
|
|
98
|
+
? formatTokenCount(context.used)
|
|
99
|
+
: "unknown";
|
|
100
|
+
const contextMax = context.max ? formatTokenCount(context.max) : "unknown";
|
|
101
|
+
const avgResponseMs =
|
|
102
|
+
info.turns > 0 && u.totalResponseMs
|
|
103
|
+
? Math.round(u.totalResponseMs / info.turns)
|
|
104
|
+
: 0;
|
|
105
|
+
const fastestResponseMs =
|
|
106
|
+
Number.isFinite(u.fastestResponseMs) && u.fastestResponseMs > 0
|
|
107
|
+
? u.fastestResponseMs
|
|
108
|
+
: 0;
|
|
109
|
+
|
|
110
|
+
ctx.renderer.writeln(
|
|
111
|
+
` ${pc.bold("Session")} ${nameStr}turns ${info.turns}${cache ? ` · ${cache.hitPct}% cache` : ""}`,
|
|
112
|
+
);
|
|
113
|
+
ctx.renderer.writeln(
|
|
114
|
+
` ${pc.dim(`in ${totals.inputTokens.toLocaleString()} · out ${totals.outputTokens.toLocaleString()} tokens`)}`,
|
|
115
|
+
);
|
|
116
|
+
ctx.renderer.writeln();
|
|
117
|
+
ctx.renderer.writeln(
|
|
118
|
+
` ${pc.bold("Context")} ${contextUsed} / ${contextMax} (${context.known ? `${context.pct}%` : "unknown"})${context.warn ? pc.yellow(" nearing limit") : ""}`,
|
|
119
|
+
);
|
|
120
|
+
ctx.renderer.writeln(
|
|
121
|
+
` ${context.warn ? pc.yellow(context.bar) : pc.dim(context.bar)}`,
|
|
122
|
+
);
|
|
123
|
+
ctx.renderer.writeln(
|
|
124
|
+
` ${pc.dim(`response last ${u.lastResponseMs ? formatDuration(u.lastResponseMs) : "—"} · avg ${avgResponseMs ? formatDuration(avgResponseMs) : "—"} · best ${fastestResponseMs ? formatDuration(fastestResponseMs) : "—"}`)}`,
|
|
125
|
+
);
|
|
126
|
+
if (u.estimatedCostUsd > 0) {
|
|
127
|
+
ctx.renderer.writeln(
|
|
128
|
+
` ${pc.dim(`estimated session cost ${formatUsd(u.estimatedCostUsd)}`)}`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function writePlan(ctx: CommandContext, be: BackendRef): Promise<void> {
|
|
134
|
+
const planSource = be?.usage?.getPlanUsage ? be : getPooledBackend("claude");
|
|
135
|
+
const plan = buildPlanDisplay(
|
|
136
|
+
await planSource?.usage?.getPlanUsage?.().catch(() => undefined),
|
|
137
|
+
);
|
|
138
|
+
if (plan) {
|
|
139
|
+
ctx.renderer.writeln();
|
|
140
|
+
ctx.renderer.writeln(
|
|
141
|
+
` ${pc.bold("Plan")}${plan.plan ? ` ${plan.plan}` : ""}${plan.ageLabel ? pc.dim(` (${plan.ageLabel})`) : ""}`,
|
|
142
|
+
);
|
|
143
|
+
for (const w of plan.windows) {
|
|
144
|
+
ctx.renderer.writeln(
|
|
145
|
+
` ${w.label.padEnd(6)}${pc.dim(w.bar)} ${String(w.percent).padStart(3)}%${w.resetLabel ? pc.dim(` reset ${w.resetLabel}`) : ""}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function writePlugins(ctx: CommandContext): void {
|
|
152
|
+
const plugins = getLoadedPlugins();
|
|
153
|
+
if (plugins.length > 0) {
|
|
154
|
+
ctx.renderer.writeln();
|
|
155
|
+
ctx.renderer.writeln(` ${pc.bold("Plugins")}`);
|
|
156
|
+
for (const p of plugins) {
|
|
157
|
+
const ver = p.plugin.version ? pc.dim(` v${p.plugin.version}`) : "";
|
|
158
|
+
const desc = p.plugin.description
|
|
159
|
+
? ` ${pc.dim(p.plugin.description)}`
|
|
160
|
+
: "";
|
|
161
|
+
const tools = p.plugin.mcpServerPath
|
|
162
|
+
? pc.green("mcp")
|
|
163
|
+
: pc.dim("actions only");
|
|
164
|
+
ctx.renderer.writeln(
|
|
165
|
+
` ${pc.green("●")} ${p.plugin.name}${ver} ${tools}${desc}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const statusCommand: Command = {
|
|
172
|
+
name: "status",
|
|
173
|
+
description: "Session stats",
|
|
174
|
+
async handler(_args, ctx) {
|
|
175
|
+
const info = getSessionInfo(ctx.chatId());
|
|
176
|
+
const be = ctx.backend;
|
|
177
|
+
const activeModel = getChatSettings(ctx.chatId()).model ?? ctx.config.model;
|
|
178
|
+
ctx.renderer.writeln();
|
|
179
|
+
|
|
180
|
+
const totals = await liveUsageTotals(be, info);
|
|
181
|
+
const cache = buildCacheDisplay({
|
|
182
|
+
cacheMetrics: be?.cacheMetrics,
|
|
183
|
+
inputTokens: totals.inputTokens,
|
|
184
|
+
cacheRead: totals.cacheRead,
|
|
185
|
+
cacheWrite: totals.cacheWrite,
|
|
186
|
+
});
|
|
187
|
+
const model = await backendModelSummary(
|
|
188
|
+
be,
|
|
189
|
+
activeModel,
|
|
190
|
+
info.usage.contextWindow,
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
writeSessionAndContext(ctx, info, totals, cache, model.contextWindow);
|
|
194
|
+
await writePlan(ctx, be);
|
|
195
|
+
if (model.line) {
|
|
196
|
+
ctx.renderer.writeln();
|
|
197
|
+
ctx.renderer.writeln(model.line);
|
|
198
|
+
}
|
|
199
|
+
writePlugins(ctx);
|
|
200
|
+
ctx.reprompt();
|
|
201
|
+
},
|
|
202
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Terminal command registry — the `Command` shape and the name/alias index
|
|
3
|
+
* `tryRunCommand` resolves against.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TalonConfig } from "../../util/config.js";
|
|
7
|
+
import type { Backend } from "../../core/agent-runtime/capabilities.js";
|
|
8
|
+
import type { Renderer } from "./renderer.js";
|
|
9
|
+
|
|
10
|
+
// ── Types ────────────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
export type CommandContext = {
|
|
13
|
+
/** Current chat ID (getter — may change on /resume). */
|
|
14
|
+
chatId: () => string;
|
|
15
|
+
config: TalonConfig;
|
|
16
|
+
renderer: Renderer;
|
|
17
|
+
reprompt: () => void;
|
|
18
|
+
initNewChat: (id?: string) => void;
|
|
19
|
+
waitForInput: () => Promise<string>;
|
|
20
|
+
/** Close the terminal (for /quit). */
|
|
21
|
+
close: () => void;
|
|
22
|
+
/** AI backend (available after bootstrap). */
|
|
23
|
+
backend?: Backend | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CommandHandler = (args: string, ctx: CommandContext) => Promise<void>;
|
|
27
|
+
|
|
28
|
+
export type Command = {
|
|
29
|
+
name: string;
|
|
30
|
+
aliases?: string[];
|
|
31
|
+
argHint?: string;
|
|
32
|
+
description: string;
|
|
33
|
+
handler: CommandHandler;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ── Registry ─────────────────────────────────────────────────────────────────
|
|
37
|
+
|
|
38
|
+
const commands: Command[] = [];
|
|
39
|
+
const nameIndex = new Map<string, Command>();
|
|
40
|
+
|
|
41
|
+
export function registerCommand(cmd: Command): void {
|
|
42
|
+
commands.push(cmd);
|
|
43
|
+
nameIndex.set(cmd.name, cmd);
|
|
44
|
+
if (cmd.aliases) {
|
|
45
|
+
for (const alias of cmd.aliases) {
|
|
46
|
+
nameIndex.set(alias, cmd);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Try to run a slash command. Returns true if handled, false if not a command. */
|
|
52
|
+
export async function tryRunCommand(
|
|
53
|
+
text: string,
|
|
54
|
+
ctx: CommandContext,
|
|
55
|
+
): Promise<boolean> {
|
|
56
|
+
if (!text.startsWith("/")) return false;
|
|
57
|
+
|
|
58
|
+
const spaceIdx = text.indexOf(" ");
|
|
59
|
+
const cmdName = (spaceIdx === -1 ? text : text.slice(0, spaceIdx))
|
|
60
|
+
.slice(1)
|
|
61
|
+
.toLowerCase();
|
|
62
|
+
const args = spaceIdx === -1 ? "" : text.slice(spaceIdx + 1).trim();
|
|
63
|
+
|
|
64
|
+
const cmd = nameIndex.get(cmdName);
|
|
65
|
+
if (!cmd) return false;
|
|
66
|
+
|
|
67
|
+
await cmd.handler(args, ctx);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Get all registered commands (for /help rendering). */
|
|
72
|
+
export function getCommands(): readonly Command[] {
|
|
73
|
+
return commands;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Clear all registered commands (for testing). */
|
|
77
|
+
export function clearCommands(): void {
|
|
78
|
+
commands.length = 0;
|
|
79
|
+
nameIndex.clear();
|
|
80
|
+
}
|