@selesai/code 0.8.2 → 0.8.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +4 -2
  3. package/dist/cli/args.d.ts +2 -0
  4. package/dist/cli/args.js +18 -0
  5. package/dist/cli/config-selector.js +2 -2
  6. package/dist/cli/startup-ui.d.ts +1 -1
  7. package/dist/cli/startup-ui.js +2 -2
  8. package/dist/core/keybindings.d.ts +44 -4
  9. package/dist/core/llama/provider.js +22 -7
  10. package/dist/core/model-resolver.js +2 -0
  11. package/dist/core/model-runtime.d.ts +4 -2
  12. package/dist/core/model-runtime.js +23 -2
  13. package/dist/core/remote-catalog-provider.js +72 -67
  14. package/dist/core/settings-manager.d.ts +8 -0
  15. package/dist/core/settings-manager.js +18 -0
  16. package/dist/extensions/pi-powerline-footer/CHANGELOG.md +2 -1
  17. package/dist/extensions/pi-powerline-footer/index.ts +3 -272
  18. package/dist/extensions/pi-powerline-footer/package.json +4 -5
  19. package/dist/extensions/pi-powerline-footer/powerline-config.ts +0 -6
  20. package/dist/extensions/pi-powerline-footer/tests/fixed-editor-border.test.ts +2 -2
  21. package/dist/extensions/pi-powerline-footer/tests/guide.test.ts +1 -1
  22. package/dist/extensions/pi-powerline-footer/tsconfig.json +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/main.js +1 -0
  25. package/dist/modes/interactive/components/settings-selector.d.ts +6 -2
  26. package/dist/modes/interactive/components/settings-selector.js +20 -0
  27. package/dist/modes/interactive/interactive-mode.d.ts +28 -0
  28. package/dist/modes/interactive/interactive-mode.js +214 -28
  29. package/dist/modes/interactive/theme/dark.json +1 -0
  30. package/dist/modes/interactive/theme/light.json +1 -0
  31. package/dist/modes/interactive/theme/theme-controller.d.ts +3 -0
  32. package/dist/modes/interactive/theme/theme-controller.js +10 -1
  33. package/dist/modes/interactive/theme/theme.d.ts +2 -2
  34. package/dist/modes/interactive/theme/theme.js +24 -6
  35. package/docs/keybindings.md +35 -7
  36. package/docs/settings.md +16 -14
  37. package/docs/usage.md +5 -0
  38. package/package.json +5 -5
  39. package/dist/extensions/pi-powerline-footer/fixed-editor/cluster.ts +0 -113
  40. package/dist/extensions/pi-powerline-footer/fixed-editor/terminal-split.ts +0 -1080
  41. package/dist/extensions/pi-powerline-footer/tests/fixed-editor.test.ts +0 -1452
  42. package/dist/extensions/pi-powerline-footer/tests/jump-shortcuts.test.ts +0 -49
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to `@selesai/code` will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [0.8.3] - 2026-08-12
8
+
9
+ ### Added
10
+ - **Pi v0.84.1 migration with fullscreen TUI by default.** Selesai now runs Pi's native fullscreen TUI mode (`tuiMode: "fullscreen"`) by default, with an alternate-screen viewport, a fixed dock for the input editor/status/widgets/footer, a scrollable transcript, and fullscreen-specific keybindings. `--tui-mode regular`, the `tuiMode` setting, or the `TUI mode` entry in `/settings` opt back into regular mode. The powerline extension's unsupported terminal-split compositor and mouse-scroll/chat-jump machinery were removed; its status, editor, and widgets now live in Pi's native dock through the standard extension APIs.
11
+
12
+ ### Changed
13
+ - **Runtime dependencies updated to Pi v0.84.1.** `@earendil-works/pi-ai`, `@earendil-works/pi-tui`, and `@earendil-works/pi-agent-core` moved to 0.84.1 and `undici` to 8.9.0, matching upstream `@earendil-works/pi-coding-agent` 0.84.1. The model-runtime/refresh APIs were ported to the new pi-ai publication model, and the interactive renderer now selects between Pi's `TuiMainScreen` (regular) and `TuiAltScreen` (fullscreen).
14
+
5
15
  ## [0.8.2] - 2026-08-11
6
16
 
7
17
  ### Fixed
