@selesai/code 0.8.7 → 0.8.8

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 (51) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/dist/cli/args.d.ts +1 -0
  3. package/dist/cli/args.js +11 -0
  4. package/dist/cli/args.test.d.ts +1 -0
  5. package/dist/cli/args.test.js +24 -0
  6. package/dist/core/agent-session.d.ts +4 -1
  7. package/dist/core/agent-session.js +5 -6
  8. package/dist/core/experimental.d.ts +4 -0
  9. package/dist/core/experimental.js +4 -0
  10. package/dist/core/extensions/types.d.ts +4 -0
  11. package/dist/core/keybindings.d.ts +24 -0
  12. package/dist/core/model-config.d.ts +5 -0
  13. package/dist/core/model-config.js +1 -0
  14. package/dist/core/project-trust.js +2 -2
  15. package/dist/core/sdk.d.ts +5 -3
  16. package/dist/core/sdk.js +2 -1
  17. package/dist/core/session-manager.js +2 -2
  18. package/dist/core/settings-manager.d.ts +6 -0
  19. package/dist/core/settings-manager.js +12 -0
  20. package/dist/core/system-prompt.js +1 -1
  21. package/dist/core/tools/bash.js +2 -0
  22. package/dist/core/tools/edit.js +2 -0
  23. package/dist/core/tools/find.js +1 -1
  24. package/dist/core/tools/grep.js +1 -1
  25. package/dist/core/tools/read.js +2 -0
  26. package/dist/core/tools/write.js +2 -0
  27. package/dist/extensions/pi-subagents/src/runs/background/result-watcher.ts +0 -18
  28. package/dist/extensions/pi-subagents/test/integration/result-watcher.test.ts +26 -0
  29. package/dist/extensions/tokenin-onboarding.ts +1 -1
  30. package/dist/index.d.ts +1 -1
  31. package/dist/main.js +4 -0
  32. package/dist/modes/interactive/components/model-selector.js +2 -1
  33. package/dist/modes/interactive/components/settings-selector.d.ts +3 -1
  34. package/dist/modes/interactive/components/settings-selector.js +10 -0
  35. package/dist/modes/interactive/components/tool-execution.js +10 -1
  36. package/dist/modes/interactive/interactive-mode.d.ts +8 -2
  37. package/dist/modes/interactive/interactive-mode.js +81 -25
  38. package/dist/modes/interactive/model-catalog-refresh.d.ts +6 -0
  39. package/dist/modes/interactive/model-catalog-refresh.js +33 -0
  40. package/dist/modes/interactive/theme/dark.json +2 -0
  41. package/dist/modes/interactive/theme/light.json +2 -0
  42. package/dist/modes/interactive/theme/theme-controller.d.ts +10 -2
  43. package/dist/modes/interactive/theme/theme-controller.js +25 -11
  44. package/dist/modes/interactive/theme/theme-schema.json +9 -1
  45. package/dist/modes/interactive/theme/theme.d.ts +5 -3
  46. package/dist/modes/interactive/theme/theme.js +12 -2
  47. package/dist/utils/abort.d.ts +4 -0
  48. package/dist/utils/abort.js +47 -0
  49. package/dist/utils/tools-manager.d.ts +5 -1
  50. package/dist/utils/tools-manager.js +12 -21
  51. package/package.json +6 -6
package/CHANGELOG.md CHANGED
@@ -2,7 +2,21 @@
2
2
 
3
3
  All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
- ## [Unreleased]
5
+ ## [0.8.8] - 2026-08-18
6
+
7
+ ### Added
8
+ - **`--use-theme <name>` CLI flag.** Sets the initial interactive theme for a run; the fullscreen theme controller applies an invocation-level theme before settings load, so the flag wins until you change the theme in `/settings`.
9
+ - **Transcript search highlighting.** The fullscreen TUI now highlights search matches live in the transcript, themeable via new optional `searchMatchBg`/`searchMatchText` colors (falling back to `selectedBg`/`text`).
10
+ - **Fullscreen exit output option.** `/settings` gained a **Fullscreen exit output** entry: exiting fullscreen can print the transcript as before (`"transcript"`) or only a session resume hint (`"resume-hint"`) via the new `fullscreenExitOutput` setting.
11
+ - **`defaultTools` setting.** Seeds the initial built-in tool selection for `createAgentSession`; when set, new sessions start with only the listed tools instead of the `read`/`bash`/`edit`/`write` default.
12
+ - **`sendUserMessage` template expansion.** Extensions can pass `expandPromptTemplates: true` to dispatch extension commands and expand skill commands/prompt templates from a sent user message.
13
+ - **Experimental strict tool sampling.** Under `PI_EXPERIMENTAL=1`, the built-in read/edit/write/bash tools request `json_schema` tool output with `strict: "prefer"`.
14
+
15
+ ### Changed
16
+ - **Managed-tool downloads moved off the startup path.** The TUI mounts and stays responsive while `fd`/`rg` install; download progress and warnings are reported as chat status lines (with an `app.tools.expand` hint) instead of raw `console.log` output, and the interactive startup accepts input (with interrupt/exit/submit feedback) while it completes.
17
+ - **Collapsed large tool output.** Tool components now preview only the first 10 lines with an inline `... (N more lines)` expand hint instead of dumping the whole block.
18
+ - **Shared model-catalog refreshes.** Concurrent interactive all-catalog refreshes reuse one in-flight refresh while keeping each caller's cancellation independent, and refreshes are bounded by a short timeout.
19
+ - **Runtime dependencies updated to Pi 0.84.2.** `@earendil-works/pi-ai`, `@earendil-works/pi-tui`, and `@earendil-works/pi-agent-core` moved to 0.84.2; `openAIResponses` model config gained an optional `supportsAdditionalTools` flag; `exportToHtml` accepts a `themeName` option.
6
20
 
