@psnext/slingcli 2.5.20260623-1 → 2.5.20260629-1

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 (108) hide show
  1. package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +5 -2
  2. package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +2 -1
  3. package/node_modules/@earendil-works/pi-agent-core/dist/harness/utils/shell-output.js +1 -1
  4. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  5. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +51 -47
  6. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js +9 -3
  7. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +9 -6
  8. package/node_modules/@earendil-works/pi-ai/dist/api/cloudflare.js +0 -18
  9. package/node_modules/@earendil-works/pi-ai/dist/api/google-generative-ai.js +6 -3
  10. package/node_modules/@earendil-works/pi-ai/dist/api/google-vertex.js +6 -3
  11. package/node_modules/@earendil-works/pi-ai/dist/api/mistral-conversations.js +1 -1
  12. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +15 -25
  13. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +30 -26
  14. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +189 -204
  15. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +24 -21
  16. package/node_modules/@earendil-works/pi-ai/dist/api/openrouter-images.js +2 -5
  17. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js +11 -2
  18. package/node_modules/@earendil-works/pi-ai/dist/auth/helpers.js +2 -2
  19. package/node_modules/@earendil-works/pi-ai/dist/auth/resolve.js +19 -4
  20. package/node_modules/@earendil-works/pi-ai/dist/compat.js +81 -7
  21. package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +45 -0
  22. package/node_modules/@earendil-works/pi-ai/dist/images-models.js +7 -3
  23. package/node_modules/@earendil-works/pi-ai/dist/index.js +1 -0
  24. package/node_modules/@earendil-works/pi-ai/dist/legacy-api-aliases.js +49 -0
  25. package/node_modules/@earendil-works/pi-ai/dist/models.js +7 -3
  26. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +25 -8
  27. package/node_modules/@earendil-works/pi-ai/dist/providers/ant-ling.models.js +3 -3
  28. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +0 -34
  29. package/node_modules/@earendil-works/pi-ai/dist/providers/cerebras.models.js +2 -0
  30. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.js +2 -2
  31. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +21 -9
  32. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js +85 -0
  33. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.js +2 -2
  34. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-workers-ai.models.js +13 -13
  35. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js +2 -2
  36. package/node_modules/@earendil-works/pi-ai/dist/providers/faux.js +1 -20
  37. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +19 -1
  38. package/node_modules/@earendil-works/pi-ai/dist/providers/huggingface.models.js +72 -0
  39. package/node_modules/@earendil-works/pi-ai/dist/providers/minimax-cn.models.js +4 -4
  40. package/node_modules/@earendil-works/pi-ai/dist/providers/minimax.models.js +4 -4
  41. package/node_modules/@earendil-works/pi-ai/dist/providers/nvidia.models.js +19 -0
  42. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +11 -11
  43. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +15 -15
  44. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +310 -39
  45. package/node_modules/@earendil-works/pi-ai/dist/providers/together.models.js +37 -18
  46. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +349 -77
  47. package/node_modules/@earendil-works/pi-ai/dist/providers/xai.models.js +7 -0
  48. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js +6 -6
  49. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js +6 -6
  50. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +95 -0
  51. package/node_modules/@earendil-works/pi-ai/dist/utils/headers.js +10 -0
  52. package/node_modules/@earendil-works/pi-ai/dist/utils/oauth/openai-codex.js +2 -2
  53. package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +86 -0
  54. package/node_modules/@earendil-works/pi-ai/package.json +1 -33
  55. package/node_modules/@earendil-works/pi-coding-agent/dist/cli/file-processor.js +13 -25
  56. package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +15 -30
  57. package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +6 -3
  58. package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js +5 -1
  59. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-registry.js +2 -4
  60. package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +1 -1
  61. package/node_modules/@earendil-works/pi-coding-agent/dist/core/resource-loader.js +2 -0
  62. package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +9 -5
  63. package/node_modules/@earendil-works/pi-coding-agent/dist/core/settings-manager.js +11 -0
  64. package/node_modules/@earendil-works/pi-coding-agent/dist/core/timings.js +24 -14
  65. package/node_modules/@earendil-works/pi-coding-agent/dist/core/tools/read.js +12 -25
  66. package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +20 -4
  67. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/assistant-message.js +9 -9
  68. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/extension-editor.js +12 -3
  69. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/session-selector.js +14 -3
  70. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/user-message.js +1 -1
  71. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +37 -25
  72. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +14 -0
  73. package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +16 -0
  74. package/node_modules/@earendil-works/pi-coding-agent/dist/rpc-entry.js +10 -0
  75. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-convert.js +21 -15
  76. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/image-process.js +83 -0
  77. package/node_modules/@earendil-works/pi-coding-agent/dist/utils/mime.js +41 -0
  78. package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +12 -12
  79. package/node_modules/@earendil-works/pi-coding-agent/package.json +8 -5
  80. package/node_modules/@earendil-works/pi-orchestrator/dist/cli.js +136 -0
  81. package/node_modules/@earendil-works/pi-orchestrator/dist/config.js +55 -0
  82. package/node_modules/@earendil-works/pi-orchestrator/dist/handler.js +105 -0
  83. package/node_modules/@earendil-works/pi-orchestrator/dist/index.js +11 -0
  84. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/client.js +56 -0
  85. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/protocol.js +12 -0
  86. package/node_modules/@earendil-works/pi-orchestrator/dist/ipc/server.js +145 -0
  87. package/node_modules/@earendil-works/pi-orchestrator/dist/radius.js +365 -0
  88. package/node_modules/@earendil-works/pi-orchestrator/dist/rpc-process.js +168 -0
  89. package/node_modules/@earendil-works/pi-orchestrator/dist/serve.js +70 -0
  90. package/node_modules/@earendil-works/pi-orchestrator/dist/storage.js +58 -0
  91. package/node_modules/@earendil-works/pi-orchestrator/dist/supervisor.js +290 -0
  92. package/node_modules/@earendil-works/pi-orchestrator/dist/types.js +2 -0
  93. package/node_modules/@earendil-works/pi-orchestrator/package.json +45 -0
  94. package/node_modules/@earendil-works/pi-tui/dist/components/markdown.js +3 -0
  95. package/node_modules/@earendil-works/pi-tui/package.json +1 -1
  96. package/node_modules/@types/node/README.md +1 -1
  97. package/node_modules/@types/node/diagnostics_channel.d.ts +17 -20
  98. package/node_modules/@types/node/package.json +2 -2
  99. package/node_modules/brace-expansion/dist/commonjs/index.js +24 -14
  100. package/node_modules/brace-expansion/dist/commonjs/index.js.map +1 -1
  101. package/node_modules/brace-expansion/dist/esm/index.js +24 -14
  102. package/node_modules/brace-expansion/dist/esm/index.js.map +1 -1
  103. package/node_modules/brace-expansion/package.json +2 -2
  104. package/node_modules/google-auth-library/build/src/auth/googleauth.js +1 -1
  105. package/node_modules/google-auth-library/package.json +4 -4
  106. package/package.json +7 -6
  107. package/slingshot/index.js +302 -304
  108. package/node_modules/@earendil-works/pi-ai/dist/api-registry.js +0 -44