package/README.md CHANGED
@@ -86,14 +86,16 @@ This is the interaction layer used by the workflow modes and available to normal
86
86
 
87
87
  ### A richer terminal workspace
88
88
 
89
+ The interactive terminal runs Pi's native **fullscreen TUI mode** by default: the chat scrolls inside an alternate-screen viewport while the input editor, status line, and widgets stay fixed in a dock at the bottom. Choose `regular` (inline output) at any time with `--tui-mode regular`, the `tuiMode` setting, or the `TUI mode` entry in `/settings`.
90
+
89
91
  The bundled `pi-powerline-footer` improves the interactive display with:
90
92
 
91
93
  - a powerline status bar for model, thinking level, path, git state, context, tokens, cost, cache, and session data
92
- - a fixed-editor layout that keeps the editor and status area visible while chat scrolls
94
+ - status, editor, and widgets that live in Pi's native fixed dock (the old terminal-split compositor was decommissioned)
93
95
  - a file-backed queue/inbox for deferred ideas and compaction-safe prompts (`# <idea>`, `/idea`, `/ideas`, and `/queue`), plus session-aware `/cd`
94
96
  - a recurring `/guide` tour with compact and remembered-dismissal modes, covering workflows, delegation/research prompts, skills, handoff, and recovery
95
97
  - persistent bash mode with shell state, transcript output, history, and ghost suggestions
96
- - editor stash/restore, prompt history, clipboard shortcuts, and message-jump shortcuts
98
+ - editor stash/restore, prompt history, clipboard shortcuts, and editor boundary navigation
97
99
  - themed working-vibe messages, Nerd Font detection, configurable presets, layout, placement, and cost display
98
100
 
99
101
  ### Coordination between sessions
@@ -3,6 +3,7 @@
3
3
  */
4
4
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
5
5
  import type { ExtensionFlag } from "../core/extensions/types.ts";
6
+ import type { TuiMode } from "../core/settings-manager.ts";
6
7
  export type Mode = "text" | "json" | "rpc";
