@selesai/code 0.11.0 → 0.13.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/cli/startup-ui.js +2 -1
  3. package/dist/core/agent-session.d.ts +12 -1
  4. package/dist/core/agent-session.js +77 -9
  5. package/dist/core/compaction/branch-summarization.js +4 -3
  6. package/dist/core/compaction/compaction.d.ts +5 -0
  7. package/dist/core/compaction/compaction.js +19 -4
  8. package/dist/core/extensions/index.d.ts +1 -1
  9. package/dist/core/extensions/runner.d.ts +5 -0
  10. package/dist/core/extensions/runner.js +48 -1
  11. package/dist/core/extensions/types.d.ts +18 -1
  12. package/dist/core/llama/client.d.ts +6 -0
  13. package/dist/core/llama/client.js +7 -0
  14. package/dist/core/llama/provider.d.ts +3 -1
  15. package/dist/core/llama/provider.js +28 -6
  16. package/dist/core/session-manager.js +4 -2
  17. package/dist/core/settings-manager.d.ts +8 -1
  18. package/dist/core/settings-manager.js +17 -0
  19. package/dist/core/usage-totals.d.ts +2 -0
  20. package/dist/core/usage-totals.js +10 -0
  21. package/dist/defaults/models.json +32 -9
  22. package/dist/extensions/pi-zentui/extensions/zentui/format.ts +2 -1
  23. package/dist/extensions/pi-zentui/test/format.test.ts +15 -1
  24. package/dist/extensions/tokenin-onboarding.ts +112 -2
  25. package/dist/index.d.ts +2 -1
  26. package/dist/index.js +1 -0
  27. package/dist/main.js +2 -0
  28. package/dist/modes/interactive/components/footer.js +2 -2
  29. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  30. package/dist/modes/interactive/components/settings-selector.js +10 -0
  31. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  32. package/dist/modes/interactive/interactive-mode.js +59 -43
  33. package/dist/modes/rpc/rpc-client.d.ts +7 -0
  34. package/dist/modes/rpc/rpc-client.js +7 -0
  35. package/dist/modes/rpc/rpc-mode.js +3 -0
  36. package/dist/modes/rpc/rpc-types.d.ts +12 -0
  37. package/dist/utils/shell.js +6 -4
  38. package/docs/settings.md +4 -0
  39. package/package.json +4 -4
