@oh-my-pi/pi-coding-agent 15.2.3 → 15.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/dist/types/config/settings-schema.d.ts +34 -1
- package/dist/types/config/settings.d.ts +6 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/goals/runtime.d.ts +4 -0
- package/dist/types/hashline/constants.d.ts +0 -2
- package/dist/types/hashline/hash.d.ts +13 -39
- package/dist/types/hashline/parser.d.ts +2 -6
- package/dist/types/modes/components/status-line/types.d.ts +10 -0
- package/dist/types/modes/components/status-line.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/shared.d.ts +9 -0
- package/dist/types/modes/theme/shimmer.d.ts +6 -3
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/modes/utils/context-usage.d.ts +17 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +5 -1
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -1
- package/dist/types/task/types.d.ts +35 -0
- package/dist/types/tools/bash-command-fixup.d.ts +0 -5
- package/dist/types/utils/clipboard.d.ts +3 -1
- package/dist/types/utils/image-resize.d.ts +4 -1
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +1 -8
- package/src/config/settings-schema.ts +29 -1
- package/src/config/settings.ts +19 -0
- package/src/discovery/helpers.ts +5 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +5 -7
- package/src/edit/streaming.ts +24 -12
- package/src/extensibility/plugins/legacy-pi-compat.ts +27 -5
- package/src/goals/runtime.ts +35 -13
- package/src/hashline/constants.ts +0 -3
- package/src/hashline/diff.ts +1 -1
- package/src/hashline/execute.ts +2 -2
- package/src/hashline/grammar.lark +7 -8
- package/src/hashline/hash.ts +21 -43
- package/src/hashline/input.ts +15 -13
- package/src/hashline/parser.ts +62 -161
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +1 -1
- package/src/modes/components/model-selector.ts +53 -22
- package/src/modes/components/status-line/segments.ts +53 -0
- package/src/modes/components/status-line/types.ts +4 -0
- package/src/modes/components/status-line.ts +147 -12
- package/src/modes/controllers/command-controller.ts +9 -0
- package/src/modes/controllers/event-controller.ts +10 -1
- package/src/modes/interactive-mode.ts +74 -18
- package/src/modes/shared.ts +16 -0
- package/src/modes/theme/shimmer.ts +15 -6
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +1 -1
- package/src/modes/utils/context-usage.ts +25 -2
- package/src/modes/utils/ui-helpers.ts +11 -1
- package/src/prompts/agents/frontmatter.md +1 -0
- package/src/prompts/tools/hashline.md +62 -81
- package/src/sdk.ts +24 -0
- package/src/session/agent-session.ts +58 -0
- package/src/session/session-manager.ts +54 -1
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/task/executor.ts +50 -1
- package/src/task/index.ts +11 -0
- package/src/task/render.ts +26 -2
- package/src/task/types.ts +35 -0
- package/src/tools/bash-command-fixup.ts +0 -10
- package/src/tools/bash.ts +1 -9
- package/src/utils/clipboard.ts +68 -3
- package/src/utils/commit-message-generator.ts +6 -1
- package/src/utils/image-resize.ts +51 -26
- package/src/utils/title-generator.ts +45 -13
- package/dist/types/modes/components/status-line-segment-editor.d.ts +0 -24
- package/src/modes/components/status-line-segment-editor.ts +0 -359
package/src/task/types.ts
CHANGED
|
@@ -173,6 +173,7 @@ export interface AgentDefinition {
|
|
|
173
173
|
thinkingLevel?: ThinkingLevel;
|
|
174
174
|
output?: unknown;
|
|
175
175
|
blocking?: boolean;
|
|
176
|
+
autoloadSkills?: string[];
|
|
176
177
|
source: AgentSource;
|
|
177
178
|
filePath?: string;
|
|
178
179
|
}
|
|
@@ -211,6 +212,30 @@ export interface AgentProgress {
|
|
|
211
212
|
modelOverride?: string | string[];
|
|
212
213
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
213
214
|
extractedToolData?: Record<string, unknown[]>;
|
|
215
|
+
/**
|
|
216
|
+
* Auto-retry state when the subagent is sleeping between provider retries
|
|
217
|
+
* (e.g. 429 rate-limit with retry-after). Cleared when the retry resolves
|
|
218
|
+
* or fails. Surfacing this to the parent prevents the task tool from
|
|
219
|
+
* looking indefinitely "in progress" when a child is actually blocked on
|
|
220
|
+
* provider quota.
|
|
221
|
+
*/
|
|
222
|
+
retryState?: {
|
|
223
|
+
attempt: number;
|
|
224
|
+
maxAttempts: number;
|
|
225
|
+
delayMs: number;
|
|
226
|
+
errorMessage: string;
|
|
227
|
+
startedAtMs: number;
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Terminal retry failure surfaced once the subagent gave up retrying
|
|
231
|
+
* (e.g. retry-after exceeded the cap, or all attempts exhausted). Carries
|
|
232
|
+
* the final error so the parent UI can render "blocked: rate-limited"
|
|
233
|
+
* instead of waiting for a status that never arrives.
|
|
234
|
+
*/
|
|
235
|
+
retryFailure?: {
|
|
236
|
+
attempt: number;
|
|
237
|
+
errorMessage: string;
|
|
238
|
+
};
|
|
214
239
|
}
|
|
215
240
|
|
|
216
241
|
/** Result from a single agent execution */
|
|
@@ -250,6 +275,16 @@ export interface SingleResult {
|
|
|
250
275
|
nestedPatches?: NestedRepoPatch[];
|
|
251
276
|
/** Data extracted by registered subprocess tool handlers (keyed by tool name) */
|
|
252
277
|
extractedToolData?: Record<string, unknown[]>;
|
|
278
|
+
/**
|
|
279
|
+
* Terminal retry failure, when the subagent exited because the auto-retry
|
|
280
|
+
* loop gave up (retry-after exceeded the cap, or all attempts exhausted).
|
|
281
|
+
* Lets the parent task tool surface a "blocked: rate-limited" outcome
|
|
282
|
+
* instead of a generic failure.
|
|
283
|
+
*/
|
|
284
|
+
retryFailure?: {
|
|
285
|
+
attempt: number;
|
|
286
|
+
errorMessage: string;
|
|
287
|
+
};
|
|
253
288
|
/** Output metadata for agent:// URL integration */
|
|
254
289
|
outputMeta?: { lineCount: number; charCount: number };
|
|
255
290
|
}
|
|
@@ -35,13 +35,3 @@ export interface BashFixupResult {
|
|
|
35
35
|
export function applyBashFixups(command: string): BashFixupResult {
|
|
36
36
|
return nativeApplyBashFixups(command);
|
|
37
37
|
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Human-readable notice for the fixups that fired. Mirrors the shape of
|
|
41
|
-
* `formatTimeoutClampNotice` so it can ride alongside the other bash notices.
|
|
42
|
-
*/
|
|
43
|
-
export function formatBashFixupNotice(stripped: readonly string[]): string | undefined {
|
|
44
|
-
if (!stripped.length) return undefined;
|
|
45
|
-
const quoted = stripped.map(s => `\`${s}\``).join(", ");
|
|
46
|
-
return `<system-warning>Stripped redundant ${quoted} — bash output is already truncated and stderr is already merged into stdout. NEVER use these patterns.</system-warning>`;
|
|
47
|
-
}
|
package/src/tools/bash.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { renderStatusLine } from "../tui";
|
|
|
17
17
|
import { CachedOutputBlock } from "../tui/output-block";
|
|
18
18
|
import { getSixelLineMask } from "../utils/sixel";
|
|
19
19
|
import type { ToolSession } from ".";
|
|
20
|
-
import { applyBashFixups
|
|
20
|
+
import { applyBashFixups } from "./bash-command-fixup";
|
|
21
21
|
import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
|
|
22
22
|
import { checkBashInterception } from "./bash-interceptor";
|
|
23
23
|
import { canUseInteractiveBashPty } from "./bash-pty-selection";
|
|
@@ -233,7 +233,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
233
233
|
readonly #asyncEnabled: boolean;
|
|
234
234
|
readonly #autoBackgroundEnabled: boolean;
|
|
235
235
|
readonly #autoBackgroundThresholdMs: number;
|
|
236
|
-
#bashFixupNoticeEmitted = false;
|
|
237
236
|
|
|
238
237
|
constructor(private readonly session: ToolSession) {
|
|
239
238
|
this.#asyncEnabled = this.session.settings.get("async.enabled");
|
|
@@ -475,12 +474,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
475
474
|
// Apply conservative bash fixups (strip trailing `| head|tail` and redundant
|
|
476
475
|
// `2>&1`). The helper is single-line only and refuses anything that could
|
|
477
476
|
// change semantics.
|
|
478
|
-
let bashFixups: string[] = [];
|
|
479
477
|
if (this.session.settings.get("bash.stripTrailingHeadTail")) {
|
|
480
478
|
const fixup = applyBashFixups(command);
|
|
481
479
|
if (fixup.stripped.length > 0) {
|
|
482
480
|
command = fixup.command;
|
|
483
|
-
bashFixups = fixup.stripped;
|
|
484
481
|
}
|
|
485
482
|
}
|
|
486
483
|
|
|
@@ -562,11 +559,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
|
|
562
559
|
const pendingNotices: string[] = [];
|
|
563
560
|
const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
|
|
564
561
|
if (timeoutClampNotice) pendingNotices.push(timeoutClampNotice);
|
|
565
|
-
const bashFixupNotice = this.#bashFixupNoticeEmitted ? undefined : formatBashFixupNotice(bashFixups);
|
|
566
|
-
if (bashFixupNotice) {
|
|
567
|
-
pendingNotices.push(bashFixupNotice);
|
|
568
|
-
this.#bashFixupNoticeEmitted = true;
|
|
569
|
-
}
|
|
570
562
|
|
|
571
563
|
if (asyncRequested) {
|
|
572
564
|
if (!AsyncJobManager.instance()) {
|
package/src/utils/clipboard.ts
CHANGED
|
@@ -2,7 +2,13 @@ import { execSync } from "node:child_process";
|
|
|
2
2
|
import type { ClipboardImage } from "@oh-my-pi/pi-natives";
|
|
3
3
|
import * as native from "@oh-my-pi/pi-natives";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
function hasDisplay(): boolean {
|
|
6
|
+
return process.platform !== "linux" || Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isWsl(): boolean {
|
|
10
|
+
return process.platform === "linux" && Boolean(process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP);
|
|
11
|
+
}
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Copy text to the system clipboard.
|
|
@@ -59,11 +65,66 @@ export async function copyToClipboard(text: string): Promise<void> {
|
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
68
|
+
// PowerShell one-liner that emits the clipboard image as base64-encoded PNG on
|
|
69
|
+
// stdout, or nothing when the clipboard does not hold image data. Used as the
|
|
70
|
+
// WSL bridge — arboard cannot read the Windows clipboard through WSLg.
|
|
71
|
+
const POWERSHELL_IMAGE_SCRIPT = `
|
|
72
|
+
$ErrorActionPreference = 'Stop'
|
|
73
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
74
|
+
Add-Type -AssemblyName System.Drawing
|
|
75
|
+
$img = [System.Windows.Forms.Clipboard]::GetImage()
|
|
76
|
+
if ($img -ne $null) {
|
|
77
|
+
$ms = New-Object System.IO.MemoryStream
|
|
78
|
+
$img.Save($ms, [System.Drawing.Imaging.ImageFormat]::Png)
|
|
79
|
+
[Console]::Out.Write([Convert]::ToBase64String($ms.ToArray()))
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
|
|
83
|
+
const POWERSHELL_TIMEOUT_MS = 5000;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Read a clipboard image through the Windows host's PowerShell.
|
|
87
|
+
*
|
|
88
|
+
* WSLg exposes a Wayland socket but no native clipboard image transport, so
|
|
89
|
+
* `arboard` returns `ContentNotAvailable`. PowerShell, reached via WSL interop,
|
|
90
|
+
* can read the Windows clipboard directly and round-trip the bitmap as PNG.
|
|
91
|
+
*
|
|
92
|
+
* Returns null when no image is on the clipboard, the host PowerShell is
|
|
93
|
+
* missing, or the bridge times out.
|
|
94
|
+
*/
|
|
95
|
+
async function readImageViaPowerShell(): Promise<ClipboardImage | null> {
|
|
96
|
+
try {
|
|
97
|
+
const proc = Bun.spawn(["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", POWERSHELL_IMAGE_SCRIPT], {
|
|
98
|
+
stdout: "pipe",
|
|
99
|
+
stderr: "ignore",
|
|
100
|
+
stdin: "ignore",
|
|
101
|
+
});
|
|
102
|
+
const timer = setTimeout(() => proc.kill(), POWERSHELL_TIMEOUT_MS);
|
|
103
|
+
let stdout = "";
|
|
104
|
+
try {
|
|
105
|
+
stdout = await new Response(proc.stdout).text();
|
|
106
|
+
await proc.exited;
|
|
107
|
+
} finally {
|
|
108
|
+
clearTimeout(timer);
|
|
109
|
+
}
|
|
110
|
+
if (proc.exitCode !== 0) return null;
|
|
111
|
+
const b64 = stdout.trim();
|
|
112
|
+
if (!b64) return null;
|
|
113
|
+
const bytes = Buffer.from(b64, "base64");
|
|
114
|
+
if (bytes.byteLength === 0) return null;
|
|
115
|
+
return { data: new Uint8Array(bytes), mimeType: "image/png" };
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
62
121
|
/**
|
|
63
122
|
* Read an image from the system clipboard.
|
|
64
123
|
*
|
|
65
124
|
* Returns null on Termux (no image clipboard support) or when no display
|
|
66
|
-
* server is available (headless/SSH without forwarding).
|
|
125
|
+
* server is available (headless/SSH without forwarding). Under WSL the
|
|
126
|
+
* Windows clipboard is reached through `powershell.exe`, since WSLg's
|
|
127
|
+
* Wayland clipboard does not carry image payloads through to `arboard`.
|
|
67
128
|
*
|
|
68
129
|
* @returns PNG payload or null when no image is available.
|
|
69
130
|
*/
|
|
@@ -72,7 +133,11 @@ export async function readImageFromClipboard(): Promise<ClipboardImage | null> {
|
|
|
72
133
|
return null;
|
|
73
134
|
}
|
|
74
135
|
|
|
75
|
-
if (
|
|
136
|
+
if (isWsl()) {
|
|
137
|
+
const image = await readImageViaPowerShell();
|
|
138
|
+
if (image) return image;
|
|
139
|
+
// Fall through: arboard may still succeed on a future WSLg release.
|
|
140
|
+
} else if (!hasDisplay()) {
|
|
76
141
|
return null;
|
|
77
142
|
}
|
|
78
143
|
|
|
@@ -15,6 +15,8 @@ import { toReasoningEffort } from "../thinking";
|
|
|
15
15
|
|
|
16
16
|
const COMMIT_SYSTEM_PROMPT = prompt.render(commitSystemPrompt);
|
|
17
17
|
const MAX_DIFF_CHARS = 4000;
|
|
18
|
+
const COMMIT_MAX_TOKENS = 60;
|
|
19
|
+
const REASONING_SAFE_MAX_TOKENS = 1024;
|
|
18
20
|
|
|
19
21
|
/** File patterns that should be excluded from commit message generation diffs. */
|
|
20
22
|
const NOISE_SUFFIXES = [".lock", ".lockb", "-lock.json", "-lock.yaml"];
|
|
@@ -99,13 +101,16 @@ export async function generateCommitMessage(
|
|
|
99
101
|
if (!apiKey) continue;
|
|
100
102
|
|
|
101
103
|
try {
|
|
104
|
+
const maxTokens = candidate.model.reasoning
|
|
105
|
+
? Math.max(COMMIT_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS)
|
|
106
|
+
: COMMIT_MAX_TOKENS;
|
|
102
107
|
const response = await completeSimple(
|
|
103
108
|
candidate.model,
|
|
104
109
|
{
|
|
105
110
|
systemPrompt: [COMMIT_SYSTEM_PROMPT],
|
|
106
111
|
messages: [{ role: "user", content: userMessage, timestamp: Date.now() }],
|
|
107
112
|
},
|
|
108
|
-
{ apiKey, maxTokens
|
|
113
|
+
{ apiKey, maxTokens, reasoning: toReasoningEffort(candidate.thinkingLevel) },
|
|
109
114
|
);
|
|
110
115
|
|
|
111
116
|
if (response.stopReason === "error") {
|
|
@@ -5,6 +5,7 @@ export interface ImageResizeOptions {
|
|
|
5
5
|
maxHeight?: number;
|
|
6
6
|
maxBytes?: number;
|
|
7
7
|
jpegQuality?: number;
|
|
8
|
+
excludeWebP?: boolean;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export interface ResizedImage {
|
|
@@ -29,6 +30,7 @@ const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
|
|
|
29
30
|
maxHeight: 1568,
|
|
30
31
|
maxBytes: DEFAULT_MAX_BYTES,
|
|
31
32
|
jpegQuality: 80,
|
|
33
|
+
excludeWebP: Bun.env.OMP_NO_WEBP !== undefined,
|
|
32
34
|
};
|
|
33
35
|
|
|
34
36
|
/** Pick the smallest of N encoded buffers. */
|
|
@@ -49,11 +51,13 @@ Buffer.prototype.toBase64 = function (this: Buffer) {
|
|
|
49
51
|
*
|
|
50
52
|
* Strategy:
|
|
51
53
|
* 1. Probe metadata. If already within all limits, return original.
|
|
52
|
-
* 2. Resize to fit max dimensions and encode at high quality across PNG/JPEG
|
|
54
|
+
* 2. Resize to fit max dimensions and encode at high quality across PNG/JPEG (+ WebP) — return smallest.
|
|
53
55
|
* 3. If still too large, walk a lossy JPEG/WebP quality ladder.
|
|
54
56
|
* 4. If still too large, walk a dimension-scale ladder × quality ladder.
|
|
55
57
|
* 5. If still too large, return the smallest variant produced.
|
|
56
58
|
*
|
|
59
|
+
* Set OMP_NO_WEBP to exclude WebP from encoding (llama.cpp STB doesn't decode it).
|
|
60
|
+
*
|
|
57
61
|
* Backed by `Bun.Image`: a chainable native pipeline that runs decode/transform/encode
|
|
58
62
|
* off the JS thread when the terminal (`.bytes()`) is awaited.
|
|
59
63
|
*/
|
|
@@ -99,44 +103,65 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
99
103
|
targetHeight = opts.maxHeight;
|
|
100
104
|
}
|
|
101
105
|
|
|
102
|
-
// First-attempt encoder: try PNG
|
|
103
|
-
// PNG wins for line art / few-color UI; JPEG
|
|
104
|
-
// WebP usually beats JPEG by 25–35%
|
|
106
|
+
// First-attempt encoder: try PNG and JPEG (+ WebP if not excluded) — return smallest.
|
|
107
|
+
// PNG wins for line art / few-color UI; JPEG wins for photographic content;
|
|
108
|
+
// WebP usually beats JPEG by 25–35% but is disabled when OMP_NO_WEBP is set
|
|
109
|
+
// because many local inference backends (llama.cpp STB) don't decode it.
|
|
105
110
|
async function encodeSmallest(
|
|
106
111
|
width: number,
|
|
107
112
|
height: number,
|
|
108
113
|
quality: number,
|
|
109
114
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
|
110
|
-
const
|
|
111
|
-
new Bun.Image(inputBuffer)
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
const candidates = await Promise.all([
|
|
116
|
+
new Bun.Image(inputBuffer)
|
|
117
|
+
.resize(width, height)
|
|
118
|
+
.png()
|
|
119
|
+
.bytes()
|
|
120
|
+
.then(b => ({ buffer: b, mimeType: "image/png" })),
|
|
121
|
+
new Bun.Image(inputBuffer)
|
|
122
|
+
.resize(width, height)
|
|
123
|
+
.jpeg({ quality })
|
|
124
|
+
.bytes()
|
|
125
|
+
.then(b => ({ buffer: b, mimeType: "image/jpeg" })),
|
|
126
|
+
...(opts.excludeWebP
|
|
127
|
+
? []
|
|
128
|
+
: [
|
|
129
|
+
new Bun.Image(inputBuffer)
|
|
130
|
+
.resize(width, height)
|
|
131
|
+
.webp({ quality })
|
|
132
|
+
.bytes()
|
|
133
|
+
.then(b => ({ buffer: b, mimeType: "image/webp" })),
|
|
134
|
+
]),
|
|
114
135
|
]);
|
|
115
|
-
return pickSmallest(
|
|
116
|
-
{ buffer: pngBuffer, mimeType: "image/png" },
|
|
117
|
-
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
118
|
-
{ buffer: webpBuffer, mimeType: "image/webp" },
|
|
119
|
-
);
|
|
136
|
+
return pickSmallest(...candidates);
|
|
120
137
|
}
|
|
121
138
|
|
|
122
|
-
// Lossy
|
|
123
|
-
//
|
|
124
|
-
//
|
|
139
|
+
// Lossy encoder for quality/dimension fallback ladders. PNG is excluded since
|
|
140
|
+
// it's lossless and doesn't respond to quality parameters. WebP is included
|
|
141
|
+
// unless OMP_NO_WEBP is set (llama.cpp STB incompatibility).
|
|
125
142
|
async function encodeLossy(
|
|
126
143
|
width: number,
|
|
127
144
|
height: number,
|
|
128
145
|
quality: number,
|
|
129
146
|
): Promise<{ buffer: Uint8Array; mimeType: string }> {
|
|
130
|
-
const
|
|
131
|
-
new Bun.Image(inputBuffer)
|
|
132
|
-
|
|
147
|
+
const candidates = await Promise.all([
|
|
148
|
+
new Bun.Image(inputBuffer)
|
|
149
|
+
.resize(width, height)
|
|
150
|
+
.jpeg({ quality })
|
|
151
|
+
.bytes()
|
|
152
|
+
.then(b => ({ buffer: b, mimeType: "image/jpeg" })),
|
|
153
|
+
...(opts.excludeWebP
|
|
154
|
+
? []
|
|
155
|
+
: [
|
|
156
|
+
new Bun.Image(inputBuffer)
|
|
157
|
+
.resize(width, height)
|
|
158
|
+
.webp({ quality })
|
|
159
|
+
.bytes()
|
|
160
|
+
.then(b => ({ buffer: b, mimeType: "image/webp" })),
|
|
161
|
+
]),
|
|
133
162
|
]);
|
|
134
|
-
return pickSmallest(
|
|
135
|
-
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
|
|
136
|
-
{ buffer: webpBuffer, mimeType: "image/webp" },
|
|
137
|
-
);
|
|
163
|
+
return pickSmallest(...candidates);
|
|
138
164
|
}
|
|
139
|
-
|
|
140
165
|
// Quality ladder — more aggressive steps for tighter budgets
|
|
141
166
|
const qualitySteps = [70, 60, 50, 40];
|
|
142
167
|
const scaleSteps = [1.0, 0.75, 0.5, 0.35, 0.25];
|
|
@@ -145,7 +170,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
145
170
|
let finalWidth = targetWidth;
|
|
146
171
|
let finalHeight = targetHeight;
|
|
147
172
|
|
|
148
|
-
// First attempt: resize to target, try PNG/JPEG
|
|
173
|
+
// First attempt: resize to target, try PNG/JPEG (+ WebP), pick smallest
|
|
149
174
|
best = await encodeSmallest(targetWidth, targetHeight, opts.jpegQuality);
|
|
150
175
|
|
|
151
176
|
if (best.buffer.length <= opts.maxBytes) {
|
|
@@ -163,7 +188,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
|
|
|
163
188
|
};
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
// Still too large — lossy
|
|
191
|
+
// Still too large — lossy JPEG (+ WebP) ladder with decreasing quality
|
|
167
192
|
for (const quality of qualitySteps) {
|
|
168
193
|
best = await encodeLossy(targetWidth, targetHeight, quality);
|
|
169
194
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
6
|
-
import { type Api, completeSimple, type Model } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import { type Api, type AssistantMessage, completeSimple, type Model, type Tool } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { logger, prompt } from "@oh-my-pi/pi-utils";
|
|
8
8
|
import type { ModelRegistry } from "../config/model-registry";
|
|
9
9
|
import { resolveRoleSelection } from "../config/model-resolver";
|
|
@@ -16,6 +16,25 @@ const DEFAULT_TERMINAL_TITLE = "π";
|
|
|
16
16
|
const TERMINAL_TITLE_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
17
17
|
|
|
18
18
|
const MAX_INPUT_CHARS = 2000;
|
|
19
|
+
const TITLE_MAX_TOKENS = 30;
|
|
20
|
+
const REASONING_SAFE_MAX_TOKENS = 1024;
|
|
21
|
+
const SET_TITLE_TOOL_NAME = "set_title";
|
|
22
|
+
|
|
23
|
+
const setTitleTool: Tool = {
|
|
24
|
+
name: SET_TITLE_TOOL_NAME,
|
|
25
|
+
description: "Set the generated session title.",
|
|
26
|
+
parameters: {
|
|
27
|
+
type: "object",
|
|
28
|
+
properties: {
|
|
29
|
+
title: {
|
|
30
|
+
type: "string",
|
|
31
|
+
description: "A concise 3-6 word title for the session.",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
required: ["title"],
|
|
35
|
+
additionalProperties: false,
|
|
36
|
+
},
|
|
37
|
+
};
|
|
19
38
|
|
|
20
39
|
function getTitleModel(registry: ModelRegistry, settings: Settings, currentModel?: Model<Api>): Model<Api> | undefined {
|
|
21
40
|
const availableModels = registry.getAvailable();
|
|
@@ -76,14 +95,16 @@ ${truncatedMessage}
|
|
|
76
95
|
// account_uuid rather than the snapshot-at-call-site value.
|
|
77
96
|
const metadata = metadataResolver?.(model.provider);
|
|
78
97
|
|
|
79
|
-
// Title generation is a 3-6 word task
|
|
80
|
-
//
|
|
81
|
-
//
|
|
98
|
+
// Title generation is a 3-6 word task, but some reasoning backends ignore
|
|
99
|
+
// disableReasoning. Keep the normal cheap budget for non-reasoning models
|
|
100
|
+
// while reserving enough output room for reasoning models to still emit
|
|
101
|
+
// the forced tool call after any unavoidable thinking tokens.
|
|
102
|
+
const maxTokens = model.reasoning ? Math.max(TITLE_MAX_TOKENS, REASONING_SAFE_MAX_TOKENS) : TITLE_MAX_TOKENS;
|
|
82
103
|
const request = {
|
|
83
104
|
model: `${model.provider}/${model.id}`,
|
|
84
105
|
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
85
106
|
userMessage,
|
|
86
|
-
maxTokens
|
|
107
|
+
maxTokens,
|
|
87
108
|
};
|
|
88
109
|
logger.debug("title-generator: request", request);
|
|
89
110
|
|
|
@@ -93,11 +114,13 @@ ${truncatedMessage}
|
|
|
93
114
|
{
|
|
94
115
|
systemPrompt: [request.systemPrompt],
|
|
95
116
|
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
117
|
+
tools: [setTitleTool],
|
|
96
118
|
},
|
|
97
119
|
{
|
|
98
120
|
apiKey,
|
|
99
|
-
maxTokens:
|
|
121
|
+
maxTokens: request.maxTokens,
|
|
100
122
|
disableReasoning: true,
|
|
123
|
+
toolChoice: { type: "tool", name: SET_TITLE_TOOL_NAME },
|
|
101
124
|
metadata,
|
|
102
125
|
},
|
|
103
126
|
);
|
|
@@ -111,13 +134,7 @@ ${truncatedMessage}
|
|
|
111
134
|
return null;
|
|
112
135
|
}
|
|
113
136
|
|
|
114
|
-
|
|
115
|
-
for (const content of response.content) {
|
|
116
|
-
if (content.type === "text") {
|
|
117
|
-
title += content.text;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
title = title.trim();
|
|
137
|
+
const title = extractGeneratedTitle(response.content);
|
|
121
138
|
|
|
122
139
|
logger.debug("title-generator: response", {
|
|
123
140
|
model: request.model,
|
|
@@ -140,6 +157,21 @@ ${truncatedMessage}
|
|
|
140
157
|
}
|
|
141
158
|
}
|
|
142
159
|
|
|
160
|
+
function extractGeneratedTitle(contentBlocks: AssistantMessage["content"]): string {
|
|
161
|
+
let textTitle = "";
|
|
162
|
+
for (const content of contentBlocks) {
|
|
163
|
+
if (content.type === "toolCall" && content.name === SET_TITLE_TOOL_NAME) {
|
|
164
|
+
const args = content.arguments as Record<string, unknown>;
|
|
165
|
+
const title = args.title;
|
|
166
|
+
return typeof title === "string" ? title.trim() : "";
|
|
167
|
+
}
|
|
168
|
+
if (content.type === "text") {
|
|
169
|
+
textTitle += content.text;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return textTitle.trim();
|
|
173
|
+
}
|
|
174
|
+
|
|
143
175
|
/**
|
|
144
176
|
* Remove control characters so model-generated titles cannot inject terminal escapes.
|
|
145
177
|
*/
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Status Line Segment Editor
|
|
3
|
-
*
|
|
4
|
-
* Interactive component for configuring status line segments.
|
|
5
|
-
* - Three-column layout: Left | Right | Disabled
|
|
6
|
-
* - Space: Toggle segment visibility (disabled ↔ left)
|
|
7
|
-
* - Tab: Cycle segment between columns (left → right → disabled → left)
|
|
8
|
-
* - Shift+J/K: Reorder segment within column
|
|
9
|
-
* - Live preview shown in the actual status line above
|
|
10
|
-
*/
|
|
11
|
-
import { Container } from "@oh-my-pi/pi-tui";
|
|
12
|
-
import type { StatusLineSegmentId } from "../../config/settings-schema";
|
|
13
|
-
export interface SegmentEditorCallbacks {
|
|
14
|
-
onSave: (leftSegments: StatusLineSegmentId[], rightSegments: StatusLineSegmentId[]) => void;
|
|
15
|
-
onCancel: () => void;
|
|
16
|
-
onPreview?: (leftSegments: StatusLineSegmentId[], rightSegments: StatusLineSegmentId[]) => void;
|
|
17
|
-
}
|
|
18
|
-
export declare class StatusLineSegmentEditorComponent extends Container {
|
|
19
|
-
#private;
|
|
20
|
-
private readonly callbacks;
|
|
21
|
-
constructor(currentLeft: StatusLineSegmentId[], currentRight: StatusLineSegmentId[], callbacks: SegmentEditorCallbacks);
|
|
22
|
-
handleInput(data: string): void;
|
|
23
|
-
render(width: number): string[];
|
|
24
|
-
}
|