7
8
  export interface Args {
8
9
  provider?: string;
@@ -40,6 +41,7 @@ export interface Args {
40
41
  noContextFiles?: boolean;
41
42
  listModels?: string | true;
42
43
  offline?: boolean;
44
+ tuiMode?: TuiMode;
43
45
  verbose?: boolean;
44
46
  projectTrustOverride?: boolean;
45
47
  messages: string[];
package/dist/cli/args.js CHANGED
@@ -157,6 +157,23 @@ export function parseArgs(args) {
157
157
  result.listModels = true;
158
158
  }
159
159
  }
160
+ else if (arg === "--tui-mode") {
161
+ const mode = args[i + 1];
162
+ if (mode === "regular" || mode === "fullscreen") {
163
+ result.tuiMode = mode;
164
+ i++;
165
+ }
166
+ else if (mode === undefined || mode.startsWith("-")) {
167
+ result.diagnostics.push({ type: "error", message: "--tui-mode requires regular or fullscreen" });
168
+ }
169
+ else {
170
+ i++;
171
+ result.diagnostics.push({
172
+ type: "error",
173
+ message: `Invalid TUI mode "${mode}". Valid values: regular, fullscreen`,
174
+ });
175
+ }
176
+ }
160
177
  else if (arg === "--verbose") {
161
178
  result.verbose = true;
162
179
  }
@@ -259,6 +276,7 @@ ${chalk.bold("Options:")}
259
276
  --no-context-files, -nc Disable AGENTS.md and CLAUDE.md discovery and loading
260
277
  --export <file> Export session file to HTML and exit
261
278
  --list-models [search] List available models (with optional fuzzy search)
279
+ --tui-mode <mode> TUI mode: fullscreen (default) or regular
262
280
  --verbose Force verbose startup (overrides quietStartup setting)
263
281
  --approve, -a Trust project-local files for this run
264
282
  --no-approve, -na Ignore project-local files for this run
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * TUI config selector for `pi config` command
3
3
  */
4
- import { ProcessTerminal, TUI } from "@earendil-works/pi-tui";
4
+ import { ProcessTerminal, TuiMainScreen } from "@earendil-works/pi-tui";
5
5
  import { ConfigSelectorComponent } from "../modes/interactive/components/config-selector.js";
6
6
  import { initTheme, stopThemeWatcher } from "../modes/interactive/theme/theme.js";
7
7
  /** Show TUI config selector and return when closed */
@@ -9,7 +9,7 @@ export async function selectConfig(options) {
9
9
  // Initialize theme before showing TUI
10
10
  initTheme(options.settingsManager.getTheme(), true);
11
11
  return new Promise((resolve) => {
12
- const ui = new TUI(new ProcessTerminal(), undefined, options.agentDir);
12
+ const ui = new TuiMainScreen(new ProcessTerminal(), undefined, options.agentDir);
13
13
  let resolved = false;
14
14
  const selector = new ConfigSelectorComponent(options.resolvedPaths, options.settingsManager, options.cwd, options.agentDir, () => {
15
15
  if (!resolved) {
@@ -1,4 +1,4 @@
1
- import { TUI } from "@earendil-works/pi-tui";
1
+ import { type TUI } from "@earendil-works/pi-tui";
2
2
  import { SettingsManager } from "../core/settings-manager.ts";
3
3
  export declare function createStartupTui(settingsManager: SettingsManager): Promise<TUI>;
4
4
  export declare function startStartupTui(ui: TUI, settingsManager: SettingsManager): void;
@@ -1,4 +1,4 @@
1
- import { ProcessTerminal, setKeybindings, TUI } from "@earendil-works/pi-tui";
1
+ import { ProcessTerminal, setKeybindings, TuiMainScreen } from "@earendil-works/pi-tui";
2
2
  import { existsSync } from "fs";
3
3
  import { ENV_AGENT_DIR, getAgentDir, getFirstRunMarkerPath, markFirstRunComplete } from "../config.js";
4
4
  import { KeybindingsManager } from "../core/keybindings.js";
@@ -47,7 +47,7 @@ export async function createStartupTui(settingsManager) {
47
47
  const terminalTheme = detectTerminalBackgroundFromEnv().theme;
48
48
  initTheme(resolveThemeSetting(settingsManager.getThemeSetting(), terminalTheme) ?? terminalTheme);
49
49
  setKeybindings(KeybindingsManager.create());
50
- const ui = new TUI(new ProcessTerminal(), settingsManager.getShowHardwareCursor(), getAgentDir());
50
+ const ui = new TuiMainScreen(new ProcessTerminal(), settingsManager.getShowHardwareCursor(), getAgentDir());
51
51
  ui.setClearOnShrink(settingsManager.getClearOnShrink());
52
52
  return ui;
53
53
  }
@@ -57,6 +57,14 @@ export declare const KEYBINDINGS: {
57
57
  readonly defaultKeys: "down";
58
58
  readonly description: "Move cursor down";
59
59
  };
60
+ readonly "tui.editor.historyPrevious": {
61
+ readonly defaultKeys: [];
62
+ readonly description: "Select previous prompt history entry";
63
+ };
64
+ readonly "tui.editor.historyNext": {
65
+ readonly defaultKeys: [];
66
+ readonly description: "Select next prompt history entry";
67
+ };
60
68
  readonly "tui.editor.cursorLeft": {
61
69
  readonly defaultKeys: ["left", "ctrl+b"];
62
70
  readonly description: "Move cursor left";
@@ -74,11 +82,11 @@ export declare const KEYBINDINGS: {
74
82
  readonly description: "Move cursor word right";
75
83
  };
76
84
  readonly "tui.editor.cursorLineStart": {
77
- readonly defaultKeys: ["home", "ctrl+a"];
85
+ readonly defaultKeys: ["home", "ctrl+home", "ctrl+a"];
78
86
  readonly description: "Move to line start";
79
87
  };
80
88
  readonly "tui.editor.cursorLineEnd": {
81
- readonly defaultKeys: ["end", "ctrl+e"];
89
+ readonly defaultKeys: ["end", "ctrl+end", "ctrl+e"];
82
90
  readonly description: "Move to line end";
83
91
  };
84
92
  readonly "tui.editor.jumpForward": {
@@ -90,11 +98,11 @@ export declare const KEYBINDINGS: {
90
98
  readonly description: "Jump backward to character";
91
99
  };
92
100
  readonly "tui.editor.pageUp": {
93
- readonly defaultKeys: "pageUp";
101
+ readonly defaultKeys: ["pageUp", "ctrl+pageUp"];
94
102
  readonly description: "Page up";
95
103
  };
96
104
  readonly "tui.editor.pageDown": {
97
- readonly defaultKeys: "pageDown";
105
+ readonly defaultKeys: ["pageDown", "ctrl+pageDown"];
98
106
  readonly description: "Page down";
99
107
  };
100
108
  readonly "tui.editor.deleteCharBackward": {
@@ -173,6 +181,38 @@ export declare const KEYBINDINGS: {
173
181
  readonly defaultKeys: ["escape", "ctrl+c"];
174
182
  readonly description: "Cancel selection";
175
183
  };
184
+ readonly "tui.altScreen.pageUp": {
185
+ readonly defaultKeys: "pageUp";
186
+ readonly description: "Scroll viewport up one page";
187
+ };
188
+ readonly "tui.altScreen.pageDown": {
189
+ readonly defaultKeys: "pageDown";
190
+ readonly description: "Scroll viewport down one page";
191
+ };
192
+ readonly "tui.altScreen.halfPageUp": {
193
+ readonly defaultKeys: [];
194
+ readonly description: "Scroll viewport up half a page";
195
+ };
196
+ readonly "tui.altScreen.halfPageDown": {
197
+ readonly defaultKeys: [];
198
+ readonly description: "Scroll viewport down half a page";
199
+ };
200
+ readonly "tui.altScreen.previousPrompt": {
201
+ readonly defaultKeys: "ctrl+shift+up";
202
+ readonly description: "Jump to previous semantic prompt";
203
+ };
204
+ readonly "tui.altScreen.nextPrompt": {
205
+ readonly defaultKeys: "ctrl+shift+down";
206
+ readonly description: "Jump to next semantic prompt";
207
+ };
208
+ readonly "tui.altScreen.top": {
209
+ readonly defaultKeys: "home";
210
+ readonly description: "Scroll viewport to top";
211
+ };
212
+ readonly "tui.altScreen.bottom": {
213
+ readonly defaultKeys: "end";
214
+ readonly description: "Scroll viewport to bottom";
215
+ };
176
216
  readonly "app.interrupt": {
177
217
  readonly defaultKeys: "escape";
178
218
  readonly description: "Cancel or abort";
@@ -85,19 +85,34 @@ export function createLlamaProvider() {
85
85
  },
86
86
  getModels: () => models,
87
87
  refreshModels: async (context) => {
88
- const stored = await context.store.read();
89
- if (stored) {
90
- models = stored.models.filter((model) => model.provider === LLAMA_PROVIDER_ID && model.api === "openai-completions");
88
+ const stored = context.stored;
89
+ const restored = stored
90
+ ? stored.models.filter((model) => model.provider === LLAMA_PROVIDER_ID && model.api === "openai-completions")
91
+ : [];
92
+ if (!(await context.publish({
93
+ update: () => {
94
+ models = restored;
95
+ },
96
+ }))) {
97
+ return;
91
98
  }
92
- if (!context.allowNetwork || context.signal?.aborted || context.credential?.type !== "api_key")
99
+ if (!context.allowNetwork || context.signal.aborted || context.credential?.type !== "api_key")
93
100
  return;
94
101
  const serverUrl = credentialServerUrl(context.credential);
95
102
  if (!serverUrl)
96
103
  return;
97
104
  const catalog = await new LlamaClient(serverUrl, context.credential.key).list({ signal: context.signal });
98
- setCatalog(catalog, serverUrl);
99
- if (!context.signal?.aborted)
100
- await context.store.write({ models, checkedAt: Date.now() });
105
+ const refreshed = catalog
106
+ .filter((model) => model.status.value === "loaded")
107
+ .map((model) => toPiModel(model, serverUrl));
108
+ if (context.signal.aborted)
109
+ return;
110
+ await context.publish({
111
+ persist: { models: refreshed, checkedAt: Date.now() },
112
+ update: () => {
113
+ models = refreshed;
114
+ },
115
+ });
101
116
  },
102
117
  stream: (model, context, options) => stream(model, context, options),
103
118
  streamSimple: (model, context, options) => streamSimple(model, context, options),
@@ -35,6 +35,7 @@ export const defaultModelPerProvider = {
35
35
  huggingface: "moonshotai/Kimi-K2.6",
36
36
  fireworks: "accounts/fireworks/models/kimi-k2p6",
37
37
  together: "moonshotai/Kimi-K2.6",
38
+ baseten: "zai-org/GLM-5.2",
38
39
  opencode: "kimi-k2.6",
39
40
  "opencode-go": "kimi-k2.6",
40
41
  "kimi-coding": "kimi-for-coding",
@@ -42,6 +43,7 @@ export const defaultModelPerProvider = {
42
43
  "cloudflare-ai-gateway": "workers-ai/@cf/moonshotai/kimi-k2.6",
43
44
  "qwen-token-plan": "qwen3.7-max",
44
45
  "qwen-token-plan-cn": "qwen3.7-max",
46
+ "qwen-token-plan-individual": "qwen3.8-max",
45
47
  xiaomi: "mimo-v2.5-pro",
46
48
  "xiaomi-token-plan-cn": "mimo-v2.5-pro",
47
49
  "xiaomi-token-plan-ams": "mimo-v2.5-pro",
@@ -1,4 +1,4 @@
1
- import { type Api, type AssistantMessage, type AssistantMessageEventStream, type AuthCheck, type AuthInteraction, type AuthResult, type AuthType, type Context, type Credential, type CredentialInfo, type CredentialStore, type Model, type Models, type ModelsApiStreamOptions, type ModelsRefreshOptions, type ModelsRefreshResult, type ModelsSimpleStreamOptions, type ModelsStore, type Provider } from "@earendil-works/pi-ai";
1
+ import { type Api, type AssistantMessage, type AssistantMessageEventStream, type AuthCheck, type AuthInteraction, type AuthOperationOptions, type AuthResult, type AuthType, type Context, type Credential, type CredentialInfo, type CredentialStore, type DeferredHandle, type Model, type Models, type ModelsApiStreamOptions, type ModelsDeferredCancelOptions, type ModelsDeferredFetchOptions, type ModelsRefreshOptions, type ModelsRefreshResult, type ModelsSimpleStreamOptions, type ModelsStore, type Provider } from "@earendil-works/pi-ai";
2
2
  import { type AuthStatus, type CompatibilityRequestConfig, type ProviderConfigInput } from "./provider-composer.ts";
3
3
  export interface CreateModelRuntimeOptions {
4
4
  /** Credential storage. Defaults to the file at authPath. */
@@ -13,7 +13,7 @@ export interface CreateModelRuntimeOptions {
13
13
  modelRefreshTimeoutMs?: number;
14
14
  catalogBaseUrl?: string;
15
15
  }
16
- export interface ModelRuntimeAuthOverrides {
16
+ export interface ModelRuntimeAuthOverrides extends AuthOperationOptions {
17
17
  apiKey?: string;
18
18
  env?: Record<string, string>;
19
19
  /** Require this much remaining OAuth-token validity; defaults to five minutes. */
@@ -73,6 +73,8 @@ export declare class ModelRuntime implements Models {
73
73
  complete<TApi extends Api>(model: Model<TApi>, context: Context, options?: ModelsApiStreamOptions<TApi>): Promise<AssistantMessage>;
74
74
  streamSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): AssistantMessageEventStream;
75
75
  completeSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): Promise<AssistantMessage>;
76
+ fetchDeferred(model: Model<Api>, handle: DeferredHandle, options?: ModelsDeferredFetchOptions): Promise<AssistantMessage>;
77
+ cancelDeferred(model: Model<Api>, handle: DeferredHandle, options?: ModelsDeferredCancelOptions): Promise<void>;
76
78
  login(providerId: string, type: AuthType, interaction: AuthInteraction): Promise<Credential>;
77
79
  logout(providerId: string): Promise<void>;
78
80
  getCredentialStore(): CredentialStore;
@@ -313,10 +313,15 @@ export class ModelRuntime {
313
313
  const provider = this.models.getProvider(model.provider);
314
314
  if (!provider)
315
315
  throw new ModelsError("provider", `Unknown provider: ${model.provider}`);
316
- const resolution = await this.getAuth(model, { apiKey: options?.apiKey, env: options?.env });
316
+ const resolution = await this.getAuth(model, {
317
+ apiKey: options?.apiKey,
318
+ env: options?.env,
319
+ signal: options?.signal,
320
+ });
317
321
  if (!resolution)
318
322
  throw new ModelsError("auth", `Provider is not configured: ${model.provider}`);
319
- const { transformHeaders, ...providerOptions } = options ?? {};
323
+ const { transformHeaders, ...rawProviderOptions } = options ?? {};
324
+ const providerOptions = rawProviderOptions;
320
325
  let headers = mergeHeaders(resolution.auth.headers, providerOptions.headers);
321
326
  if (transformHeaders)
322
327
  headers = await transformHeaders(headers ?? {});
@@ -352,6 +357,22 @@ export class ModelRuntime {
352
357
  completeSimple(model, context, options) {
353
358
  return this.streamSimple(model, context, options).result();
354
359
  }
360
+ async fetchDeferred(model, handle, options) {
361
+ return lazyStream(model, async () => {
362
+ const prepared = await this.prepareRequest(model, options);
363
+ if (!prepared.provider.fetchDeferred) {
364
+ throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`);
365
+ }
366
+ return prepared.provider.fetchDeferred(prepared.model, handle, prepared.options);
367
+ }).result();
368
+ }
369
+ async cancelDeferred(model, handle, options) {
370
+ const prepared = await this.prepareRequest(model, options);
371
+ if (!prepared.provider.cancelDeferred) {
372
+ throw new ModelsError("provider", `Provider ${model.provider} does not support deferred responses`);
373
+ }
374
+ await prepared.provider.cancelDeferred(prepared.model, handle, prepared.options);
375
+ }
355
376
  async login(providerId, type, interaction) {
356
377
  const credential = await this.models.login(providerId, type, interaction);
357
378
  await this.refresh({ allowNetwork: this.modelNetworkEnabled });
@@ -38,77 +38,82 @@ function remoteModels(entry, localGeneratedAt) {
38
38
  /** Add a persisted pi.dev catalog overlay to a static built-in provider. */
39
39
  export function withRemoteCatalog(provider, catalogBaseUrl = DEFAULT_CATALOG_BASE_URL, localGeneratedAt) {
40
40
  let dynamicModels = [];
41
- let inflightRefresh;
42
41
  return {
43
42
  ...provider,
44
43
  getModels: () => mergeModels(provider.getModels(), dynamicModels),
45
- refreshModels: (context) => {
46
- inflightRefresh ??= (async () => {
47
- try {
48
- const stored = await context.store.read();
49
- dynamicModels = remoteModels(stored, localGeneratedAt).filter((model) => model.provider === provider.id);
50
- if (!context.allowNetwork || context.signal?.aborted)
51
- return;
52
- if (!context.force &&
53
- stored?.checkedAt !== undefined &&
54
- stored.lastModified !== undefined &&
55
- Date.now() - stored.checkedAt < REMOTE_CATALOG_REFRESH_INTERVAL_MS) {
56
- return;
57
- }
58
- // Only revalidate when a cached body backs the validator, so a 304 can never
59
- // leave the overlay empty.
60
- const validator = stored?.models.length ? stored.etag : undefined;
61
- const url = new URL(`/api/models/providers/${encodeURIComponent(provider.id)}`, catalogBaseUrl);
62
- const response = await fetch(url, {
63
- headers: {
64
- accept: "application/json",
65
- "User-Agent": getPiUserAgent(VERSION),
66
- ...(validator ? { "if-none-match": validator } : {}),
67
- },
68
- signal: context.signal,
69
- });
70
- if (context.signal?.aborted)
71
- return;
72
- const checkedAt = Date.now();
73
- // Unchanged: dynamicModels already holds the stored overlay, so only the
74
- // freshness window moves.
75
- if (response.status === 304 && stored) {
76
- await context.store.write({ ...stored, checkedAt });
77
- return;
78
- }
79
- if (response.status === 404 || response.status === 501) {
80
- await context.store.write({
81
- ...(stored ?? { models: [] }),
82
- checkedAt,
83
- lastModified: 0,
84
- etag: undefined,
85
- });
86
- return;
87
- }
88
- if (!response.ok) {
89
- // Transient failure: the cached body and its validator stay valid, so keep the
90
- // etag and let the next refresh revalidate instead of downloading the catalog.
91
- await context.store.write({ ...(stored ?? { models: [] }), checkedAt });
92
- throw new Error(`Model catalog request failed for ${provider.id}: ${response.status}`);
93
- }
94
- const refreshed = parseCatalog(provider.id, await response.json());
95
- const lastModified = Date.parse(response.headers.get("last-modified") ?? "");
96
- if (context.signal?.aborted)
97
- return;
98
- const entry = {
99
- models: refreshed,
44
+ refreshModels: async (context) => {
45
+ const stored = context.stored;
46
+ const restored = remoteModels(stored, localGeneratedAt).filter((model) => model.provider === provider.id);
47
+ if (!(await context.publish({
48
+ update: () => {
49
+ dynamicModels = restored;
50
+ },
51
+ }))) {
52
+ return;
53
+ }
54
+ if (!context.allowNetwork || context.signal.aborted)
55
+ return;
56
+ if (!context.force &&
57
+ stored?.checkedAt !== undefined &&
58
+ stored.lastModified !== undefined &&
59
+ Date.now() - stored.checkedAt < REMOTE_CATALOG_REFRESH_INTERVAL_MS) {
60
+ return;
61
+ }
62
+ // Only revalidate when a cached body backs the validator, so a 304 can never
63
+ // leave the overlay empty.
64
+ const validator = stored?.models.length ? stored.etag : undefined;
65
+ const url = new URL(`/api/models/providers/${encodeURIComponent(provider.id)}`, catalogBaseUrl);
66
+ const response = await fetch(url, {
67
+ headers: {
68
+ accept: "application/json",
69
+ "User-Agent": getPiUserAgent(VERSION),
70
+ ...(validator ? { "if-none-match": validator } : {}),
71
+ },
72
+ signal: context.signal,
73
+ });
74
+ if (context.signal.aborted)
75
+ return;
76
+ const checkedAt = Date.now();
77
+ // Unchanged: dynamicModels already holds the stored overlay, so only the
78
+ // freshness window moves.
79
+ if (response.status === 304 && stored) {
80
+ await context.publish({ persist: { ...stored, checkedAt } });
81
+ return;
82
+ }
83
+ if (response.status === 404 || response.status === 501) {
84
+ await context.publish({
85
+ persist: {
86
+ ...(stored ?? { models: [] }),
100
87
  checkedAt,
101
- lastModified: Number.isNaN(lastModified) ? 0 : lastModified,
102
- etag: response.headers.get("etag") ?? undefined,
103
- };
104
- dynamicModels = remoteModels(entry, localGeneratedAt);
105
- await context.store.write(entry);
106
- }
107
- finally {
108
- inflightRefresh = undefined;
109
- }
110
- })();
111
- return inflightRefresh;
88
+ lastModified: 0,
89
+ etag: undefined,
90
+ },
91
+ });
92
+ return;
93
+ }
94
+ if (!response.ok) {
95
+ // Transient failure: the cached body and its validator stay valid, so keep the
96
+ // etag and let the next refresh revalidate instead of downloading the catalog.
97
+ await context.publish({ persist: { ...(stored ?? { models: [] }), checkedAt } });
98
+ throw new Error(`Model catalog request failed for ${provider.id}: ${response.status}`);
99
+ }
100
+ const refreshed = parseCatalog(provider.id, await response.json());
101
+ const lastModified = Date.parse(response.headers.get("last-modified") ?? "");
102
+ if (context.signal.aborted)
103
+ return;
104
+ const entry = {
105
+ models: refreshed,
106
+ checkedAt,
107
+ lastModified: Number.isNaN(lastModified) ? 0 : lastModified,
108
+ etag: response.headers.get("etag") ?? undefined,
109
+ };
110
+ const published = remoteModels(entry, localGeneratedAt);
111
+ await context.publish({
112
+ persist: entry,
113
+ update: () => {
114
+ dynamicModels = published;
115
+ },
116
+ });
112
117
  },
113
118
  };
114
119
  }
@@ -1,5 +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
4
  export interface CompactionSettings {
4
5
  enabled?: boolean;
5
6
  reserveTokens?: number;
@@ -20,6 +21,7 @@ export interface RetrySettings {
20
21
  baseDelayMs?: number;
21
22
  provider?: ProviderRetrySettings;
22
23
  }
24
+ export type TuiMode = RendererTuiMode;
23
25
  export interface TerminalSettings {
24
26
  showImages?: boolean;
25
27
  imageWidthCells?: number;
@@ -93,6 +95,8 @@ export interface Settings {
93
95
  themes?: string[];
94
96
  agents?: string[];
95
97
  terminal?: TerminalSettings;
98
+ tuiMode?: TuiMode;
99
+ fullscreenScrollbar?: ScrollViewScrollbar;
96
100
  images?: ImageSettings;
97
101
  enabledModels?: string[];
98
102
  doubleEscapeAction?: "fork" | "tree" | "none";
@@ -278,6 +282,10 @@ export declare class SettingsManager {
278
282
  setClearOnShrink(enabled: boolean): void;
279
283
  getShowTerminalProgress(): boolean;
280
284
  setShowTerminalProgress(enabled: boolean): void;
285
+ getTuiMode(): TuiMode;
286
+ setTuiMode(mode: TuiMode): void;
287
+ getFullscreenScrollbar(): ScrollViewScrollbar;
288
+ setFullscreenScrollbar(mode: ScrollViewScrollbar): void;
281
289
  getImageAutoResize(): boolean;
282
290
  setImageAutoResize(enabled: boolean): void;
283
291
  getBlockImages(): boolean;
@@ -816,6 +816,24 @@ export class SettingsManager {
816
816
  this.markModified("terminal", "showTerminalProgress");
817
817
  this.save();
818
818
  }
819
+ getTuiMode() {
820
+ // Selesai defaults to Pi's native fullscreen TUI; users may explicitly choose regular.
821
+ return this.settings.tuiMode === "regular" ? "regular" : "fullscreen";
822
+ }
823
+ setTuiMode(mode) {
824
+ this.globalSettings.tuiMode = mode;
825
+ this.markModified("tuiMode");
826
+ this.save();
827
+ }
828
+ getFullscreenScrollbar() {
829
+ const mode = this.settings.fullscreenScrollbar;
830
+ return mode === "always" || mode === "hidden" ? mode : "auto";
831
+ }
832
+ setFullscreenScrollbar(mode) {
833
+ this.globalSettings.fullscreenScrollbar = mode;
834
+ this.markModified("fullscreenScrollbar");
835
+ this.save();
836
+ }
819
837
  getImageAutoResize() {
820
838
  return this.settings.images?.autoResize ?? true;
821
839
  }
@@ -7,7 +7,8 @@
7
7
  - **Guide defaults to compact mode** — When `selesaiGuide.mode` is unset, the recurring guide opens in compact mode instead of the full tour. Explicit `full`, `compact`, or `off` settings are still honored.
8
8
 
9
9
  ### Changed
10
- - **Upstream 0.12.3 merge (Selesai fork)** — Updated from upstream v0.5.4 to v0.12.3. Adopts the Powerline queue/inbox (`/idea`, `/ideas`, `/queue`, `/ideas next`, `/ideas issue`), compaction-aware queue delivery, `/cd` session directory switching, cost currency and subagent cost accounting, `powerline.separator`/`layout`/`placement`/`disabledSegments`/`welcome` options, git host icons, cached session token stats, strict TypeScript gate, and all upstream fixes through 0.12.3. Selesai-specific features are preserved: the fixed editor and chat scrolling, the guide overlay, the TPS tracker, session-usage accounting, and mode-aware autocomplete trigger forwarding.
10
+ - **Upstream 0.12.3 merge (Selesai fork)** — Updated from upstream v0.5.4 to v0.12.3. Adopts the Powerline queue/inbox (`/idea`, `/ideas`, `/queue`, `/ideas next`, `/ideas issue`), compaction-aware queue delivery, `/cd` session directory switching, cost currency and subagent cost accounting, `powerline.separator`/`layout`/`placement`/`disabledSegments`/`welcome` options, git host icons, cached session token stats, strict TypeScript gate, and all upstream fixes through 0.12.3. Selesai-specific features are preserved: the guide overlay, the TPS tracker, session-usage accounting, and mode-aware autocomplete trigger forwarding.
11
+ - **Native fullscreen dock** — Removed the unsupported terminal-split compositor and its fixed-editor/mouse-scroll/chat-jump controls. Powerline status, editor, and widgets now use Pi 0.84.1's native fullscreen dock; Selesai defaults to fullscreen while `tuiMode: "regular"` opts out.
11
12
 
12
13
 
13
14
  ## [0.12.3] - 2026-08-09