@@ -1,6 +1,6 @@
1
1
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import type { Transport } from "@earendil-works/pi-ai";
3
- import type { ScrollViewScrollbar, TuiMode as RendererTuiMode } from "@earendil-works/pi-tui";
3
+ import type { ScrollViewScrollbar, TerminalCapabilities, TuiMode as RendererTuiMode } from "@earendil-works/pi-tui";
4
4
  export interface CompactionSettings {
5
5
  enabled?: boolean;
6
6
  reserveTokens?: number;
@@ -28,6 +28,9 @@ export interface TerminalSettings {
28
28
  imageWidthCells?: number;
29
29
  clearOnShrink?: boolean;
30
30
  showTerminalProgress?: boolean;
31
+ hyperlinks?: boolean | "auto";
32
+ images?: "kitty" | "iterm2" | "auto" | false;
33
+ trueColor?: boolean | "auto";
31
34
  }
32
35
  export interface ImageSettings {
33
36
  autoResize?: boolean;
@@ -104,6 +107,7 @@ export interface Settings {
104
107
  defaultTools?: string[];
105
108
  fullscreenExitOutput?: FullscreenExitOutput;
106
109
  fullscreenScrollbar?: ScrollViewScrollbar;
110
+ fullscreenCopyOnSelect?: boolean;
107
111
  images?: ImageSettings;
108
112
  enabledModels?: string[];
109
113
  doubleEscapeAction?: "fork" | "tree" | "none";
@@ -285,6 +289,7 @@ export declare class SettingsManager {
285
289
  setThemePaths(paths: string[]): void;
286
290
  setProjectThemePaths(paths: string[]): void;
287
291
  getThinkingBudgets(): ThinkingBudgetsSettings | undefined;
292
+ getTerminalCapabilityOverrides(): Partial<TerminalCapabilities>;
288
293
  getShowImages(): boolean;
289
294
  setShowImages(show: boolean): void;
290
295
  getImageWidthCells(): number;
@@ -299,6 +304,8 @@ export declare class SettingsManager {
299
304
  setFullscreenScrollbar(mode: ScrollViewScrollbar): void;
300
305
  getFullscreenExitOutput(): FullscreenExitOutput;
301
306
  setFullscreenExitOutput(output: FullscreenExitOutput): void;
307
+ getFullscreenCopyOnSelect(): boolean;
308
+ setFullscreenCopyOnSelect(enabled: boolean): void;
302
309
  getImageAutoResize(): boolean;
303
310
  setImageAutoResize(enabled: boolean): void;
304
311
  getBlockImages(): boolean;
@@ -786,6 +786,15 @@ export class SettingsManager {
786
786
  getThinkingBudgets() {
787
787
  return this.settings.thinkingBudgets;
788
788
  }
789
+ getTerminalCapabilityOverrides() {
790
+ const terminal = this.settings.terminal;
791
+ const images = terminal?.images;
792
+ return {
793
+ ...(images === "kitty" || images === "iterm2" ? { images } : images === false ? { images: null } : {}),
794
+ ...(typeof terminal?.trueColor === "boolean" ? { trueColor: terminal.trueColor } : {}),
795
+ ...(typeof terminal?.hyperlinks === "boolean" ? { hyperlinks: terminal.hyperlinks } : {}),
796
+ };
797
+ }
789
798
  getShowImages() {
790
799
  return this.settings.terminal?.showImages ?? true;
791
800
  }
@@ -864,6 +873,14 @@ export class SettingsManager {
864
873
  this.markModified("fullscreenExitOutput");
865
874
  this.save();
866
875
  }
876
+ getFullscreenCopyOnSelect() {
877
+ return this.settings.fullscreenCopyOnSelect ?? true;
878
+ }
879
+ setFullscreenCopyOnSelect(enabled) {
880
+ this.globalSettings.fullscreenCopyOnSelect = enabled;
881
+ this.markModified("fullscreenCopyOnSelect");
882
+ this.save();
883
+ }
867
884
  getImageAutoResize() {
868
885
  return this.settings.images?.autoResize ?? true;
869
886
  }
@@ -1,5 +1,7 @@
1
1
  import type { Usage } from "@earendil-works/pi-ai/compat";
2
2
  import type { SessionEntry } from "./session-manager.ts";
3
+ /** Format a USD cost with enough precision that sub-cent per-call costs don't round to $0.000. */
4
+ export declare function formatCost(cost: number): string;
3
5
  export interface UsageTotals {
4
6
  input: number;
5
7
  output: number;
@@ -1,3 +1,13 @@
1
+ /** Format a USD cost with enough precision that sub-cent per-call costs don't round to $0.000. */
2
+ export function formatCost(cost) {
3
+ if (cost === 0)
4
+ return "$0.000";
5
+ if (cost >= 1)
6
+ return `$${cost.toFixed(3)}`;
7
+ if (cost >= 0.01)
8
+ return `$${cost.toFixed(4)}`;
9
+ return `$${cost.toFixed(6)}`;
10
+ }
1
11
  export function createUsageTotals() {
2
12
  return {
3
13
  input: 0,
@@ -24,7 +24,10 @@
24
24
  "high": "high",
25
25
  "xhigh": null,
26
26
  "max": "max"
27
- }
27
+ },
28
+ "compat": {
29
+ "requiresReasoningContentOnAssistantMessages": true
30
+ }
28
31
  },
29
32
  {
30
33
  "id": "celestial-max",
@@ -40,7 +43,10 @@
40
43
  "high": "high",
41
44
  "xhigh": null,
42
45
  "max": "max"
43
- }
46
+ },
47
+ "compat": {
48
+ "requiresReasoningContentOnAssistantMessages": true
49
+ }
44
50
  },
45
51
  {
46
52
  "id": "celestial-ultra",
@@ -56,7 +62,10 @@
56
62
  "high": "high",
57
63
  "xhigh": null,
58
64
  "max": "max"
59
- }
65
+ },
66
+ "compat": {
67
+ "requiresReasoningContentOnAssistantMessages": true
68
+ }
60
69
  },
61
70
  {
62
71
  "id": "deepseek-v4-flash",
@@ -72,7 +81,10 @@
72
81
  "high": "high",
73
82
  "xhigh": null,
74
83
  "max": "max"
75
- }
84
+ },
85
+ "compat": {
86
+ "requiresReasoningContentOnAssistantMessages": true
87
+ }
76
88
  },