7
21
  ## [0.8.7] - 2026-08-17
8
22
 
@@ -37,6 +37,7 @@ export interface Args {
37
37
  promptTemplates?: string[];
38
38
  noPromptTemplates?: boolean;
39
39
  themes?: string[];
40
+ useTheme?: string;
40
41
  noThemes?: boolean;
41
42
  noContextFiles?: boolean;
42
43
  listModels?: string | true;
package/dist/cli/args.js CHANGED
@@ -142,6 +142,16 @@ export function parseArgs(args) {
142
142
  else if (arg === "--no-prompt-templates" || arg === "-np") {
143
143
  result.noPromptTemplates = true;
144
144
  }
145
+ else if (arg === "--use-theme") {
146
+ const themeName = args[i + 1];
147
+ if (themeName === undefined || themeName.startsWith("-")) {
148
+ result.diagnostics.push({ type: "error", message: "--use-theme requires a theme name" });
149
+ }
150
+ else {
151
+ result.useTheme = themeName;
152
+ i++;
153
+ }
154
+ }
145
155
  else if (arg === "--no-themes") {
146
156
  result.noThemes = true;
147
157
  }
@@ -272,6 +282,7 @@ ${chalk.bold("Options:")}
272
282
  --prompt-template <path> Load a prompt template file or directory (can be used multiple times)
273
283
  --no-prompt-templates, -np Disable prompt template discovery and loading
274
284
  --theme <path> Load a theme file or directory (can be used multiple times)
285
+ --use-theme <name[/name]> Set the initial interactive theme for this run
275
286
  --no-themes Disable theme discovery and loading
276
287
  --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading
277
288
  --export <file> Export session file to HTML and exit
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "vitest";
2
+ import { parseArgs } from "./args.js";
3
+ describe("parseArgs --use-theme", () => {
4
+ test("parses --use-theme with a theme name", () => {
5
+ const result = parseArgs(["--use-theme", "nord-border", "hello"]);
6
+ expect(result.useTheme).toBe("nord-border");
7
+ expect(result.messages).toEqual(["hello"]);
8
+ });
9
+ test("accepts light/dark slash form", () => {
10
+ const result = parseArgs(["--use-theme", "light/dark"]);
11
+ expect(result.useTheme).toBe("light/dark");
12
+ });
13
+ test("reports an error when --use-theme is missing a theme name", () => {
14
+ const result = parseArgs(["--use-theme"]);
15
+ expect(result.useTheme).toBeUndefined();
16
+ expect(result.diagnostics.some((d) => d.type === "error")).toBe(true);
17
+ });
18
+ test("reports an error when --use-theme precedes a flag", () => {
19
+ const result = parseArgs(["--use-theme", "--no-session"]);
20
+ expect(result.useTheme).toBeUndefined();
21
+ expect(result.noSession).toBe(true);
22
+ expect(result.diagnostics.some((d) => d.type === "error")).toBe(true);
23
+ });
24
+ });
@@ -423,6 +423,7 @@ export declare class AgentSession {
423
423
  */
424
424
  sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
425
425
  deliverAs?: "steer" | "followUp";
426
+ expandPromptTemplates?: boolean;
426
427
  }): Promise<void>;
427
428
  /**
428
429
  * Clear all queued messages and return them.
@@ -634,7 +635,9 @@ export declare class AgentSession {
634
635
  * @param outputPath Optional output path (defaults to session directory)
635
636
  * @returns Path to exported file
636
637
  */
637
- exportToHtml(outputPath?: string): Promise<string>;
638
+ exportToHtml(outputPath?: string, options?: {
639
+ themeName?: string;
640
+ }): Promise<string>;
638
641
  /**
639
642
  * Export the current session branch to a JSONL file.
640
643
  * Writes the session header followed by all entries on the current branch path.
@@ -1133,7 +1133,7 @@ export class AgentSession {
1133
1133
  if (options?.deliverAs === "nextTurn") {
1134
1134
  this._pendingNextTurnMessages.push(appMessage);
1135
1135
  }
1136
- else if (this.isStreaming) {
1136
+ else if (this.isStreaming && options?.triggerTurn !== false) {
1137
1137
  if (options?.deliverAs === "followUp") {
1138
1138
  this.agent.followUp(appMessage);
1139
1139
  }
@@ -1180,9 +1180,9 @@ export class AgentSession {
1180
1180
  if (images.length === 0)
1181
1181
  images = undefined;
1182
1182
  }
1183
- // Use prompt() with expandPromptTemplates: false to skip command handling and template expansion
1183
+ // Use prompt() with expandPromptTemplates to control command handling and template expansion
1184
1184
  await this.prompt(text, {
1185
- expandPromptTemplates: false,
1185
+ expandPromptTemplates: options?.expandPromptTemplates ?? false,
1186
1186
  streamingBehavior: options?.deliverAs,
1187
1187
  images,
1188
1188
  source: "extension",
@@ -2659,9 +2659,8 @@ export class AgentSession {
2659
2659
  * @param outputPath Optional output path (defaults to session directory)
2660
2660
  * @returns Path to exported file
2661
2661
  */
2662
- async exportToHtml(outputPath) {
2663
- const configuredThemeName = this.settingsManager.getTheme();
2664
- const themeName = configuredThemeName && getThemeByName(configuredThemeName) ? configuredThemeName : undefined;
2662
+ async exportToHtml(outputPath, options = {}) {
2663
+ const themeName = [options.themeName, this.settingsManager.getTheme()].find((candidate) => candidate !== undefined && getThemeByName(candidate) !== undefined);
2665
2664
  // Create tool renderer if we have an extension runner (for custom tool HTML rendering)
2666
2665
  const toolRenderer = createToolHtmlRenderer({
2667
2666
  getToolDefinition: (name) => this.getToolDefinition(name),
@@ -1 +1,5 @@
1
1
  export declare function areExperimentalFeaturesEnabled(): boolean;
2
+ export declare function getExperimentalToolSampling(): {
3
+ readonly type: "json_schema";
4
+ readonly strict: "prefer";
5
+ } | undefined;
@@ -1,3 +1,7 @@
1
+ const PREFER_STRICT_TOOL_SAMPLING = { type: "json_schema", strict: "prefer" };
1
2
  export function areExperimentalFeaturesEnabled() {
2
3
  return process.env.PI_EXPERIMENTAL === "1";
3
4
  }
5
+ export function getExperimentalToolSampling() {
6
+ return areExperimentalFeaturesEnabled() ? PREFER_STRICT_TOOL_SAMPLING : undefined;
7
+ }
@@ -301,6 +301,7 @@ export interface ReplacedSessionContext extends ExtensionCommandContext {
301
301
  }): Promise<void>;
302
302
  sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
303
303
  deliverAs?: "steer" | "followUp";
304
+ expandPromptTemplates?: boolean;
304
305
  }): Promise<void>;
305
306
  }
306
307
  /** Rendering options for tool results */
@@ -926,9 +927,11 @@ export interface ExtensionAPI {
926
927
  /**
927
928
  * Send a user message to the agent. Always triggers a turn.
928
929
  * When the agent is streaming, use deliverAs to specify how to queue the message.
930
+ * Set expandPromptTemplates to dispatch extension commands and expand skill commands and prompt templates.
929
931
  */
930
932
  sendUserMessage(content: string | (TextContent | ImageContent)[], options?: {
931
933
  deliverAs?: "steer" | "followUp";
934
+ expandPromptTemplates?: boolean;
932
935
  }): void;
933
936
  /** Append a custom entry to the session for state persistence (not sent to LLM). */
934
937
  appendEntry<T = unknown>(customType: string, data?: T): void;
@@ -1124,6 +1127,7 @@ export type SendMessageHandler = <T = unknown>(message: Pick<CustomMessage<T>, "
1124
1127
  }) => void;
1125
1128
  export type SendUserMessageHandler = (content: string | (TextContent | ImageContent)[], options?: {
1126
1129
  deliverAs?: "steer" | "followUp";
1130
+ expandPromptTemplates?: boolean;
1127
1131
  }) => void;
1128
1132
  export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
1129
1133
  export type SetSessionNameHandler = (name: string) => void;
@@ -197,6 +197,14 @@ export declare const KEYBINDINGS: {
197
197
  readonly defaultKeys: [];
198
198
  readonly description: "Scroll viewport down half a page";
199
199
  };
200
+ readonly "tui.altScreen.lineUp": {
201
+ readonly defaultKeys: [];
202
+ readonly description: "Scroll viewport up one line";
203
+ };
204
+ readonly "tui.altScreen.lineDown": {
205
+ readonly defaultKeys: [];
206
+ readonly description: "Scroll viewport down one line";
207
+ };
200
208
  readonly "tui.altScreen.previousPrompt": {
201
209
  readonly defaultKeys: "ctrl+shift+up";
202
210
  readonly description: "Jump to previous semantic prompt";
@@ -205,6 +213,22 @@ export declare const KEYBINDINGS: {
205
213
  readonly defaultKeys: "ctrl+shift+down";
206
214
  readonly description: "Jump to next semantic prompt";
207
215
  };
216
+ readonly "tui.altScreen.search": {
217
+ readonly defaultKeys: "ctrl+shift+f";
218
+ readonly description: "Search the primary scroll view";
219
+ };
220
+ readonly "tui.altScreen.searchNext": {
221
+ readonly defaultKeys: ["enter", "ctrl+g"];
222
+ readonly description: "Select the next search match";
223
+ };
224
+ readonly "tui.altScreen.searchPrevious": {
225
+ readonly defaultKeys: ["shift+enter", "ctrl+shift+g"];
226
+ readonly description: "Select the previous search match";
227
+ };
228
+ readonly "tui.altScreen.searchClose": {
229
+ readonly defaultKeys: "escape";
230
+ readonly description: "Close transcript search";
231
+ };
208
232
  readonly "tui.altScreen.top": {
209
233
  readonly defaultKeys: "home";
210
234
  readonly description: "Scroll viewport to top";
@@ -98,6 +98,7 @@ declare const ModelDefinitionSchema: Type.TObject<{
98
98
  supportsLongCacheRetention: Type.TOptional<Type.TBoolean>;
99
99
  supportsStrictMode: Type.TOptional<Type.TBoolean>;
100
100
  supportsOpenAIGrammarTools: Type.TOptional<Type.TBoolean>;
101
+ supportsAdditionalTools: Type.TOptional<Type.TBoolean>;
101
102
  supportsToolSearch: Type.TOptional<Type.TBoolean>;
102
103
  }>, Type.TObject<{
103
104
  supportsEagerToolInputStreaming: Type.TOptional<Type.TBoolean>;
@@ -206,6 +207,7 @@ declare const ModelOverrideSchema: Type.TObject<{
206
207
  supportsLongCacheRetention: Type.TOptional<Type.TBoolean>;
207
208
  supportsStrictMode: Type.TOptional<Type.TBoolean>;
208
209
  supportsOpenAIGrammarTools: Type.TOptional<Type.TBoolean>;
210
+ supportsAdditionalTools: Type.TOptional<Type.TBoolean>;
209
211
  supportsToolSearch: Type.TOptional<Type.TBoolean>;
210
212
  }>, Type.TObject<{
211
213
  supportsEagerToolInputStreaming: Type.TOptional<Type.TBoolean>;
@@ -292,6 +294,7 @@ declare const ProviderConfigSchema: Type.TObject<{
292
294
  supportsLongCacheRetention: Type.TOptional<Type.TBoolean>;
293
295
  supportsStrictMode: Type.TOptional<Type.TBoolean>;
294
296
  supportsOpenAIGrammarTools: Type.TOptional<Type.TBoolean>;
297
+ supportsAdditionalTools: Type.TOptional<Type.TBoolean>;
295
298
  supportsToolSearch: Type.TOptional<Type.TBoolean>;
296
299
  }>, Type.TObject<{
297
300
  supportsEagerToolInputStreaming: Type.TOptional<Type.TBoolean>;
@@ -403,6 +406,7 @@ declare const ProviderConfigSchema: Type.TObject<{
403
406
  supportsLongCacheRetention: Type.TOptional<Type.TBoolean>;
404
407
  supportsStrictMode: Type.TOptional<Type.TBoolean>;
405
408
  supportsOpenAIGrammarTools: Type.TOptional<Type.TBoolean>;
409
+ supportsAdditionalTools: Type.TOptional<Type.TBoolean>;
406
410
  supportsToolSearch: Type.TOptional<Type.TBoolean>;
407
411
  }>, Type.TObject<{
408
412
  supportsEagerToolInputStreaming: Type.TOptional<Type.TBoolean>;
@@ -511,6 +515,7 @@ declare const ProviderConfigSchema: Type.TObject<{
511
515
  supportsLongCacheRetention: Type.TOptional<Type.TBoolean>;
512
516
  supportsStrictMode: Type.TOptional<Type.TBoolean>;
513
517
  supportsOpenAIGrammarTools: Type.TOptional<Type.TBoolean>;
518
+ supportsAdditionalTools: Type.TOptional<Type.TBoolean>;
514
519
  supportsToolSearch: Type.TOptional<Type.TBoolean>;
515
520
  }>, Type.TObject<{
516
521
  supportsEagerToolInputStreaming: Type.TOptional<Type.TBoolean>;
@@ -96,6 +96,7 @@ const OpenAIResponsesCompatSchema = Type.Object({
96
96
  supportsLongCacheRetention: Type.Optional(Type.Boolean()),
97
97
  supportsStrictMode: Type.Optional(Type.Boolean()),
98
98
  supportsOpenAIGrammarTools: Type.Optional(Type.Boolean()),
99
+ supportsAdditionalTools: Type.Optional(Type.Boolean()),
99
100
  supportsToolSearch: Type.Optional(Type.Boolean()),
100
101
  });
101
102
  const AnthropicMessagesCompatSchema = Type.Object({
@@ -1,8 +1,8 @@
1
- import { CONFIG_DIR_NAME } from "../config.js";
1
+ import { APP_NAME, CONFIG_DIR_NAME } from "../config.js";
2
2
  import { emitProjectTrustEvent } from "./extensions/runner.js";
3
3
  import { getProjectTrustOptions, hasTrustRequiringProjectResources, } from "./trust-manager.js";
4
4
  function formatProjectTrustPrompt(cwd) {
5
- return `Trust project folder?\n${cwd}\n\nThis allows selesai to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
5
+ return `Trust project folder?\n${cwd}\n\nThis allows ${APP_NAME} to load ${CONFIG_DIR_NAME} settings and resources, install missing project packages, and execute project extensions.`;
6
6
  }
7
7
  async function selectProjectTrustOption(cwd, ctx) {
8
8
  const options = getProjectTrustOptions(cwd, { includeSessionOnly: true });
@@ -34,9 +34,11 @@ export interface CreateAgentSessionOptions {
34
34
  /**
35
35
  * Optional allowlist of tool names.
36
36
  *
37
- * When omitted, pi enables the default built-in tools (read, bash, edit, write)
38
- * and leaves extension/custom tools enabled unless `noTools` changes that default.
39
- * When provided, only the listed tool names are enabled.
37
+ * When omitted, pi uses the `defaultTools` setting for the initial built-in
38
+ * selection when configured. Otherwise it enables the default built-in tools
39
+ * (read, bash, edit, write). Extension/custom tools remain enabled unless
40
+ * `noTools` changes that default. When provided, only the listed tool names are
41
+ * enabled.
40
42
  */
41
43
  tools?: string[];
42
44
  /** Optional denylist of tool names to disable. Applies after `tools` when both are provided. */
package/dist/core/sdk.js CHANGED
@@ -130,10 +130,11 @@ export async function createAgentSession(options = {}) {
130
130
  thinkingLevel = clampThinkingLevel(model, thinkingLevel);
131
131
  }
132
132
  const defaultActiveToolNames = ["read", "bash", "edit", "write"];
133
+ const configuredDefaultToolNames = settingsManager.getDefaultTools();
133
134
  const allowedToolNames = options.tools ?? (options.noTools === "all" ? [] : undefined);
134
135
  const excludedToolNames = options.excludeTools;
135
136
  const excludedToolNameSet = excludedToolNames ? new Set(excludedToolNames) : undefined;
136
- const initialActiveToolNames = (options.tools ? [...options.tools] : options.noTools ? [] : defaultActiveToolNames).filter((name) => !excludedToolNameSet?.has(name));
137
+ const initialActiveToolNames = (options.tools ?? (options.noTools ? [] : (configuredDefaultToolNames ?? defaultActiveToolNames))).filter((name) => !excludedToolNameSet?.has(name));
137
138
  let agent;
138
139
  // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
139
140
  const convertToLlmWithBlockImages = (messages) => {
@@ -5,7 +5,7 @@ import { readdir, stat } from "fs/promises";
5
5
  import { join, resolve } from "path";
6
6
  import { createInterface } from "readline";
7
7
  import { StringDecoder } from "string_decoder";
8
- import { getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
8
+ import { APP_NAME, getAgentDir as getDefaultAgentDir, getSessionsDir } from "../config.js";
9
9
  import { normalizePath, resolvePath } from "../utils/paths.js";
10
10
  import { createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "./messages.js";
11
11
  export const CURRENT_SESSION_VERSION = 3;
@@ -620,7 +620,7 @@ export class SessionManager {
620
620
  if (this.fileEntries.length === 0) {
621
621
  const explicitPath = this.sessionFile;
622
622
  if (statSync(explicitPath).size > 0) {
623
- throw new Error(`Session file is not a valid pi session: ${explicitPath}`);
623
+ throw new Error(`Session file is not a valid ${APP_NAME} session: ${explicitPath}`);
624
624
  }
625
625
  this.newSession();
626
626
  this.sessionFile = explicitPath;
@@ -22,6 +22,7 @@ export interface RetrySettings {
22
22
  provider?: ProviderRetrySettings;
23
23
  }
24
24
  export type TuiMode = RendererTuiMode;
25
+ export type FullscreenExitOutput = "transcript" | "resume-hint";
25
26
  export interface TerminalSettings {
26
27
  showImages?: boolean;
27
28
  imageWidthCells?: number;
@@ -97,6 +98,8 @@ export interface Settings {
97
98
  agents?: string[];
98
99
  terminal?: TerminalSettings;
99
100
  tuiMode?: TuiMode;
101
+ defaultTools?: string[];
102
+ fullscreenExitOutput?: FullscreenExitOutput;
100
103
  fullscreenScrollbar?: ScrollViewScrollbar;
101
104
  images?: ImageSettings;
102
105
  enabledModels?: string[];
@@ -288,11 +291,14 @@ export declare class SettingsManager {
288
291
  setTuiMode(mode: TuiMode): void;
289
292
  getFullscreenScrollbar(): ScrollViewScrollbar;
290
293
  setFullscreenScrollbar(mode: ScrollViewScrollbar): void;
294
+ getFullscreenExitOutput(): FullscreenExitOutput;
295
+ setFullscreenExitOutput(output: FullscreenExitOutput): void;
291
296
  getImageAutoResize(): boolean;
292
297
  setImageAutoResize(enabled: boolean): void;
293
298
  getBlockImages(): boolean;
294
299
  setBlockImages(blocked: boolean): void;
295
300
  getEnabledModels(): string[] | undefined;
301
+ getDefaultTools(): string[] | undefined;
296
302
  setEnabledModels(patterns: string[] | undefined): void;
297
303
  getDoubleEscapeAction(): "fork" | "tree" | "none";
298
304
  setDoubleEscapeAction(action: "fork" | "tree" | "none"): void;
@@ -837,6 +837,14 @@ export class SettingsManager {
837
837
  this.markModified("fullscreenScrollbar");
838
838
  this.save();
839
839
  }
840
+ getFullscreenExitOutput() {
841
+ return this.settings.fullscreenExitOutput === "resume-hint" ? "resume-hint" : "transcript";
842
+ }
843
+ setFullscreenExitOutput(output) {
844
+ this.globalSettings.fullscreenExitOutput = output;
845
+ this.markModified("fullscreenExitOutput");
846
+ this.save();
847
+ }
840
848
  getImageAutoResize() {
841
849
  return this.settings.images?.autoResize ?? true;
842
850
  }
@@ -862,6 +870,10 @@ export class SettingsManager {
862
870
  getEnabledModels() {
863
871
  return this.settings.enabledModels;
864
872
  }
873
+ getDefaultTools() {
874
+ const tools = this.settings.defaultTools;
875
+ return tools ? [...tools] : undefined;
876
+ }
865
877
  setEnabledModels(patterns) {
866
878
  this.globalSettings.enabledModels = patterns;
867
879
  this.markModified("enabledModels");
@@ -51,7 +51,7 @@ export function buildSystemPrompt(options) {
51
51
  if (activeGuidelines.length > 0) {
52
52
  prompt += `\n\nActive tool guidelines:\n${activeGuidelines.map((guideline) => `- ${guideline}`).join("\n")}`;
53
53
  }
54
- prompt += `\nCurrent working directory: ${promptCwd}`;
54
+ prompt += `\nCurrent working directory: ${promptCwd}\n`;
55
55
  return prompt;
56
56
  }
57
57
  // Get absolute paths to documentation and examples
@@ -7,6 +7,7 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
7
7
  import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
8
8
  import { theme } from "../../modes/interactive/theme/theme.js";
9
9
  import { waitForChildProcess } from "../../utils/child-process.js";
10
+ import { getExperimentalToolSampling } from "../experimental.js";
10
11
  import { getShellConfig, getShellEnv, killProcessTree, trackDetachedChildPid, untrackDetachedChildPid, } from "../../utils/shell.js";
11
12
  import { OutputAccumulator } from "./output-accumulator.js";
12
13
  import { getTextOutput, invalidArgText, str } from "./render-utils.js";
@@ -232,6 +233,7 @@ export function createBashToolDefinition(cwd, options) {
232
233
  ? ["Inspect PI_* environment variables for current model and session details."]
233
234
  : undefined,
234
235
  parameters: bashSchema,
236
+ constrainedSampling: getExperimentalToolSampling(),
235
237
  async execute(_toolCallId, { command, timeout }, signal, onUpdate, ctx) {
236
238
  const resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
237
239
  const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook, exposeSessionEnvironment, ctx);
@@ -3,6 +3,7 @@ import { constants } from "fs";
3
3
  import { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from "fs/promises";
4
4
  import { Type } from "typebox";
5
5
  import { renderDiff } from "../../modes/interactive/components/diff.js";
6
+ import { getExperimentalToolSampling } from "../experimental.js";
6
7
  import { applyEditsToNormalizedContent, computeEditsDiff, detectLineEnding, generateDiffString, generateUnifiedPatch, normalizeToLF, restoreLineEndings, stripBom, } from "./edit-diff.js";
7
8
  import { withFileMutationQueue } from "./file-mutation-queue.js";
8
9
  import { resolveToCwd } from "./path-utils.js";
@@ -169,6 +170,7 @@ export function createEditToolDefinition(cwd, options) {
169
170
  "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.",
170
171
  ],
171
172
  parameters: editSchema,
173
+ constrainedSampling: getExperimentalToolSampling(),
172
174
  renderShell: "self",
173
175
  prepareArguments: prepareEditArguments,
174
176
  async execute(_toolCallId, input, signal, _onUpdate, _ctx) {
@@ -154,7 +154,7 @@ export function createFindToolDefinition(cwd, options) {
154
154
  return;
155
155
  }
156
156
  // Default implementation uses fd.
157
- const fdPath = await ensureTool("fd", true);
157
+ const fdPath = await ensureTool("fd");
158
158
  if (signal?.aborted) {
159
159
  settle(() => reject(new Error("Operation aborted")));
160
160
  return;
@@ -92,7 +92,7 @@ export function createGrepToolDefinition(cwd, options) {
92
92
  };
93
93
  (async () => {
94
94
  try {
95
- const rgPath = await ensureTool("rg", true);
95
+ const rgPath = await ensureTool("rg");
96
96
  if (!rgPath) {
97
97
  settle(() => reject(new Error("ripgrep (rg) is not available and could not be downloaded")));
98
98
  return;
@@ -9,6 +9,7 @@ import { getLanguageFromPath, highlightCode } from "../../modes/interactive/them
9
9
  import { processImage } from "../../utils/image-process.js";
10
10
  import { detectSupportedImageMimeTypeFromFile } from "../../utils/mime.js";
11
11
  import { formatPathRelativeToCwdOrAbsolute } from "../../utils/paths.js";
12
+ import { getExperimentalToolSampling } from "../experimental.js";
12
13
  import { resolveReadPathAsync, resolveToCwd } from "./path-utils.js";
13
14
  import { getTextOutput, renderToolPath, replaceTabs, str } from "./render-utils.js";
14
15
  import { wrapToolDefinition } from "./tool-definition-wrapper.js";
@@ -137,6 +138,7 @@ export function createReadToolDefinition(cwd, options) {
137
138
  promptSnippet: "Read file contents",
138
139
  promptGuidelines: ["Use read to examine files instead of cat or sed."],
139
140
  parameters: readSchema,
141
+ constrainedSampling: getExperimentalToolSampling(),
140
142
  async execute(_toolCallId, { path, offset, limit }, signal, _onUpdate, ctx) {
141
143
  return new Promise((resolve, reject) => {
142
144
  if (signal?.aborted) {
@@ -4,6 +4,7 @@ import { dirname } from "path";
4
4
  import { Type } from "typebox";
5
5
  import { keyHint } from "../../modes/interactive/components/keybinding-hints.js";
6
6
  import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
7
+ import { getExperimentalToolSampling } from "../experimental.js";
7
8
  import { withFileMutationQueue } from "./file-mutation-queue.js";
8
9
  import { resolveToCwd } from "./path-utils.js";
9
10
  import { normalizeDisplayText, renderToolPath, replaceTabs, str } from "./render-utils.js";
@@ -137,6 +138,7 @@ export function createWriteToolDefinition(cwd, options) {
137
138
  promptSnippet: "Create or overwrite files",
138
139
  promptGuidelines: ["Use write only for new files or complete rewrites."],
139
140
  parameters: writeSchema,
141
+ constrainedSampling: getExperimentalToolSampling(),
140
142
  async execute(_toolCallId, { path, content }, signal, _onUpdate, _ctx) {
141
143
  const absolutePath = resolveToCwd(path, cwd);
142
144
  const dir = dirname(absolutePath);
@@ -29,7 +29,6 @@ const WATCHER_RESTART_DELAY_MS = 3000;
29
29
  const POLL_INTERVAL_MS = 3000;
30
30
  const HEALTHY_SCAN_INTERVAL_MS = 60_000;
31
31
  const RETRY_DELAY_MS = 100;
32
- const SLOW_RESULT_SCAN_MS = 500;
33
32
 
34
33
  type ResultWatcherFs = Pick<typeof fs, "existsSync" | "readFileSync" | "unlinkSync" | "readdirSync" | "mkdirSync" | "realpathSync" | "statSync" | "watch">;
35
34
 
@@ -92,12 +91,6 @@ type ResultFileIdentity = {
92
91
  asyncDir?: string;
93
92
  };
94
93
 
95
- interface ResultScanStats {
96
- files: number;
97
- scheduled: number;
98
- startedAt: number;
99
- }
100
-
101
94
  function jsonStringProperty(raw: string, property: string): string | undefined {
102
95
  const matches = raw.matchAll(new RegExp(`"${property}"\\s*:\\s*("(?:\\\\.|[^"\\\\])*")`, "g"));
103
96
  let encoded: string | undefined;
@@ -547,11 +540,6 @@ export function createResultWatcher(
547
540
  void handleResult(file, triggerTurn);
548
541
  }, deps.coalesceDelayMs ?? 50);
549
542
 
550
- const logScanStats = (stats: ResultScanStats) => {
551
- const elapsed = Date.now() - stats.startedAt;
552
- if (elapsed < SLOW_RESULT_SCAN_MS) return;
553
- console.error(`Subagent result scan inspected ${stats.files} indexed result file(s), scheduled ${stats.scheduled} in ${elapsed}ms (${resultsDir}).`);
554
- };
555
543
  const indexedResultCandidates = (observed: ReadonlySet<string>): string[] => {
556
544
  const files = new Set<string>();
557
545
  if (state.currentSessionId) {
@@ -565,14 +553,11 @@ export function createResultWatcher(
565
553
  const primeExistingResults = (options: { triggerTurn?: boolean } = {}) => {
566
554
  try {
567
555
  const triggerTurn = options.triggerTurn !== false;
568
- const stats: ResultScanStats = { files: 0, scheduled: 0, startedAt: Date.now() };
569
556
  const observed = observedRunIds();
570
557
  for (const file of indexedResultCandidates(observed)) {
571
- stats.files += 1;
572
558
  const signature = resultSignature(file, observed);
573
559
  if (!signature) continue;
574
560
  if (!shouldProcessResult(file, observed, signature)) continue;
575
- stats.scheduled += 1;
576
561
  scheduleResult(file, triggerTurn);
577
562
  }
578
563
  // Selesai fork: during the lineage adoption window, also scan flat
@@ -584,15 +569,12 @@ export function createResultWatcher(
584
569
  const indexed = indexedResultCandidates(observed);
585
570
  for (const file of fsApi.readdirSync(resultsDir)) {
586
571
  if (!file.endsWith(".json") || indexed.includes(file)) continue;
587
- stats.files += 1;
588
572
  const signature = resultSignature(file, observed);
589
573
  if (!signature) continue;
590
574
  if (!shouldProcessResult(file, observed, signature)) continue;
591
- stats.scheduled += 1;
592
575
  scheduleResult(file, triggerTurn);
593
576
  }
594
577
  }
595
- logScanStats(stats);
596
578
  } catch (error) {
597
579
  if (!isNotFound(error)) console.error(`Failed to scan subagent result index in '${resultsDir}':`, error);
598
580
  }
@@ -126,6 +126,32 @@ describe("result watcher", () => {
126
126
  }
127
127
  });
128
128
 
129
+ it("keeps slow indexed result scans silent", () => {
130
+ const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-slow-scan-"));
131
+ const originalNow = Date.now;
132
+ const originalError = console.error;
133
+ const logged: unknown[][] = [];
134
+ try {
135
+ const state = createState();
136
+ state.currentSessionId = "session-current";
137
+ state.asyncJobs.set("slow-scan", { asyncId: "slow-scan", asyncDir: path.join(resultsDir, "slow-scan"), status: "running", startedAt: 0, updatedAt: 0 });
138
+ let nowCalls = 0;
139
+ Date.now = () => nowCalls++ === 0 ? 0 : 501;
140
+ console.error = (...args: unknown[]) => { logged.push(args); };
141
+ const watcher = createResultWatcher({ events: { on: () => () => {}, emit() {} } }, state, resultsDir, 60_000);
142
+ try {
143
+ watcher.primeExistingResults();
144
+ } finally {
145
+ watcher.stopResultWatcher();
146
+ }
147
+ assert.deepEqual(logged, []);
148
+ } finally {
149
+ Date.now = originalNow;
150
+ console.error = originalError;
151
+ fs.rmSync(resultsDir, { recursive: true, force: true });
152
+ }
153
+ });
154
+
129
155
  it("skips full parsing for unrelated sessions during priming, safety scans, and native watch events", async () => {
130
156
  const resultsDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-result-watcher-filter-"));
131
157
  try {
@@ -15,7 +15,7 @@ import { getAgentDir, getModelsPath } from "@selesai/code";
15
15
  import type { AuthStorage, ExtensionAPI, ExtensionCommandContext, ExtensionContext, SessionStartEvent } from "@selesai/code";
16
16
 
17
17
  export const TOKEN_IN_PROVIDER = "tokenin";
18
- export const TOKEN_IN_DASHBOARD_URL = "https://token-in.selesai.in/dashboard/tokens";
18
+ export const TOKEN_IN_DASHBOARD_URL = "https://token.selesai.in/dashboard/tokens";
19
19
  export const ONBOARDING_MARKER_NAME = ".tokenInOnboardingComplete";
20
20
  export const PLACEHOLDER_API_KEY = "sk-xxx";
21
21
 
package/dist/index.d.ts CHANGED
@@ -18,7 +18,7 @@ export type { ResourceCollision, ResourceDiagnostic, ResourceLoader } from "./co
18
18
  export { DefaultResourceLoader, loadProjectContextFiles } from "./core/resource-loader.ts";
19
19
  export { AgentSessionRuntime, type AgentSessionRuntimeDiagnostic, type AgentSessionServices, type CreateAgentSessionFromServicesOptions, type CreateAgentSessionOptions, type CreateAgentSessionResult, type CreateAgentSessionRuntimeFactory, type CreateAgentSessionRuntimeResult, type CreateAgentSessionServicesOptions, createAgentSession, createAgentSessionFromServices, createAgentSessionRuntime, createAgentSessionServices, createBashTool, createCodingTools, createEditTool, createFindTool, createGrepTool, createLsTool, createReadOnlyTools, createReadTool, createWriteTool, type PromptTemplate, } from "./core/sdk.ts";
20
20
  export { type BranchSummaryEntry, buildContextEntries, buildSessionContext, type CompactionEntry, CURRENT_SESSION_VERSION, type CustomEntry, type CustomMessageEntry, type FileEntry, getLatestCompactionEntry, type ModelChangeEntry, migrateSessionEntries, type NewSessionOptions, parseSessionEntries, type SessionContext, type SessionEntry, type SessionEntryBase, type SessionHeader, type SessionInfo, type SessionInfoEntry, SessionManager, type SessionMessageEntry, type SessionTreeNode, sessionEntryToContextMessages, type ThinkingLevelChangeEntry, } from "./core/session-manager.ts";
21
- export { type AutoHandoffSettings, type CompactionSettings, type DefaultProjectTrust, type ImageSettings, type PackageSource, type RetrySettings, SettingsManager, type SettingsManagerCreateOptions, type TuiMode, } from "./core/settings-manager.ts";
21
+ export { type AutoHandoffSettings, type CompactionSettings, type DefaultProjectTrust, type FullscreenExitOutput, type ImageSettings, type PackageSource, type RetrySettings, SettingsManager, type SettingsManagerCreateOptions, type TuiMode, } from "./core/settings-manager.ts";
22
22
  export { formatSkillsForPrompt, type LoadSkillsFromDirOptions, type LoadSkillsResult, loadSkills, loadSkillsFromDir, type Skill, type SkillFrontmatter, } from "./core/skills.ts";
23
23
  export { createSyntheticSourceInfo } from "./core/source-info.ts";
24
24
  export { type EditDiffResult, generateDiffString, generateUnifiedPatch } from "./core/tools/edit-diff.ts";
package/dist/main.js CHANGED
@@ -491,6 +491,9 @@ export async function main(args, options) {
491
491
  await showFirstTimeSetup(startupSettingsManager);
492
492
  time("firstTimeSetup");
493
493
  }
494
+ if (appMode === "interactive" && parsed.useTheme !== undefined) {
495
+ startupSettingsManager.applyOverrides({ theme: parsed.useTheme });
496
+ }
494
497
  // Decide the final runtime cwd before creating cwd-bound runtime services.
495
498
  // --session and --resume may select a session from another project, so project-local
496
499
  // settings, resources, provider registrations, and models must be resolved only after
@@ -723,6 +726,7 @@ export async function main(args, options) {
723
726
  initialMessages: parsed.messages,
724
727
  verbose: parsed.verbose,
725
728
  tuiMode: parsed.tuiMode,
729
+ initialThemeSetting: parsed.useTheme,
726
730
  });
727
731
  if (startupBenchmark) {
728
732
  await interactiveMode.init();