@oh-my-pi/pi-coding-agent 15.2.3 → 15.2.4
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 +21 -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/shared.d.ts +9 -0
- package/dist/types/modes/theme/shimmer.d.ts +6 -3
- package/package.json +7 -7
- package/src/config/prompt-templates.ts +1 -8
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +5 -7
- package/src/edit/streaming.ts +24 -12
- 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/modes/controllers/event-controller.ts +2 -1
- package/src/modes/interactive-mode.ts +51 -10
- package/src/modes/shared.ts +16 -0
- package/src/modes/theme/shimmer.ts +15 -6
- package/src/prompts/tools/hashline.md +62 -81
- package/src/utils/commit-message-generator.ts +6 -1
- package/src/utils/title-generator.ts +45 -13
|
@@ -24,14 +24,20 @@ const BOLD_CLOSE = "\x1b[22m";
|
|
|
24
24
|
type ShimmerTheme = Pick<Theme, "bold" | "fg" | "getFgAnsi">;
|
|
25
25
|
type ShimmerMode = "classic" | "kitt" | "disabled";
|
|
26
26
|
|
|
27
|
+
type ShimmerPaletteTier = ThemeColor | { ansi: string };
|
|
28
|
+
|
|
29
|
+
function resolveTierAnsi(theme: ShimmerTheme, tier: ShimmerPaletteTier): string {
|
|
30
|
+
return typeof tier === "string" ? theme.getFgAnsi(tier) : tier.ansi;
|
|
31
|
+
}
|
|
32
|
+
|
|
27
33
|
/** Three-tier color stack a shimmer character cycles through as the band sweeps. */
|
|
28
34
|
export interface ShimmerPalette {
|
|
29
35
|
/** Color for chars outside / at the edge of the band (intensity < ~0.22). */
|
|
30
|
-
low:
|
|
36
|
+
low: ShimmerPaletteTier;
|
|
31
37
|
/** Color for chars approaching the crest (~0.22 ≤ intensity < ~0.65). */
|
|
32
|
-
mid:
|
|
38
|
+
mid: ShimmerPaletteTier;
|
|
33
39
|
/** Color at the band's crest (intensity ≥ ~0.65). */
|
|
34
|
-
high:
|
|
40
|
+
high: ShimmerPaletteTier;
|
|
35
41
|
/** Whether to bold the crest tier. Default `false`. */
|
|
36
42
|
bold?: boolean;
|
|
37
43
|
}
|
|
@@ -78,11 +84,14 @@ function compile(theme: ShimmerTheme, palette: ShimmerPalette): CompiledPalette
|
|
|
78
84
|
const p = palette as ShimmerPalette & PaletteCache;
|
|
79
85
|
const cached = p[kCompiled];
|
|
80
86
|
if (cached && p[kCompiledFor] === theme) return cached;
|
|
81
|
-
const
|
|
87
|
+
const lowOpen = resolveTierAnsi(theme, palette.low);
|
|
88
|
+
const midOpen = resolveTierAnsi(theme, palette.mid);
|
|
89
|
+
const highColorOpen = resolveTierAnsi(theme, palette.high);
|
|
90
|
+
const highOpen = palette.bold ? `${BOLD_OPEN}${highColorOpen}` : highColorOpen;
|
|
82
91
|
const highClose = palette.bold ? `${BOLD_CLOSE}${FG_RESET}` : FG_RESET;
|
|
83
92
|
const out: CompiledPalette = {
|
|
84
|
-
low: { open:
|
|
85
|
-
mid: { open:
|
|
93
|
+
low: { open: lowOpen, close: FG_RESET },
|
|
94
|
+
mid: { open: midOpen, close: FG_RESET },
|
|
86
95
|
high: { open: highOpen, close: highClose },
|
|
87
96
|
};
|
|
88
97
|
p[kCompiledFor] = theme;
|
|
@@ -1,58 +1,36 @@
|
|
|
1
1
|
Your patch language is a compact, line-anchored edit format.
|
|
2
2
|
|
|
3
|
-
A patch contains one or more file sections. The first non-blank line of every edit section MUST be
|
|
3
|
+
A patch contains one or more file sections. The first non-blank line of every edit section MUST be `§PATH`.
|
|
4
4
|
Operations reference lines in the file by their line number and hash, called "Anchors", e.g. `5th`, `123ab`.
|
|
5
5
|
You MUST copy them verbatim from the latest output for the file you're editing.
|
|
6
6
|
|
|
7
7
|
Purely textual format. The tool has NO awareness of language, indentation, brackets, fences, or table widths. You MUST emit valid syntax in replacements/insertions.
|
|
8
8
|
|
|
9
9
|
<ops>
|
|
10
|
-
|
|
10
|
+
§PATH header: subsequent ops apply to PATH
|
|
11
11
|
Each op line is ONE of:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
»ANCHOR insert lines AFTER the anchored line (or EOF); payload follows on subsequent lines
|
|
13
|
+
«ANCHOR insert lines BEFORE the anchored line (or BOF); payload follows on subsequent lines
|
|
14
|
+
≔A..B replace the inclusive range A..B with payload; delete the range if no payload follows
|
|
15
|
+
≔A shorthand for ≔A..A
|
|
16
16
|
</ops>
|
|
17
17
|
|
|
18
|
-
<format-reminder>
|
|
19
|
-
Op lines carry no content — payload goes on the next line.
|
|
20
|
-
|
|
21
|
-
WRONG: + 5pg| some code
|
|
22
|
-
WRONG: {{hsep}} some code
|
|
23
|
-
RIGHT: + 5pg
|
|
24
|
-
{{hsep}}some code
|
|
25
|
-
|
|
26
|
-
A single `+`/`<`/`=` op accepts MANY `{{hsep}}` payload lines. To insert N consecutive lines, write ONE op followed by N payload lines — NEVER N ops with one payload each.
|
|
27
|
-
|
|
28
|
-
WRONG (one op per inserted line, with fabricated anchors):
|
|
29
|
-
+ 5pg
|
|
30
|
-
{{hsep}}first new line
|
|
31
|
-
+ 6xx ← FABRICATED
|
|
32
|
-
{{hsep}}second new line
|
|
33
|
-
|
|
34
|
-
RIGHT (one op, many payload lines):
|
|
35
|
-
+ 5pg
|
|
36
|
-
{{hsep}}first new line
|
|
37
|
-
{{hsep}}second new line
|
|
38
|
-
</format-reminder>
|
|
39
|
-
|
|
40
18
|
<rules>
|
|
41
|
-
- Every payload line MUST start with `{{hsep}}` immediately followed by payload text. Do NOT add a readability space after `{{hsep}}`.
|
|
42
|
-
- Every character after `{{hsep}}` is file content. If the target line intentionally starts with one space, write exactly one space after `{{hsep}}`; otherwise write none.
|
|
43
19
|
- Payload text is verbatim — NEVER escape unicode.
|
|
20
|
+
- Payload ends at the next `»`, `«`, `≔`, `§`, envelope marker, or EOF.
|
|
21
|
+
- `≔A..B` with no payload deletes the range. To keep a blank line, include one explicit empty payload line.
|
|
44
22
|
- **Payload is only what's NEW relative to your range:**
|
|
45
|
-
-
|
|
46
|
-
-
|
|
23
|
+
- `≔` replaces inside; NEVER include lines outside.
|
|
24
|
+
- `»`/`«` adds at the anchor; NEVER repeat line A or neighbors.
|
|
47
25
|
- Payload matching nearby content duplicates — drop it or widen.
|
|
48
26
|
- **Pick a self-contained unit first.** Touching a multiline construct? Widen to the whole thing.
|
|
49
|
-
- Then smallest op: add →
|
|
27
|
+
- Then smallest op: add → `»`/`«`; delete/replace → `≔`.
|
|
50
28
|
</rules>
|
|
51
29
|
|
|
52
30
|
<brace-shapes>
|
|
53
31
|
When braces bound your edit, you SHOULD prefer these shapes:
|
|
54
32
|
- **Whole block**: range spans `{` through matching `}`.
|
|
55
|
-
- **Signature only**: one-line
|
|
33
|
+
- **Signature only**: one-line `≔` on the opener; body untouched.
|
|
56
34
|
- **Insert inside**: anchor on `{` or last interior line; NEVER repeat the braces.
|
|
57
35
|
- **End on `}`**: only when that `}` is part of the change. Otherwise extend or stop earlier.
|
|
58
36
|
</brace-shapes>
|
|
@@ -61,9 +39,9 @@ When braces bound your edit, you SHOULD prefer these shapes:
|
|
|
61
39
|
- **NEVER replay past your range.** Stop before B+1; extend B if it must go.
|
|
62
40
|
- **NEVER duplicate chunks inside one payload.** Caught re-emitting? Rewrite.
|
|
63
41
|
- **Anchor only inside the visible region.** B+1 truncated? Re-`read` first.
|
|
64
|
-
- **You SHOULD prefer the narrowest self-contained edit.**
|
|
42
|
+
- **You SHOULD prefer the narrowest self-contained edit.** Narrow range beats wide range.
|
|
65
43
|
- **Anchors reference the file as last read.** NEVER shift for prior ops.
|
|
66
|
-
- **One
|
|
44
|
+
- **One `»`/`«` op per block, NOT per line.** N lines = ONE op, N payloads. Collapse adjacent ops.
|
|
67
45
|
- **NEVER fabricate anchor hashes.** Missing? Re-`read`.
|
|
68
46
|
</common-failures>
|
|
69
47
|
|
|
@@ -79,71 +57,74 @@ When braces bound your edit, you SHOULD prefer these shapes:
|
|
|
79
57
|
|
|
80
58
|
<examples>
|
|
81
59
|
# Replace one line (the payload must re-emit the original indentation)
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
60
|
+
§mod.ts
|
|
61
|
+
≔{{hrefr 1}}
|
|
62
|
+
const TITLE = "Mrs";
|
|
85
63
|
|
|
86
64
|
# Replace a full multiline statement (widen to a self-contained boundary)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
65
|
+
§mod.ts
|
|
66
|
+
≔{{hrefr 3}}..{{hrefr 6}}
|
|
67
|
+
return [
|
|
68
|
+
"Mrs",
|
|
69
|
+
name?.trim() || "guest",
|
|
70
|
+
].join(" ");
|
|
93
71
|
|
|
94
72
|
# Insert AFTER/BEFORE a line
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
73
|
+
§mod.ts
|
|
74
|
+
»{{hrefr 4}}
|
|
75
|
+
"Dr",
|
|
76
|
+
«{{hrefr 5}}
|
|
77
|
+
"Dr",
|
|
100
78
|
|
|
101
79
|
# Append to file
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
80
|
+
§mod.ts
|
|
81
|
+
»EOF
|
|
82
|
+
export const done = true;
|
|
105
83
|
|
|
106
84
|
# Delete a line
|
|
107
|
-
|
|
108
|
-
|
|
85
|
+
§mod.ts
|
|
86
|
+
≔{{hrefr 5}}
|
|
87
|
+
|
|
88
|
+
# Blank a line (replace with LF: the empty payload is the blank line before `»EOF`)
|
|
89
|
+
§mod.ts
|
|
90
|
+
≔{{hrefr 5}}
|
|
109
91
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
= {{hrefr 5}}..{{hrefr 5}}
|
|
92
|
+
»EOF
|
|
93
|
+
export const done = true;
|
|
113
94
|
</examples>
|
|
114
95
|
|
|
115
96
|
<anti-pattern>
|
|
116
97
|
# WRONG — replaces 2 lines just to add one.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
98
|
+
§mod.ts
|
|
99
|
+
≔{{hrefr 1}}..{{hrefr 2}}
|
|
100
|
+
const TITLE = "Mr";
|
|
101
|
+
const DEBUG = false;
|
|
102
|
+
export function greet(name) {
|
|
122
103
|
# RIGHT — same effect, one-line insert
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
§mod.ts
|
|
105
|
+
»{{hrefr 1}}
|
|
106
|
+
const DEBUG = false;
|
|
126
107
|
|
|
127
108
|
# WRONG — replace from the middle of a larger statement (error-prone)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
109
|
+
§mod.ts
|
|
110
|
+
≔{{hrefr 4}}..{{hrefr 5}}
|
|
111
|
+
"Dr",
|
|
112
|
+
name?.trim() || "guest",
|
|
132
113
|
# RIGHT — widen to the full statement
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
114
|
+
§mod.ts
|
|
115
|
+
≔{{hrefr 3}}..{{hrefr 6}}
|
|
116
|
+
return [
|
|
117
|
+
"Dr",
|
|
118
|
+
name?.trim() || "guest",
|
|
119
|
+
].join(" ");
|
|
139
120
|
</anti-pattern>
|
|
140
121
|
|
|
141
122
|
<critical>
|
|
142
123
|
- Copy anchors verbatim (line number + 2-char hash); NEVER include the `|TEXT` body.
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
146
|
-
- Multiple ops are cheap. SHOULD prefer two narrow ops over one wide
|
|
147
|
-
- Before
|
|
124
|
+
- NEVER write unified diff syntax. Headers are `§PATH`; ops are `»`/`«`/`≔`.
|
|
125
|
+
- `≔A..B` deletes the range when no payload follows. To keep a blank line, include one explicit empty payload line.
|
|
126
|
+
- `≔A..B` with payload writes exactly that payload. Edge line matches just outside? Widen, or it duplicates.
|
|
127
|
+
- Multiple ops are cheap. SHOULD prefer two narrow ops over one wide `≔`.
|
|
128
|
+
- Before `≔A..B`, mentally delete A..B. Splits an unclosed bracket/brace/string from above, or orphans a closer inside? You're bisecting a construct.
|
|
148
129
|
- NEVER use this tool to reformat code (indentation, whitespace, line wrapping, style). Run the project's formatter instead.
|
|
149
130
|
</critical>
|
|
@@ -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") {
|
|
@@ -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
|
*/
|