77
89
  {
78
90
  "id": "deepseek-v4-flash-vision-exp",
@@ -88,7 +100,10 @@
88
100
  "high": "high",
89
101
  "xhigh": null,
90
102
  "max": "max"
91
- }
103
+ },
104
+ "compat": {
105
+ "requiresReasoningContentOnAssistantMessages": true
106
+ }
92
107
  },
93
108
  {
94
109
  "id": "deepseek-v4-pro",
@@ -104,7 +119,10 @@
104
119
  "high": "high",
105
120
  "xhigh": null,
106
121
  "max": "max"
107
- }
122
+ },
123
+ "compat": {
124
+ "requiresReasoningContentOnAssistantMessages": true
125
+ }
108
126
  },
109
127
  {
110
128
  "id": "gpt-5.6-luna",
@@ -200,7 +218,10 @@
200
218
  "reasoning": false,
201
219
  "input": ["text", "image"],
202
220
  "contextWindow": 256000,
203
- "maxTokens": 64000
221
+ "maxTokens": 64000,
222
+ "compat": {
223
+ "thinkingFormat": "qwen"
224
+ }
204
225
  },
205
226
  {
206
227
  "id": "gemini-3.7-flash",
@@ -219,7 +240,8 @@
219
240
  "maxTokens": 8192,
220
241
  "compat": {
221
242
  "supportsDeveloperRole": false,
222
- "supportsReasoningEffort": false
243
+ "supportsReasoningEffort": false,
244
+ "thinkingFormat": "qwen"
223
245
  }
224
246
  },
225
247
  {
@@ -231,7 +253,8 @@
231
253
  "maxTokens": 8192,
232
254
  "compat": {
233
255
  "supportsDeveloperRole": false,
234
- "supportsReasoningEffort": false
256
+ "supportsReasoningEffort": false,
257
+ "thinkingFormat": "qwen"
235
258
  }
236
259
  }
237
260
  ]
@@ -1,5 +1,6 @@
1
1
  import { homedir, hostname, userInfo } from "node:os";