@@ -11,7 +11,7 @@ export const defaultModelPerProvider = {
11
11
  "amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
12
12
  "ant-ling": "Ring-2.6-1T",
13
13
  anthropic: "claude-opus-4-8",
14
- openai: "gpt-5.4",
14
+ openai: "gpt-5.5",
15
15
  "azure-openai-responses": "gpt-5.4",
16
16
  "openai-codex": "gpt-5.5",
17
17
  nvidia: "nvidia/nemotron-3-super-120b-a12b",
@@ -11,6 +11,7 @@ import { loadPromptTemplates } from "./prompt-templates.js";
11
11
  import { SettingsManager } from "./settings-manager.js";
12
12
  import { loadSkills } from "./skills.js";
13
13
  import { createSourceInfo } from "./source-info.js";
14
+ import { resetTimings } from "./timings.js";
14
15
  function resolvePromptInput(input, description) {
15
16
  if (!input) {
16
17
  return undefined;
@@ -216,6 +217,7 @@ export class DefaultResourceLoader {
216
217
  return this.loadCurrentExtensionSet({ includeInlineFactories: true });
217
218
  }
218
219
  async reload(options) {
220
+ resetTimings("extensions");
219
221
  if (this.loaded) {
220
222
  clearExtensionCache();
221
223
  }
@@ -533,10 +533,13 @@ export class SessionManager {
533
533
  this.sessionFile = resolvePath(sessionFile);
534
534
  if (existsSync(this.sessionFile)) {
535
535
  this.fileEntries = loadEntriesFromFile(this.sessionFile);
536
- // If file was empty or corrupted (no valid header), truncate and start fresh
537
- // to avoid appending messages without a session header (which breaks the session)
536
+ // If file was empty, initialize it with a valid session header. If it was
537
+ // non-empty but did not parse as a pi session, fail without modifying it.
538
538
  if (this.fileEntries.length === 0) {
539
539
  const explicitPath = this.sessionFile;
540
+ if (statSync(explicitPath).size > 0) {
541
+ throw new Error(`Session file is not a valid pi session: ${explicitPath}`);
542
+ }
540
543
  this.newSession();
541
544
  this.sessionFile = explicitPath;
542
545
  this._rewriteFile();
@@ -744,12 +747,13 @@ export class SessionManager {
744
747
  }
745
748
  /** Append a session info entry (e.g., display name). Returns entry id. */
746
749
  appendSessionInfo(name) {
750
+ const sanitizedName = name.replace(/[\r\n]+/g, " ").trim();
747
751
  const entry = {
748
752
  type: "session_info",
749
753
  id: generateId(this.byId),
750
754
  parentId: this.leafId,
751
755
  timestamp: new Date().toISOString(),
752
- name: name.trim(),
756
+ name: sanitizedName,
753
757
  };
754
758
  this._appendEntry(entry);
755
759
  return entry.id;
@@ -1112,8 +1116,8 @@ export class SessionManager {
1112
1116
  return new SessionManager(cwd, dir, undefined, true);
1113
1117
  }
1114
1118
  /** Create an in-memory session (no file persistence) */
1115
- static inMemory(cwd = process.cwd()) {
1116
- return new SessionManager(cwd, "", undefined, false);
1119
+ static inMemory(cwd = process.cwd(), options) {
1120
+ return new SessionManager(cwd, "", undefined, false, options);
1117
1121
  }
1118
1122
  /**
1119
1123
  * Fork a session from another project directory into the current project.
@@ -581,6 +581,17 @@ export class SettingsManager {
581
581
  getHideThinkingBlock() {
582
582
  return this.settings.hideThinkingBlock ?? false;
583
583
  }
584
+ getExternalEditorCommand() {
585
+ const configuredEditor = this.settings.externalEditor;
586
+ if (typeof configuredEditor === "string" && configuredEditor.trim() !== "") {
587
+ return configuredEditor;
588
+ }
589
+ const environmentEditor = process.env.VISUAL || process.env.EDITOR;
590
+ if (environmentEditor) {
591
+ return environmentEditor;
592
+ }
593
+ return process.platform === "win32" ? "notepad" : "nano";
594
+ }
584
595
  setHideThinkingBlock(hide) {
585
596
  this.globalSettings.hideThinkingBlock = hide;
586
597
  this.markModified("hideThinkingBlock");
@@ -3,29 +3,39 @@
3
3
  * Enable with PI_TIMING=1 environment variable.
4
4
  */
5
5
  const ENABLED = process.env.PI_TIMING === "1";
6
- const timings = [];
7
- let lastTime = Date.now();
8
- export function resetTimings() {
6
+ const timingNamespaces = new Map();
7
+ export function resetTimings(namespace = "main") {
9
8
  if (!ENABLED)
10
9
  return;
11
- timings.length = 0;
12
- lastTime = Date.now();
10
+ timingNamespaces.set(namespace, { timings: [], lastTime: Date.now() });
13
11
  }
14
- export function time(label) {
12
+ export function time(label, namespace = "main") {
15
13
  if (!ENABLED)
16
14
  return;
17
15
  const now = Date.now();
18
- timings.push({ label, ms: now - lastTime });
19
- lastTime = now;
16
+ if (!timingNamespaces.has(namespace)) {
17
+ resetTimings(namespace);
18
+ }
19
+ const timingNamespace = timingNamespaces.get(namespace);
20
+ timingNamespace.timings.push({ label, ms: now - timingNamespace.lastTime });
21
+ timingNamespace.lastTime = now;
20
22
  }
21
- export function printTimings() {
22
- if (!ENABLED || timings.length === 0)
23
+ function printTimingGroup(title, timings) {
24
+ const printableTimings = timings.filter((timing) => timing.ms >= 0);
25
+ if (printableTimings.length === 0)
23
26
  return;
24
- console.error("\n--- Startup Timings ---");
25
- for (const t of timings) {
27
+ console.error(`\n--- ${title} ---`);
28
+ for (const t of printableTimings) {
26
29
  console.error(` ${t.label}: ${t.ms}ms`);
27
30
  }
28
- console.error(` TOTAL: ${timings.reduce((a, b) => a + b.ms, 0)}ms`);
29
- console.error("------------------------\n");
31
+ console.error(` TOTAL: ${printableTimings.reduce((a, b) => a + b.ms, 0)}ms`);
32
+ console.error(`${"-".repeat(title.length + 8)}\n`);
33
+ }
34
+ export function printTimings() {
35
+ if (!ENABLED)
36
+ return;
37
+ for (const [namespace, timingNamespace] of timingNamespaces) {
38
+ printTimingGroup(`Startup Timings: ${namespace}`, timingNamespace.timings);
39
+ }
30
40
  }
31
41
  //# sourceMappingURL=timings.js.map
@@ -6,7 +6,7 @@ import { Type } from "typebox";
6
6
  import { getReadmePath } from "../../config.js";
7
7
  import { keyHint, keyText } from "../../modes/interactive/components/keybinding-hints.js";
8
8
  import { getLanguageFromPath, highlightCode } from "../../modes/interactive/theme/theme.js";
9
- import { formatDimensionNote, resizeImage } from "../../utils/image-resize.js";
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
12
  import { resolveReadPathAsync, resolveToCwd } from "./path-utils.js";
@@ -133,7 +133,7 @@ export function createReadToolDefinition(cwd, options) {
133
133
  return {
134
134
  name: "read",
135
135
  label: "read",
136
- description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
136
+ description: `Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
137
137
  promptSnippet: "Read file contents",
138
138
  promptGuidelines: ["Use read to examine files instead of cat or sed."],
139
139
  parameters: readSchema,
@@ -165,35 +165,22 @@ export function createReadToolDefinition(cwd, options) {
165
165
  if (mimeType) {
166
166
  // Read image as binary.
167
167
  const buffer = await ops.readFile(absolutePath);
168
- if (autoResizeImages) {
169
- // Resize image if needed before sending it back to the model.
170
- const resized = await resizeImage(buffer, mimeType);
171
- if (!resized) {
172
- let textNote = `Read image file [${mimeType}]\n[Image omitted: could not be resized below the inline image size limit.]`;
173
- if (nonVisionImageNote)
174
- textNote += `\n${nonVisionImageNote}`;
175
- content = [{ type: "text", text: textNote }];
176
- }
177
- else {
178
- const dimensionNote = formatDimensionNote(resized);
179
- let textNote = `Read image file [${resized.mimeType}]`;
180
- if (dimensionNote)
181
- textNote += `\n${dimensionNote}`;
182
- if (nonVisionImageNote)
183
- textNote += `\n${nonVisionImageNote}`;
184
- content = [
185
- { type: "text", text: textNote },
186
- { type: "image", data: resized.data, mimeType: resized.mimeType },
187
- ];
188
- }
168
+ const processed = await processImage(buffer, mimeType, { autoResizeImages });
169
+ if (!processed.ok) {
170
+ let textNote = `Read image file [${mimeType}]\n${processed.message}`;
171
+ if (nonVisionImageNote)
172
+ textNote += `\n${nonVisionImageNote}`;
173
+ content = [{ type: "text", text: textNote }];
189
174
  }
190
175
  else {
191
- let textNote = `Read image file [${mimeType}]`;
176
+ let textNote = `Read image file [${processed.mimeType}]`;
177
+ if (processed.hints.length > 0)
178
+ textNote += `\n${processed.hints.join("\n")}`;
192
179
  if (nonVisionImageNote)
193
180
  textNote += `\n${nonVisionImageNote}`;
194
181
  content = [
195
182
  { type: "text", text: textNote },
196
- { type: "image", data: buffer.toString("base64"), mimeType },
183
+ { type: "image", data: processed.data, mimeType: processed.mimeType },
197
184
  ];
198
185
  }
199
186
  }
@@ -35,6 +35,7 @@ import { initTheme, stopThemeWatcher } from "./modes/interactive/theme/theme.js"
35
35
  import { handleConfigCommand, handlePackageCommand } from "./package-manager-cli.js";
36
36
  import { isLocalPath, normalizePath, resolvePath } from "./utils/paths.js";
37
37
  import { cleanupWindowsSelfUpdateQuarantine } from "./utils/windows-self-update.js";
38
+ const EXTENSION_LOAD_FAILURE_HINT = 'Hint: Start without extensions using "sling -ne".';
38
39
  /**
39
40
  * Read all content from piped stdin.
40
41
  * Returns undefined if stdin is a TTY (interactive terminal).
@@ -167,7 +168,6 @@ function validateSessionIdFlags(parsed) {
167
168
  parsed.session ? "--session" : undefined,
168
169
  parsed.continue ? "--continue" : undefined,
169
170
  parsed.resume ? "--resume" : undefined,
170
- parsed.noSession ? "--no-session" : undefined,
171
171
  ].filter((flag) => flag !== undefined);
172
172
  if (conflictingFlags.length > 0) {
173
173
  console.error(chalk.red(`Error: --session-id cannot be combined with ${conflictingFlags.join(", ")}`));
@@ -182,6 +182,16 @@ function validateSessionIdFlags(parsed) {
182
182
  process.exit(1);
183
183
  }
184
184
  }
185
+ function openSessionOrExit(path, sessionDir) {
186
+ try {
187
+ return SessionManager.open(path, sessionDir);
188
+ }
189
+ catch (error) {
190
+ const message = error instanceof Error ? error.message : String(error);
191
+ console.error(chalk.red(`Error: ${message}`));
192
+ process.exit(1);
193
+ }
194
+ }
185
195
  function forkSessionOrExit(sourcePath, cwd, sessionDir, sessionId) {
186
196
  try {
187
197
  return SessionManager.forkFrom(sourcePath, cwd, sessionDir, { id: sessionId });
@@ -194,7 +204,7 @@ function forkSessionOrExit(sourcePath, cwd, sessionDir, sessionId) {
194
204
  }
195
205
  async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
196
206
  if (parsed.noSession || parsed.help || parsed.listModels !== undefined) {
197
- return SessionManager.inMemory(cwd);
207
+ return SessionManager.inMemory(cwd, parsed.sessionId !== undefined ? { id: parsed.sessionId } : undefined);
198
208
  }
199
209
  if (parsed.fork) {
200
210
  if (parsed.sessionId) {
@@ -220,7 +230,7 @@ async function createSessionManager(parsed, cwd, sessionDir, settingsManager) {
220
230
  switch (resolved.type) {
221
231
  case "path":
222
232
  case "local":
223
- return SessionManager.open(resolved.path, sessionDir);
233
+ return openSessionOrExit(resolved.path, sessionDir);
224
234
  case "global": {
225
235
  console.log(chalk.yellow(`Session found in different project: ${resolved.cwd}`));
226
236
  const shouldFork = await promptConfirm("Fork this session into current directory?");
@@ -621,6 +631,9 @@ export async function main(args, options) {
621
631
  time("resolveModelScope");
622
632
  reportDiagnostics(runtime.diagnostics);
623
633
  if (runtime.diagnostics.some((diagnostic) => diagnostic.type === "error")) {
634
+ if (runtime.diagnostics.some((diagnostic) => diagnostic.message.includes("Failed to load extension"))) {
635
+ console.error(chalk.yellow(EXTENSION_LOAD_FAILURE_HINT));
636
+ }
624
637
  process.exit(1);
625
638
  }
626
639
  time("createAgentSession");
@@ -650,9 +663,12 @@ export async function main(args, options) {
650
663
  if (startupBenchmark) {
651
664
  await interactiveMode.init();
652
665
  time("interactiveMode.init");
653
- printTimings();
666
+ // Give the TUI's stdin handler a brief chance to consume terminal query replies
667
+ // (Kitty keyboard protocol, device attributes, cell size) before restoring the terminal.
668
+ await new Promise((resolve) => setTimeout(resolve, 150));
654
669
  interactiveMode.stop();
655
670
  stopThemeWatcher();
671
+ printTimings();
656
672
  if (process.stdout.writableLength > 0) {
657
673
  await new Promise((resolve) => process.stdout.once("drain", resolve));
658
674
  }
@@ -93,21 +93,21 @@ export class AssistantMessageComponent extends Container {
93
93
  }
94
94
  }
95
95
  }
96
- // Check if aborted - show after partial content
97
- // But only if there are no tool calls (tool execution components will show the error)
96
+ // Check if incomplete/failed - show after partial content.
97
+ // For aborted/error tool calls, tool execution components show the error.
98
+ // Length stops can happen before a tool call is complete, so surface them here too.
98
99
  const hasToolCalls = message.content.some((c) => c.type === "toolCall");
99
100
  this.hasToolCalls = hasToolCalls;
100
- if (!hasToolCalls) {
101
+ if (message.stopReason === "length") {
102
+ this.contentContainer.addChild(new Spacer(1));
103
+ this.contentContainer.addChild(new Text(theme.fg("error", "Error: Model stopped because it reached the maximum output token limit. The response may be incomplete."), 1, 0));
104
+ }
105
+ else if (!hasToolCalls) {
101
106
  if (message.stopReason === "aborted") {
102
107
  const abortMessage = message.errorMessage && message.errorMessage !== "Request was aborted"
103
108
  ? message.errorMessage
104
109
  : "Operation aborted";
105
- if (hasVisibleContent) {
106
- this.contentContainer.addChild(new Spacer(1));
107
- }
108
- else {
109
- this.contentContainer.addChild(new Spacer(1));
110
- }
110
+ this.contentContainer.addChild(new Spacer(1));
111
111
  this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
112
112
  }
113
113
  else if (message.stopReason === "error") {
@@ -16,6 +16,7 @@ export class ExtensionEditorComponent extends Container {
16
16
  onCancelCallback;
17
17
  tui;
18
18
  keybindings;
19
+ externalEditorCommand;
19
20
  _focused = false;
20
21
  get focused() {
21
22
  return this._focused;
@@ -24,10 +25,11 @@ export class ExtensionEditorComponent extends Container {
24
25
  this._focused = value;
25
26
  this.editor.focused = value;
26
27
  }
27
- constructor(tui, keybindings, title, prefill, onSubmit, onCancel, options) {
28
+ constructor(tui, keybindings, title, prefill, onSubmit, onCancel, options, externalEditorCommand) {
28
29
  super();
29
30
  this.tui = tui;
30
31
  this.keybindings = keybindings;
32
+ this.externalEditorCommand = externalEditorCommand;
31
33
  this.onSubmitCallback = onSubmit;
32
34
  this.onCancelCallback = onCancel;
33
35
  // Add top border
@@ -48,7 +50,7 @@ export class ExtensionEditorComponent extends Container {
48
50
  this.addChild(this.editor);
49
51
  this.addChild(new Spacer(1));
50
52
  // Add hint
51
- const hasExternalEditor = !!(process.env.VISUAL || process.env.EDITOR);
53
+ const hasExternalEditor = !!this.getExternalEditorCommand();
52
54
  const hint = keyHint("tui.select.confirm", "submit") +
53
55
  " " +
54
56
  keyHint("tui.input.newLine", "newline") +
@@ -75,8 +77,15 @@ export class ExtensionEditorComponent extends Container {
75
77
  // Forward to editor
76
78
  this.editor.handleInput(keyData);
77
79
  }
80
+ getExternalEditorCommand() {
81
+ const editorCmd = this.externalEditorCommand || process.env.VISUAL || process.env.EDITOR;
82
+ if (editorCmd) {
83
+ return editorCmd;
84
+ }
85
+ return process.platform === "win32" ? "notepad" : "nano";
86
+ }
78
87
  async openExternalEditor() {
79
- const editorCmd = process.env.VISUAL || process.env.EDITOR;
88
+ const editorCmd = this.getExternalEditorCommand();
80
89
  if (!editorCmd) {
81
90
  return;
82
91
  }
@@ -168,7 +168,7 @@ function buildSessionTree(sessions) {
168
168
  const byPath = new Map();
169
169
  for (const session of sessions) {
170
170
  const sessionPath = canonicalizePath(session.path) ?? session.path;
171
- byPath.set(sessionPath, { session, children: [] });
171
+ byPath.set(sessionPath, { session, children: [], latestActivity: session.modified.getTime() });
172
172
  }
173
173
  const roots = [];
174
174
  for (const session of sessions) {
@@ -182,9 +182,20 @@ function buildSessionTree(sessions) {
182
182
  roots.push(node);
183
183
  }
184
184
  }
185
- // Sort children and roots by modified date (descending)
185
+ const updateLatestActivity = (node) => {
186
+ let latestActivity = node.session.modified.getTime();
187
+ for (const child of node.children) {
188
+ latestActivity = Math.max(latestActivity, updateLatestActivity(child));
189
+ }
190
+ node.latestActivity = latestActivity;
191
+ return latestActivity;
192
+ };
193
+ for (const root of roots) {
194
+ updateLatestActivity(root);
195
+ }
196
+ // Sort children and roots by latest activity in each subtree (descending)
186
197
  const sortNodes = (nodes) => {
187
- nodes.sort((a, b) => b.session.modified.getTime() - a.session.modified.getTime());
198
+ nodes.sort((a, b) => b.latestActivity - a.latestActivity);
188
199
  for (const node of nodes) {
189
200
  sortNodes(node.children);
190
201
  }
@@ -13,7 +13,7 @@ export class UserMessageComponent extends Container {
13
13
  this.contentBox = new Box(1, 1, (content) => theme.bg("userMessageBg", content));
14
14
  this.contentBox.addChild(new Markdown(text, 0, 0, markdownTheme, {
15
15
  color: (content) => theme.fg("userMessageText", content),
16
- }, { preserveOrderedListMarkers: true }));
16
+ }, { preserveOrderedListMarkers: true, preserveBackslashEscapes: true }));
17
17
  this.addChild(this.contentBox);
18
18
  }
19
19
  render(width) {
@@ -134,6 +134,7 @@ export function isApiKeyLoginProvider(providerId, oauthProviderIds, builtInProvi
134
134
  export class InteractiveMode {
135
135
  runtimeHost;
136
136
  ui;
137
+ loadedResourcesContainer;
137
138
  chatContainer;
138
139
  pendingMessagesContainer;
139
140
  statusContainer;
@@ -246,6 +247,7 @@ export class InteractiveMode {
246
247
  this.ui = new TUI(new ProcessTerminal(), this.settingsManager.getShowHardwareCursor());
247
248
  this.ui.setClearOnShrink(this.settingsManager.getClearOnShrink());
248
249
  this.headerContainer = new Container();
250
+ this.loadedResourcesContainer = new Container();
249
251
  this.chatContainer = new Container();
250
252
  this.pendingMessagesContainer = new Container();
251
253
  this.statusContainer = new Container();
@@ -439,8 +441,10 @@ export class InteractiveMode {
439
441
  : "";
440
442
  console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
441
443
  }
442
- // Add header container as first child. Populate it after detectThemeIfUnset.
444
+ // Add header container as first child. Populate it after applying theme settings.
445
+ // Keep loaded resources before chat so restored session messages never precede them.
443
446
  this.ui.addChild(this.headerContainer);
447
+ this.ui.addChild(this.loadedResourcesContainer);
444
448
  this.ui.addChild(this.chatContainer);
445
449
  this.ui.addChild(this.pendingMessagesContainer);
446
450
  this.ui.addChild(this.statusContainer);
@@ -490,7 +494,7 @@ export class InteractiveMode {
490
494
  hint("app.tools.expand", "more"),
491
495
  ].join(theme.fg("muted", " · "));
492
496
  const compactOnboarding = theme.fg("dim", `Press ${keyText("app.tools.expand")} to show full startup help and loaded resources.`);
493
- const onboarding = theme.fg("dim", `Sling can explain its own features and look up its docs. Ask it how to use or extend Sling.`);
497
+ const onboarding = theme.fg("dim", `Pi can explain its own features and look up its docs. Ask it how to use or extend Pi.`);
494
498
  this.builtInHeader = new ExpandableText(() => `${logo}\n${compactInstructions}\n${compactOnboarding}\n\n${onboarding}`, () => `${logo}\n${expandedInstructions}\n\n${onboarding}`, this.getStartupExpansionState(), 1, 0);
495
499
  // Setup UI layout
496
500
  this.headerContainer.addChild(new Spacer(1));
@@ -657,7 +661,7 @@ export class InteractiveMode {
657
661
  return "tmux extended-keys is off. Modified Enter keys may not work. Add `set -g extended-keys on` to ~/.tmux.conf and restart tmux.";
658
662
  }
659
663
  if (extendedKeysFormat === "xterm") {
660
- return "tmux extended-keys-format is xterm. Sling works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
664
+ return "tmux extended-keys-format is xterm. Pi works best with csi-u. Add `set -g extended-keys-format csi-u` to ~/.tmux.conf and restart tmux.";
661
665
  }
662
666
  return undefined;
663
667
  }
@@ -990,6 +994,8 @@ export class InteractiveMode {
990
994
  return lines.join("\n");
991
995
  }
992
996
  showLoadedResources(options) {
997
+ // Resource rendering is idempotent; chat clears no longer clear this separate container.
998
+ this.loadedResourcesContainer.clear();
993
999
  const showListing = options?.force || this.options.verbose || !this.settingsManager.getQuietStartup();
994
1000
  const showDiagnostics = showListing || options?.showDiagnosticsWhenQuiet === true;
995
1001
  if (!showListing && !showDiagnostics) {
@@ -1005,8 +1011,8 @@ export class InteractiveMode {
1005
1011
  };
1006
1012
  const addLoadedSection = (name, collapsedBody, expandedBody = collapsedBody, color = "mdHeading") => {
1007
1013
  const section = new ExpandableText(() => `${sectionHeader(name, color)}\n${collapsedBody}`, () => `${sectionHeader(name, color)}\n${expandedBody}`, this.getStartupExpansionState(), 0, 0);
1008
- this.chatContainer.addChild(section);
1009
- this.chatContainer.addChild(new Spacer(1));
1014
+ this.loadedResourcesContainer.addChild(section);
1015
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1010
1016
  };
1011
1017
  const skillsResult = this.session.resourceLoader.getSkills();
1012
1018
  const promptsResult = this.session.resourceLoader.getPrompts();
@@ -1040,7 +1046,7 @@ export class InteractiveMode {
1040
1046
  if (showListing) {
1041
1047
  const contextFiles = this.session.resourceLoader.getAgentsFiles().agentsFiles;
1042
1048
  if (contextFiles.length > 0) {
1043
- this.chatContainer.addChild(new Spacer(1));
1049
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1044
1050
  const contextList = contextFiles
1045
1051
  .map((f) => theme.fg("dim", ` ${this.formatDisplayPath(f.path)}`))
1046
1052
  .join("\n");
@@ -1103,14 +1109,14 @@ export class InteractiveMode {
1103
1109
  const skillDiagnostics = skillsResult.diagnostics;
1104
1110
  if (skillDiagnostics.length > 0) {
1105
1111
  const warningLines = this.formatDiagnostics(skillDiagnostics, sourceInfos);
1106
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1107
- this.chatContainer.addChild(new Spacer(1));
1112
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Skill conflicts]")}\n${warningLines}`, 0, 0));
1113
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1108
1114
  }
1109
1115
  const promptDiagnostics = promptsResult.diagnostics;
1110
1116
  if (promptDiagnostics.length > 0) {
1111
1117
  const warningLines = this.formatDiagnostics(promptDiagnostics, sourceInfos);
1112
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1113
- this.chatContainer.addChild(new Spacer(1));
1118
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Prompt conflicts]")}\n${warningLines}`, 0, 0));
1119
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1114
1120
  }
1115
1121
  const extensionDiagnostics = [];
1116
1122
  const extensionErrors = this.session.resourceLoader.getExtensions().errors;
@@ -1126,14 +1132,14 @@ export class InteractiveMode {
1126
1132
  extensionDiagnostics.push(...shortcutDiagnostics);
1127
1133
  if (extensionDiagnostics.length > 0) {
1128
1134
  const warningLines = this.formatDiagnostics(extensionDiagnostics, sourceInfos);
1129
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1130
- this.chatContainer.addChild(new Spacer(1));
1135
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Extension issues]")}\n${warningLines}`, 0, 0));
1136
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1131
1137
  }
1132
1138
  const themeDiagnostics = themesResult.diagnostics;
1133
1139
  if (themeDiagnostics.length > 0) {
1134
1140
  const warningLines = this.formatDiagnostics(themeDiagnostics, sourceInfos);
1135
- this.chatContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1136
- this.chatContainer.addChild(new Spacer(1));
1141
+ this.loadedResourcesContainer.addChild(new Text(`${theme.fg("warning", "[Theme conflicts]")}\n${warningLines}`, 0, 0));
1142
+ this.loadedResourcesContainer.addChild(new Spacer(1));
1137
1143
  }
1138
1144
  }
1139
1145
  }
@@ -1261,6 +1267,7 @@ export class InteractiveMode {
1261
1267
  process.exit(1);
1262
1268
  }
1263
1269
  renderCurrentSessionState() {
1270
+ this.loadedResourcesContainer.clear();
1264
1271
  this.chatContainer.clear();
1265
1272
  this.pendingMessagesContainer.clear();
1266
1273
  this.compactionQueuedMessages = [];
@@ -1741,7 +1748,7 @@ export class InteractiveMode {
1741
1748
  }, () => {
1742
1749
  this.hideExtensionEditor();
1743
1750
  resolve(undefined);
1744
- });
1751
+ }, undefined, this.settingsManager.getExternalEditorCommand());
1745
1752
  this.editorContainer.clear();
1746
1753
  this.editorContainer.addChild(this.extensionEditor);
1747
1754
  this.ui.setFocus(this.extensionEditor);
@@ -2677,7 +2684,7 @@ export class InteractiveMode {
2677
2684
  if (this.chatContainer.children.length > 0) {
2678
2685
  this.chatContainer.addChild(new Spacer(1));
2679
2686
  }
2680
- this.chatContainer.addChild(new Text(theme.fg("warning", `This project is not trusted. Project ${CONFIG_DIR_NAME} resources and packages are ignored. Use /trust to save a trust decision, then restart sling.`), 1, 0));
2687
+ this.chatContainer.addChild(new Text(theme.fg("warning", `This project is not trusted. Project ${CONFIG_DIR_NAME} resources and packages are ignored. Use /trust to save a trust decision, then restart pi.`), 1, 0));
2681
2688
  }
2682
2689
  async getUserInput() {
2683
2690
  const queuedInput = this.pendingUserInputs.shift();
@@ -2791,7 +2798,7 @@ export class InteractiveMode {
2791
2798
  this.ui.stop();
2792
2799
  }
2793
2800
  catch { }
2794
- console.error("sling exiting due to uncaughtException:");
2801
+ console.error("pi exiting due to uncaughtException:");
2795
2802
  console.error(error);
2796
2803
  process.exit(1);
2797
2804
  }
@@ -2965,9 +2972,11 @@ export class InteractiveMode {
2965
2972
  if (isExpandable(activeHeader)) {
2966
2973
  activeHeader.setExpanded(expanded);
2967
2974
  }
2968
- for (const child of this.chatContainer.children) {
2969
- if (isExpandable(child)) {
2970
- child.setExpanded(expanded);
2975
+ for (const container of [this.loadedResourcesContainer, this.chatContainer]) {
2976
+ for (const child of container.children) {
2977
+ if (isExpandable(child)) {
2978
+ child.setExpanded(expanded);
2979
+ }
2971
2980
  }
2972
2981
  }
2973
2982
  this.ui.requestRender();
@@ -2987,10 +2996,9 @@ export class InteractiveMode {
2987
2996
  this.showStatus(`Thinking blocks: ${this.hideThinkingBlock ? "hidden" : "visible"}`);
2988
2997
  }
2989
2998
  async openExternalEditor() {
2990
- // Determine editor (respect $VISUAL, then $EDITOR)
2991
- const editorCmd = process.env.VISUAL || process.env.EDITOR;
2999
+ const editorCmd = this.settingsManager.getExternalEditorCommand();
2992
3000
  if (!editorCmd) {
2993
- this.showWarning("No editor configured. Set $VISUAL or $EDITOR environment variable.");
3001
+ this.showWarning("No editor configured. Set externalEditor in settings.json or $VISUAL/$EDITOR.");
2994
3002
  return;
2995
3003
  }
2996
3004
  const currentText = this.editor.getExpandedText?.() ?? this.editor.getText();
@@ -3527,7 +3535,7 @@ export class InteractiveMode {
3527
3535
  onSelect: (selection) => {
3528
3536
  trustStore.setMany(selection.updates);
3529
3537
  done();
3530
- this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart sling for this to take effect.`);
3538
+ this.showStatus(`Saved trust decision: ${selection.trusted ? "trusted" : "untrusted"}. Restart pi for this to take effect.`);
3531
3539
  },
3532
3540
  onCancel: () => {
3533
3541
  done();
@@ -4454,8 +4462,12 @@ export class InteractiveMode {
4454
4462
  return;
4455
4463
  }
4456
4464
  this.session.setSessionName(name);
4465
+ const sessionName = this.sessionManager.getSessionName();
4466
+ if (sessionName !== name) {
4467
+ this.showWarning(`Session name was normalized from ${JSON.stringify(name)} to ${JSON.stringify(sessionName)}`);
4468
+ }
4457
4469
  this.chatContainer.addChild(new Spacer(1));
4458
- this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${name}`), 1, 0));
4470
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Session name set: ${sessionName ?? name}`), 1, 0));
4459
4471
  this.ui.requestRender();
4460
4472
  }
4461
4473
  handleSessionCommand() {
@@ -298,6 +298,20 @@ export class RpcClient {
298
298
  const response = await this.send({ type: "get_fork_messages" });
299
299
  return this.getData(response).messages;
300
300
  }
301
+ /**
302
+ * Get session entries in append order, optionally only those after the `since` entry id.
303
+ */
304
+ async getEntries(since) {
305
+ const response = await this.send({ type: "get_entries", since });
306
+ return this.getData(response);
307
+ }
308
+ /**
309
+ * Get the session entry tree.
310
+ */
311
+ async getTree() {
312
+ const response = await this.send({ type: "get_tree" });
313
+ return this.getData(response);
314
+ }
301
315
  /**
302
316
  * Get text of last assistant message.
303
317
  */
@@ -477,6 +477,22 @@ export async function runRpcMode(runtimeHost) {
477
477
  const messages = session.getUserMessagesForForking();
478
478
  return success(id, "get_fork_messages", { messages });
479
479
  }
480
+ case "get_entries": {
481
+ const sessionManager = session.sessionManager;
482
+ let entries = sessionManager.getEntries();
483
+ if (command.since !== undefined) {
484
+ const sinceIndex = entries.findIndex((e) => e.id === command.since);
485
+ if (sinceIndex === -1) {
486
+ return error(id, "get_entries", `Entry not found: ${command.since}`);
487
+ }
488
+ entries = entries.slice(sinceIndex + 1);
489
+ }
490
+ return success(id, "get_entries", { entries, leafId: sessionManager.getLeafId() });
491
+ }
492
+ case "get_tree": {
493
+ const sessionManager = session.sessionManager;
494
+ return success(id, "get_tree", { tree: sessionManager.getTree(), leafId: sessionManager.getLeafId() });
495
+ }
480
496
  case "get_last_assistant_text": {
481
497
  const text = session.getLastAssistantText();
482
498
  return success(id, "get_last_assistant_text", { text });