2
2
  import type { ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
3
+ import { formatCost } from "../../../../core/usage-totals.ts";
3
4
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
4
5
  import type {
5
6
  ColorSource,
@@ -360,7 +361,7 @@ export function buildTokenLabel(totals: UsageTotals, cacheHitIcon = "󰆼"): str
360
361
  }
361
362
 
362
363
  export function buildCostLabel(totals: UsageTotals): string {
363
- return `$${totals.cost.toFixed(3)}`;
364
+ return formatCost(totals.cost);
364
365
  }
365
366
 
366
367
  export function buildSessionDurationLabel(startEpoch: number): string {
@@ -134,7 +134,7 @@ describe("usage formatting", () => {
134
134
  const totals = getUsageTotals(ctx as never);
135
135
 
136
136
  expect(totals.cost).toBe(0.0123);
137
- expect(buildCostLabel(totals)).toBe("$0.012");
137
+ expect(buildCostLabel(totals)).toBe("$0.0123");
138
138
  });
139
139
 
140
140
  it("keeps the rate-card estimate when no reconcile entry exists", () => {
@@ -162,6 +162,20 @@ describe("usage formatting", () => {
162
162
  expect(totals.cost).toBe(0.0123);
163
163
  });
164
164
 
165
+ it("shows sub-cent reconciled costs without rounding to zero", () => {
166
+ const assistant = makeMessageUsageEntry("assistant", makeUsage(1_000, 500, 0), {
167
+ provider: "tokenin",
168
+ responseId: "chatcmpl-873",
169
+ });
170
+ const reconcile = makeReconcileEntry("tokenin", "chatcmpl-873", 0.00033601652);
171
+ const ctx = makeSessionContext([assistant, reconcile]);
172
+
173
+ const totals = getUsageTotals(ctx as never);
174
+
175
+ expect(totals.cost).toBe(0.00033601652);
176
+ expect(buildCostLabel(totals)).toBe("$0.000336");
177
+ });
178
+
165
179
  it("formats cache atoms independently and omits zero totals", () => {
166
180
  expect(buildCacheReadLabel(1_200)).toBe("R1.2k");
167
181
  expect(buildCacheWriteLabel(300)).toBe("W300");
@@ -16,6 +16,7 @@ import type { AuthStorage, ExtensionAPI, ExtensionCommandContext, ExtensionConte
16
16
 
17
17
  export const TOKEN_IN_PROVIDER = "tokenin";
18
18
  export const TOKEN_IN_DASHBOARD_URL = "https://token.selesai.in/dashboard/tokens";
19
+ export const TOKEN_IN_DEFAULT_BASE_URL = "https://lite.andlet.me/v1";
19
20
  export const ONBOARDING_MARKER_NAME = ".tokenInOnboardingComplete";
20
21
  export const PLACEHOLDER_API_KEY = "sk-xxx";
21
22
 
@@ -239,6 +240,93 @@ async function promptForToken(ctx: ExtensionContext): Promise<string | undefined
239
240
  return ctx.ui.input("Paste your Token-In token", "sk-...");
240
241
  }
241
242
 
243
+ // ---------------------------------------------------------------------------
244
+ // /tokenin usage — LiteLLM /key/info quota lookup
245
+ // ---------------------------------------------------------------------------
246
+
247
+ export interface TokenInUsage {
248
+ spend: number;
249
+ maxBudget?: number;
250
+ budgetResetAt?: string;
251
+ }
252
+
253
+ /** Parse a LiteLLM /key/info payload into spend/budget/reset. Exported for tests. */
254
+ export function parseTokenInUsage(payload: unknown): TokenInUsage | undefined {
255
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
256
+ const obj = payload as Record<string, unknown>;
257
+ const info =
258
+ obj.info && typeof obj.info === "object" && !Array.isArray(obj.info)
259
+ ? (obj.info as Record<string, unknown>)
260
+ : obj;
261
+ const spend = Number(info.spend);
262
+ if (!Number.isFinite(spend) || spend < 0) return undefined;
263
+ const usage: TokenInUsage = { spend };
264
+ const maxBudget = Number(info.max_budget);
265
+ if (Number.isFinite(maxBudget) && maxBudget > 0) usage.maxBudget = maxBudget;
266
+ if (typeof info.budget_reset_at === "string") usage.budgetResetAt = info.budget_reset_at;
267
+ return usage;
268
+ }
269
+
270
+ /** Format usage as a boxed multi-line display with a progress bar. Exported for tests. */
271
+ export function formatTokenInUsage(usage: TokenInUsage): string {
272
+ const money = (v: number) => `$${v.toFixed(2)}`;
273
+ const pct = usage.maxBudget !== undefined ? (usage.spend / usage.maxBudget) * 100 : 0;
274
+ const barWidth = 40;
275
+ const filled = usage.maxBudget !== undefined ? Math.min(barWidth, Math.round((pct / 100) * barWidth)) : 0;
276
+ const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
277
+ const pctText = usage.maxBudget !== undefined ? `${pct.toFixed(1)}%` : "—";
278
+
279
+ let summary = `Spent ${money(usage.spend)}`;
280
+ if (usage.maxBudget !== undefined) {
281
+ summary += ` of ${money(usage.maxBudget)} budget (${pct.toFixed(1)}%)`;
282
+ } else {
283
+ summary += " (no budget limit set)";
284
+ }
285
+
286
+ let footer = usage.maxBudget !== undefined ? `${money(Math.max(0, usage.maxBudget - usage.spend))} remaining` : "";
287
+ if (usage.budgetResetAt) {
288
+ const d = new Date(usage.budgetResetAt);
289
+ if (!Number.isNaN(d.getTime())) {
290
+ const ms = d.getTime() - Date.now();
291
+ if (ms > 0) {
292
+ const days = Math.floor(ms / 86_400_000);
293
+ const hours = Math.floor((ms % 86_400_000) / 3_600_000);
294
+ const reset = days > 0 ? `${days}d ${hours}h` : `${Math.max(1, Math.ceil(ms / 3_600_000))}h`;
295
+ footer += footer ? ` · resets in ${reset}` : `resets in ${reset}`;
296
+ }
297
+ }
298
+ }
299
+
300
+ const W = 50; // total box width
301
+ const inner = W - 4; // content width between │ padding
302
+ const pad = (s: string) => s + " ".repeat(Math.max(0, inner - s.length));
303
+ const top = `╭─ Token-In usage ${"─".repeat(W - 19)}╮`;
304
+ const bottom = `╰${"─".repeat(W - 2)}╯`;
305
+ return [
306
+ top,
307
+ `│ ${pad(summary)} │`,
308
+ `│ ${bar} ${pctText.padStart(5)} │`,
309
+ `│ ${pad(footer)} │`,
310
+ bottom,
311
+ ].join("\n");
312
+ }
313
+
314
+ /** Query LiteLLM /key/info at the proxy root with the account key. Exported for tests. */
315
+ export async function fetchTokenInUsage(apiKey: string, baseUrl: string): Promise<TokenInUsage> {
316
+ const root = baseUrl.replace(/\/v1\/?$/, "");
317
+ const response = await fetch(`${root}/key/info`, {
318
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" },
319
+ signal: AbortSignal.timeout(5_000),
320
+ });
321
+ if (!response.ok) {
322
+ throw new Error(`Token-In usage endpoint returned HTTP ${response.status}`);
323
+ }
324
+ const payload: unknown = await response.json();
325
+ const usage = parseTokenInUsage(payload);
326
+ if (!usage) throw new Error("Token-In usage endpoint returned an unexpected payload");
327
+ return usage;
328
+ }
329
+
242
330
  // ---------------------------------------------------------------------------
243
331
  // /tokenin subcommand handlers
244
332
  // ---------------------------------------------------------------------------
@@ -327,6 +415,25 @@ async function handleTokenInSwitch(ctx: ExtensionCommandContext): Promise<void>
327
415
  ctx.ui.notify(`Switched to account ${account.label}.`, "info");
328
416
  }
329
417
 
418
+ async function handleTokenInUsage(ctx: ExtensionCommandContext): Promise<void> {
419
+ const apiKey = getStoredTokenInApiKey(ctx.modelRegistry.authStorage);
420
+ if (!apiKey) {
421
+ ctx.ui.notify("No active Token-In token. Use /tokenin add first.", "warning");
422
+ return;
423
+ }
424
+ const auth = readTokenInAuth();
425
+ const activeId = getActiveTokenInAccountId(ctx.modelRegistry.authStorage);
426
+ const account = auth.accounts.find((a) => a.id === activeId);
427
+ const baseUrl = account?.baseUrl ?? getTokenInBaseUrl(readModelsJson()) ?? TOKEN_IN_DEFAULT_BASE_URL;
428
+ try {
429
+ const usage = await fetchTokenInUsage(apiKey, baseUrl);
430
+ ctx.ui.notify(formatTokenInUsage(usage), "info");
431
+ } catch (err) {
432
+ const message = err instanceof Error ? err.message : String(err);
433
+ ctx.ui.notify(`Could not fetch Token-In usage: ${message}`, "warning");
434
+ }
435
+ }
436
+
330
437
  async function handleTokenInRemove(ctx: ExtensionCommandContext): Promise<void> {
331
438
  if (!ctx.hasUI) {
332
439
  ctx.ui.notify("This command requires interactive mode.", "warning");
@@ -441,12 +548,13 @@ export default function tokenInOnboardingExtension(pi: ExtensionAPI): void {
441
548
  });
442
549
 
443
550
  pi.registerCommand("tokenin", {
444
- description: "Manage TokenIN accounts: /tokenin add|switch|remove",
551
+ description: "Manage TokenIN accounts: /tokenin add|switch|remove|usage",
445
552
  getArgumentCompletions: (prefix: string) => {
446
553
  const subcommands = [
447
554
  { name: "add", description: "Add a TokenIN account" },
448
555
  { name: "switch", description: "Switch active account" },
449
556
  { name: "remove", description: "Remove a saved account" },
557
+ { name: "usage", description: "Show Token-IN quota usage" },
450
558
  ];
451
559
  const lowerPrefix = prefix.toLowerCase();
452
560
  const items = subcommands.filter((s) => s.name.toLowerCase().startsWith(lowerPrefix));
@@ -463,8 +571,10 @@ export default function tokenInOnboardingExtension(pi: ExtensionAPI): void {
463
571
  await handleTokenInSwitch(ctx);
464
572
  } else if (sub === "remove") {
465
573
  await handleTokenInRemove(ctx);
574
+ } else if (sub === "usage") {
575
+ await handleTokenInUsage(ctx);
466
576
  } else {
467
- ctx.ui.notify("Usage: /tokenin add|switch|remove", "info");
577
+ ctx.ui.notify("Usage: /tokenin add|switch|remove|usage", "info");
468
578
  }
469
579
  },
470
580
  });
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { readStoredCredential } from "./core/auth-storage.ts";
5
5
  export { ensureTool, getToolPath, type ManagedTool } from "./utils/tools-manager.ts";
6
6
  export { type BranchPreparation, type BranchSummaryResult, type CollectEntriesResult, type CompactionResult, type CutPointResult, calculateContextTokens, collectEntriesForBranchSummary, compact, DEFAULT_COMPACTION_SETTINGS, estimateTokens, type FileOperations, findCutPoint, findTurnStartIndex, type GenerateBranchSummaryOptions, generateBranchSummary, generateSummary, generateSummaryWithUsage, getLastAssistantUsage, prepareBranchEntries, serializeConversation, shouldCompact, } from "./core/compaction/index.ts";
7
7
  export { createEventBus, type EventBus, type EventBusController } from "./core/event-bus.ts";
8
- export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextUsage, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, SessionBeforeCompactEvent, SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionBeforeTreeEvent, SessionCompactEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, TurnEndEvent, TurnStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts";
8
+ export type { AgentEndEvent, AgentSettledEvent, AgentStartEvent, AgentToolResult, AgentToolUpdateCallback, AppKeybinding, AutocompleteProviderFactory, BashToolCallEvent, BeforeAgentStartEvent, BeforeAgentStartEventResult, BeforeProviderHeadersEvent, BeforeProviderRequestEvent, BeforeProviderRequestEventResult, BuildSystemPromptOptions, CompactOptions, ContextEvent, ContextUsage, CustomToolCallEvent, EditToolCallEvent, EntryRenderer, EntryRenderOptions, ExecOptions, ExecResult, Extension, ExtensionActions, ExtensionAPI, ExtensionCommandContext, ExtensionCommandContextActions, ExtensionContext, ExtensionContextActions, ExtensionError, ExtensionEvent, ExtensionFactory, ExtensionFlag, ExtensionHandler, ExtensionRuntime, ExtensionShortcut, ExtensionUIContext, ExtensionUIDialogOptions, ExtensionWidgetOptions, FindToolCallEvent, GrepToolCallEvent, InlineExtension, InputEvent, InputEventResult, InputSource, KeybindingsManager, LoadExtensionsResult, LsToolCallEvent, MessageEndEvent, MessageRenderer, MessageRenderOptions, MessageStartEvent, MessageUpdateEvent, ProjectTrustContext, ProjectTrustEvent, ProjectTrustEventDecision, ProjectTrustEventResult, ProjectTrustHandler, ProviderConfig, ProviderModelConfig, ReadToolCallEvent, RegisteredCommand, RegisteredTool, ResolvedCommand, SessionBeforeCompactEvent, SessionBeforeForkEvent, SessionBeforeSwitchEvent, SessionBeforeTreeEvent, SessionCompactEvent, SessionInfoChangedEvent, SessionShutdownEvent, SessionStartEvent, SessionTreeEvent, SlashCommandInfo, SlashCommandSource, SourceInfo, TerminalInputHandler, ToolCallEvent, ToolCallEventResult, ToolDefinition, ToolExecutionEndEvent, ToolExecutionMode, ToolExecutionStartEvent, ToolExecutionUpdateEvent, ToolInfo, ToolRenderResultOptions, ToolResultEvent, TurnEndEvent, TurnStartEvent, UIPromptEndEvent, UIPromptKind, UIPromptStartEvent, UserBashEvent, UserBashEventResult, WidgetPlacement, WorkingIndicatorOptions, WriteToolCallEvent, } from "./core/extensions/index.ts";
9
9
  export { createExtensionRuntime, defineTool, discoverAndLoadExtensions, ExtensionRunner, isBashToolResult, isEditToolResult, isFindToolResult, isGrepToolResult, isLsToolResult, isReadToolResult, isToolCallEventType, isWriteToolResult, wrapRegisteredTool, wrapRegisteredTools, } from "./core/extensions/index.ts";
10
10
  export type { ReadonlyFooterDataProvider } from "./core/footer-data-provider.ts";
11
11
  export { convertToLlm } from "./core/messages.ts";
@@ -32,4 +32,5 @@ export { copyToClipboard } from "./utils/clipboard.ts";
32
32
  export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.ts";
33
33
  export { convertToPng } from "./utils/image-convert.ts";
34
34
  export { formatDimensionNote, type ResizedImage, resizeImage } from "./utils/image-resize.ts";
35
+ export { detectSupportedImageMimeTypeFromFile } from "./utils/mime.ts";
35
36
  export { getShellConfig } from "./utils/shell.ts";
package/dist/index.js CHANGED
@@ -43,5 +43,6 @@ export { copyToClipboard } from "./utils/clipboard.js";
43
43
  export { parseFrontmatter, stripFrontmatter } from "./utils/frontmatter.js";
44
44
  export { convertToPng } from "./utils/image-convert.js";
45
45
  export { formatDimensionNote, resizeImage } from "./utils/image-resize.js";
46
+ export { detectSupportedImageMimeTypeFromFile } from "./utils/mime.js";
46
47
  // Shell utilities
47
48
  export { getShellConfig } from "./utils/shell.js";
package/dist/main.js CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
  import { createInterface } from "node:readline";
8
8
  import { modelsAreEqual } from "@earendil-works/pi-ai";
9
+ import { setCapabilityOverrides } from "@earendil-works/pi-tui";
9
10
  import chalk from "chalk";
10
11
  import { parseArgs, printHelp } from "./cli/args.js";
11
12
  import { CredentialPrintError, isCredentialPrintHelp, parseCredentialPrintCommand, printCredentialPrintHelp, resolveCredentialForPrint, validateCredentialPrintArgs, } from "./cli/credential-print.js";
@@ -659,6 +660,7 @@ export async function main(args, options) {
659
660
  time("createAgentSessionRuntime");
660
661
  const { services, session, modelFallbackMessage } = runtime;
661
662
  const { settingsManager, modelRuntime, resourceLoader } = services;
663
+ setCapabilityOverrides(settingsManager.getTerminalCapabilityOverrides());
662
664
  applyHttpProxySettings(settingsManager.getGlobalSettings().httpProxy);
663
665
  configureHttpDispatcher(settingsManager.getHttpIdleTimeoutMs());
664
666
  if (parsed.help) {
@@ -1,7 +1,7 @@
1
1
  import { isAbsolute, relative, resolve, sep } from "node:path";
2
2
  import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
3
  import { areExperimentalFeaturesEnabled } from "../../../core/experimental.js";
4
- import { addUsageToTotals, createUsageTotals } from "../../../core/usage-totals.js";
4
+ import { addUsageToTotals, createUsageTotals, formatCost } from "../../../core/usage-totals.js";
5
5
  import { theme } from "../theme/theme.js";
6
6
  /**
7
7
  * Sanitize text for display in a single-line status.
@@ -127,7 +127,7 @@ export class FooterComponent {
127
127
  ? state.model.provider === "kimi-coding" || this.session.modelRuntime.isUsingOAuth(state.model.provider)
128
128
  : false;
129
129
  if (usageTotals.cost || usingSubscription) {
130
- const costStr = `$${usageTotals.cost.toFixed(3)}${usingSubscription ? " (sub)" : ""}`;
130
+ const costStr = `${formatCost(usageTotals.cost)}${usingSubscription ? " (sub)" : ""}`;
131
131
  statsParts.push(costStr);
132
132
  }
133
133
  // Colorize context percentage based on usage
@@ -49,6 +49,7 @@ export interface SettingsConfig {
49
49
  tuiMode: TuiMode;
50
50
  fullscreenExitOutput: FullscreenExitOutput;
51
51
  fullscreenScrollbar: ScrollViewScrollbar;
52
+ fullscreenCopyOnSelect: boolean;
52
53
  warnings: WarningSettings;
53
54
  skills: SkillToggleItem[];
54
55
  }
@@ -86,6 +87,7 @@ export interface SettingsCallbacks {
86
87
  onTuiModeChange: (mode: TuiMode) => void;
87
88
  onFullscreenExitOutputChange: (output: FullscreenExitOutput) => void;
88
89
  onFullscreenScrollbarChange: (mode: ScrollViewScrollbar) => void;
90
+ onFullscreenCopyOnSelectChange: (enabled: boolean) => void;
89
91
  onWarningsChange: (warnings: WarningSettings) => void;
90
92
  onSkillsToggle: (item: SkillToggleItem, enabled: boolean) => void;
91
93
  onSkillsReload: () => void;
@@ -474,6 +474,13 @@ export class SettingsSelectorComponent extends Container {
474
474
  currentValue: config.fullscreenScrollbar,
475
475
  values: ["auto", "always", "hidden"],
476
476
  },
477
+ {
478
+ id: "fullscreen-copy-on-select",
479
+ label: "Fullscreen copy on select",
480
+ description: "Automatically copy selected text in fullscreen mode; disable to copy selections with Ctrl+X",
481
+ currentValue: config.fullscreenCopyOnSelect ? "true" : "false",
482
+ values: ["true", "false"],
483
+ },
477
484
  {
478
485
  id: "theme",
479
486
  label: "Theme",
@@ -697,6 +704,9 @@ export class SettingsSelectorComponent extends Container {
697
704
  case "fullscreen-scrollbar":
698
705
  callbacks.onFullscreenScrollbarChange(newValue);
699
706
  break;
707
+ case "fullscreen-copy-on-select":
708
+ callbacks.onFullscreenCopyOnSelectChange(newValue === "true");
709
+ break;
700
710
  case "theme":
701
711
  callbacks.onThemeChange(newValue);
702
712
  break;
@@ -43,6 +43,7 @@ interface InteractiveTuiOptions {
43
43
  logDirectory: string;
44
44
  terminal?: Terminal;
45
45
  onRightClickPaste?: () => void;
46
+ fullscreenCopyOnSelect?: boolean;
46
47
  }
47
48
  /** Composition root for selecting the interactive terminal renderer. */
48
49
  export declare function createInteractiveTui(options: InteractiveTuiOptions): TuiMainScreen | TuiAltScreen;
@@ -201,6 +202,7 @@ export declare class InteractiveMode {
201
202
  private setExtensionStatus;
202
203
  private showStatusIndicator;
203
204
  private clearStatusIndicator;
205
+ private showWorkingStatusIndicator;
204
206
  private setWorkingVisible;
205
207
  private setWorkingIndicator;
206
208
  private setHiddenThinkingLabel;
@@ -346,6 +348,8 @@ export declare class InteractiveMode {
346
348
  private cycleModel;
347
349
  private toggleToolOutputExpansion;
348
350
  private setToolsExpanded;
351
+ /** Update rendered assistant messages without rebuilding live tool components. */
352
+ private updateThinkingBlockVisibility;
349
353
  private toggleThinkingBlockVisibility;
350
354
  private handleOpenExternalEditor;
351
355
  clearEditor(): void;