@oh-my-pi/pi-coding-agent 13.6.2 → 13.7.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.
- package/CHANGELOG.md +6 -0
- package/package.json +7 -7
- package/src/cli/grep-cli.ts +9 -1
- package/src/commands/grep.ts +2 -0
- package/src/config/settings-schema.ts +1 -0
- package/src/discovery/helpers.ts +16 -1
- package/src/extensibility/extensions/types.ts +8 -0
- package/src/extensibility/skills.ts +6 -2
- package/src/main.ts +19 -27
- package/src/modes/components/countdown-timer.ts +39 -10
- package/src/modes/components/hook-input.ts +7 -1
- package/src/modes/components/hook-selector.ts +30 -5
- package/src/modes/components/mcp-add-wizard.ts +8 -2
- package/src/modes/components/status-line/presets.ts +2 -2
- package/src/modes/components/status-line/segments.ts +12 -0
- package/src/modes/components/status-line/token-rate.ts +66 -0
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -0
- package/src/modes/components/status-line.ts +38 -1
- package/src/modes/controllers/extension-ui-controller.ts +21 -0
- package/src/modes/controllers/mcp-command-controller.ts +4 -2
- package/src/modes/rpc/rpc-mode.ts +25 -18
- package/src/modes/rpc/rpc-types.ts +1 -1
- package/src/prompts/tools/grep.md +1 -0
- package/src/prompts/tools/hashline.md +41 -139
- package/src/session/session-manager.ts +50 -0
- package/src/tools/ask.ts +237 -75
- package/src/tools/grep.ts +6 -1
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { getPreset } from "./status-line/presets";
|
|
19
19
|
import { renderSegment, type SegmentContext } from "./status-line/segments";
|
|
20
20
|
import { getSeparator } from "./status-line/separators";
|
|
21
|
+
import { calculateTokensPerSecond } from "./status-line/token-rate";
|
|
21
22
|
|
|
22
23
|
export interface StatusLineSegmentOptions {
|
|
23
24
|
model?: { showThinkingLevel?: boolean };
|
|
@@ -65,6 +66,8 @@ export class StatusLineComponent implements Component {
|
|
|
65
66
|
#cachedPrContext: PrCacheContext | undefined = undefined;
|
|
66
67
|
#prLookupInFlight = false;
|
|
67
68
|
#defaultBranch?: string;
|
|
69
|
+
#lastTokensPerSecond: number | null = null;
|
|
70
|
+
#lastTokensPerSecondTimestamp: number | null = null;
|
|
68
71
|
|
|
69
72
|
constructor(private readonly session: AgentSession) {
|
|
70
73
|
this.#settings = {
|
|
@@ -309,11 +312,41 @@ export class StatusLineComponent implements Component {
|
|
|
309
312
|
return null;
|
|
310
313
|
}
|
|
311
314
|
|
|
315
|
+
#getTokensPerSecond(): number | null {
|
|
316
|
+
let lastAssistantTimestamp: number | null = null;
|
|
317
|
+
for (let i = this.session.state.messages.length - 1; i >= 0; i--) {
|
|
318
|
+
const message = this.session.state.messages[i];
|
|
319
|
+
if (message?.role === "assistant") {
|
|
320
|
+
lastAssistantTimestamp = message.timestamp;
|
|
321
|
+
break;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (lastAssistantTimestamp === null) {
|
|
326
|
+
this.#lastTokensPerSecond = null;
|
|
327
|
+
this.#lastTokensPerSecondTimestamp = null;
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const rate = calculateTokensPerSecond(this.session.state.messages, this.session.isStreaming);
|
|
332
|
+
if (rate !== null) {
|
|
333
|
+
this.#lastTokensPerSecond = rate;
|
|
334
|
+
this.#lastTokensPerSecondTimestamp = lastAssistantTimestamp;
|
|
335
|
+
return rate;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (this.#lastTokensPerSecondTimestamp === lastAssistantTimestamp) {
|
|
339
|
+
return this.#lastTokensPerSecond;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
|
|
312
345
|
#buildSegmentContext(width: number): SegmentContext {
|
|
313
346
|
const state = this.session.state;
|
|
314
347
|
|
|
315
348
|
// Get usage statistics
|
|
316
|
-
const
|
|
349
|
+
const aggregateUsageStats = this.session.sessionManager?.getUsageStatistics() ?? {
|
|
317
350
|
input: 0,
|
|
318
351
|
output: 0,
|
|
319
352
|
cacheRead: 0,
|
|
@@ -321,6 +354,10 @@ export class StatusLineComponent implements Component {
|
|
|
321
354
|
premiumRequests: 0,
|
|
322
355
|
cost: 0,
|
|
323
356
|
};
|
|
357
|
+
const usageStats = {
|
|
358
|
+
...aggregateUsageStats,
|
|
359
|
+
tokensPerSecond: this.#getTokensPerSecond(),
|
|
360
|
+
};
|
|
324
361
|
|
|
325
362
|
// Get context percentage
|
|
326
363
|
const lastAssistantMessage = state.messages
|
|
@@ -577,8 +577,24 @@ export class ExtensionUiController {
|
|
|
577
577
|
finish(undefined);
|
|
578
578
|
},
|
|
579
579
|
{
|
|
580
|
+
onLeft: dialogOptions?.onLeft
|
|
581
|
+
? () => {
|
|
582
|
+
this.hideHookSelector();
|
|
583
|
+
dialogOptions.onLeft?.();
|
|
584
|
+
finish(undefined);
|
|
585
|
+
}
|
|
586
|
+
: undefined,
|
|
587
|
+
onRight: dialogOptions?.onRight
|
|
588
|
+
? () => {
|
|
589
|
+
this.hideHookSelector();
|
|
590
|
+
dialogOptions.onRight?.();
|
|
591
|
+
finish(undefined);
|
|
592
|
+
}
|
|
593
|
+
: undefined,
|
|
594
|
+
helpText: dialogOptions?.helpText,
|
|
580
595
|
initialIndex: dialogOptions?.initialIndex,
|
|
581
596
|
timeout: dialogOptions?.timeout,
|
|
597
|
+
onTimeout: dialogOptions?.onTimeout,
|
|
582
598
|
tui: this.ctx.ui,
|
|
583
599
|
outline: dialogOptions?.outline,
|
|
584
600
|
maxVisible,
|
|
@@ -651,6 +667,11 @@ export class ExtensionUiController {
|
|
|
651
667
|
this.hideHookInput();
|
|
652
668
|
finish(undefined);
|
|
653
669
|
},
|
|
670
|
+
{
|
|
671
|
+
timeout: dialogOptions?.timeout,
|
|
672
|
+
onTimeout: dialogOptions?.onTimeout,
|
|
673
|
+
tui: this.ctx.ui,
|
|
674
|
+
},
|
|
654
675
|
);
|
|
655
676
|
this.ctx.editorContainer.clear();
|
|
656
677
|
this.ctx.editorContainer.addChild(this.ctx.hookInput);
|
|
@@ -839,6 +839,8 @@ export class MCPCommandController {
|
|
|
839
839
|
const userPath = getMCPConfigPath("user", cwd);
|
|
840
840
|
const projectPath = getMCPConfigPath("project", cwd);
|
|
841
841
|
|
|
842
|
+
const userPathLabel = shortenPath(userPath);
|
|
843
|
+
const projectPathLabel = shortenPath(projectPath);
|
|
842
844
|
const [userConfig, projectConfig] = await Promise.all([
|
|
843
845
|
readMCPConfigFile(userPath),
|
|
844
846
|
readMCPConfigFile(projectPath),
|
|
@@ -884,7 +886,7 @@ export class MCPCommandController {
|
|
|
884
886
|
|
|
885
887
|
// Show user-level servers
|
|
886
888
|
if (userServers.length > 0) {
|
|
887
|
-
lines.push(theme.fg("accent", "User level") + theme.fg("muted", ` (
|
|
889
|
+
lines.push(theme.fg("accent", "User level") + theme.fg("muted", ` (${userPathLabel}):`));
|
|
888
890
|
for (const name of userServers) {
|
|
889
891
|
const config = userConfig.mcpServers![name];
|
|
890
892
|
const type = config.type ?? "stdio";
|
|
@@ -907,7 +909,7 @@ export class MCPCommandController {
|
|
|
907
909
|
|
|
908
910
|
// Show project-level servers
|
|
909
911
|
if (projectServers.length > 0) {
|
|
910
|
-
lines.push(theme.fg("accent", "Project level") + theme.fg("muted", ` (
|
|
912
|
+
lines.push(theme.fg("accent", "Project level") + theme.fg("muted", ` (${projectPathLabel}):`));
|
|
911
913
|
for (const name of projectServers) {
|
|
912
914
|
const config = projectConfig.mcpServers![name];
|
|
913
915
|
const type = config.type ?? "stdio";
|
|
@@ -98,6 +98,7 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
98
98
|
|
|
99
99
|
if (opts?.timeout !== undefined) {
|
|
100
100
|
timeoutId = setTimeout(() => {
|
|
101
|
+
opts.onTimeout?.();
|
|
101
102
|
cleanup();
|
|
102
103
|
resolve(defaultValue);
|
|
103
104
|
}, opts.timeout);
|
|
@@ -119,12 +120,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
119
120
|
dialogOptions,
|
|
120
121
|
undefined,
|
|
121
122
|
{ method: "select", title, options, timeout: dialogOptions?.timeout },
|
|
122
|
-
response =>
|
|
123
|
-
"cancelled" in response && response.cancelled
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
response => {
|
|
124
|
+
if ("cancelled" in response && response.cancelled) {
|
|
125
|
+
if (response.timedOut) dialogOptions?.onTimeout?.();
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
if ("value" in response) return response.value;
|
|
129
|
+
return undefined;
|
|
130
|
+
},
|
|
128
131
|
);
|
|
129
132
|
}
|
|
130
133
|
|
|
@@ -133,12 +136,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
133
136
|
dialogOptions,
|
|
134
137
|
false,
|
|
135
138
|
{ method: "confirm", title, message, timeout: dialogOptions?.timeout },
|
|
136
|
-
response =>
|
|
137
|
-
"cancelled" in response && response.cancelled
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
response => {
|
|
140
|
+
if ("cancelled" in response && response.cancelled) {
|
|
141
|
+
if (response.timedOut) dialogOptions?.onTimeout?.();
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
if ("confirmed" in response) return response.confirmed;
|
|
145
|
+
return false;
|
|
146
|
+
},
|
|
142
147
|
);
|
|
143
148
|
}
|
|
144
149
|
|
|
@@ -151,12 +156,14 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
|
|
|
151
156
|
dialogOptions,
|
|
152
157
|
undefined,
|
|
153
158
|
{ method: "input", title, placeholder, timeout: dialogOptions?.timeout },
|
|
154
|
-
response =>
|
|
155
|
-
"cancelled" in response && response.cancelled
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
response => {
|
|
160
|
+
if ("cancelled" in response && response.cancelled) {
|
|
161
|
+
if (response.timedOut) dialogOptions?.onTimeout?.();
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
if ("value" in response) return response.value;
|
|
165
|
+
return undefined;
|
|
166
|
+
},
|
|
160
167
|
);
|
|
161
168
|
}
|
|
162
169
|
|
|
@@ -227,7 +227,7 @@ export type RpcExtensionUIRequest =
|
|
|
227
227
|
export type RpcExtensionUIResponse =
|
|
228
228
|
| { type: "extension_ui_response"; id: string; value: string }
|
|
229
229
|
| { type: "extension_ui_response"; id: string; confirmed: boolean }
|
|
230
|
-
| { type: "extension_ui_response"; id: string; cancelled: true };
|
|
230
|
+
| { type: "extension_ui_response"; id: string; cancelled: true; timedOut?: boolean };
|
|
231
231
|
|
|
232
232
|
// ============================================================================
|
|
233
233
|
// Helper type for extracting command types
|
|
@@ -3,6 +3,7 @@ Searches files using powerful regex matching built on ripgrep.
|
|
|
3
3
|
<instruction>
|
|
4
4
|
- Supports full regex syntax (e.g., `log.*Error`, `function\\s+\\w+`); literal braces need escaping (`interface\\{\\}` for `interface{}` in Go)
|
|
5
5
|
- Filter files with `glob` (e.g., `*.js`, `**/*.tsx`) or `type` (e.g., `js`, `py`, `rust`)
|
|
6
|
+
- Respects `.gitignore` by default; set `gitignore: false` to include ignored files
|
|
6
7
|
- For cross-line patterns like `struct \\{[\\s\\S]*?field`, set `multiline: true` if needed
|
|
7
8
|
- If the pattern contains a literal `\n`, multiline defaults to true
|
|
8
9
|
</instruction>
|
|
@@ -43,13 +43,12 @@ Every edit has `op`, `pos`, and `lines`. Range replaces also have `end`. Both `p
|
|
|
43
43
|
|
|
44
44
|
<rules>
|
|
45
45
|
1. **Minimize scope:** You **MUST** use one logical mutation per operation.
|
|
46
|
-
2.
|
|
47
|
-
3. **
|
|
48
|
-
|
|
49
|
-
-
|
|
50
|
-
-
|
|
51
|
-
|
|
52
|
-
5. **Preserve idiomatic sibling spacing:** When inserting declarations between top-level siblings, you **MUST** preserve existing blank-line separators. If siblings are separated by one blank line, include a trailing `""` in `lines` so inserted code keeps the same spacing.
|
|
46
|
+
2. **`end` is inclusive:** If `lines` includes a closing token (`}`, `]`, `)`, `);`, `},`), `end` **MUST** include the original boundary line. To delete a line while keeping neighbors, use `lines: null` — do not replace it with an adjacent line's content.
|
|
47
|
+
3. **Copy indentation from `read` output:** Leading whitespace in `lines` **MUST** follow adjacent lines exactly. Do not reconstruct from memory.
|
|
48
|
+
4. **Verify the splice before submitting:** For each edit op, mentally read the result:
|
|
49
|
+
- Does the last `lines` entry duplicate the line surviving after `end`? → extend `end` or remove the duplicate.
|
|
50
|
+
- Does the first `lines` entry duplicate the line before `pos`? → the edit is wrong.
|
|
51
|
+
- For `prepend`/`append`: does new code land inside or outside the enclosing block? Trace the braces.
|
|
53
52
|
</rules>
|
|
54
53
|
|
|
55
54
|
<recovery>
|
|
@@ -62,56 +61,18 @@ Every edit has `op`, `pos`, and `lines`. Range replaces also have `end`. Both `p
|
|
|
62
61
|
{{hlinefull 23 " const timeout: number = 5000;"}}
|
|
63
62
|
```
|
|
64
63
|
```
|
|
65
|
-
{
|
|
66
|
-
path: "…",
|
|
67
|
-
edits: [{
|
|
68
|
-
op: "replace",
|
|
69
|
-
pos: {{hlinejsonref 23 " const timeout: number = 5000;"}},
|
|
70
|
-
lines: [" const timeout: number = 30_000;"]
|
|
71
|
-
}]
|
|
72
|
-
}
|
|
64
|
+
{ op: "replace", pos: {{hlinejsonref 23 " const timeout: number = 5000;"}}, lines: [" const timeout: number = 30_000;"] }
|
|
73
65
|
```
|
|
74
66
|
</example>
|
|
75
67
|
|
|
76
68
|
<example name="delete lines">
|
|
77
69
|
Single line — `lines: null` deletes entirely:
|
|
78
70
|
```
|
|
79
|
-
{
|
|
80
|
-
path: "…",
|
|
81
|
-
edits: [{
|
|
82
|
-
op: "replace",
|
|
83
|
-
pos: {{hlinejsonref 7 "// @ts-ignore"}},
|
|
84
|
-
lines: null
|
|
85
|
-
}]
|
|
86
|
-
}
|
|
71
|
+
{ op: "replace", pos: {{hlinejsonref 7 "// @ts-ignore"}}, lines: null }
|
|
87
72
|
```
|
|
88
73
|
Range — add `end`:
|
|
89
74
|
```
|
|
90
|
-
{
|
|
91
|
-
path: "…",
|
|
92
|
-
edits: [{
|
|
93
|
-
op: "replace",
|
|
94
|
-
pos: {{hlinejsonref 80 " // TODO: remove after migration"}},
|
|
95
|
-
end: {{hlinejsonref 83 " }"}},
|
|
96
|
-
lines: null
|
|
97
|
-
}]
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
</example>
|
|
101
|
-
|
|
102
|
-
<example name="clear text but keep the line break">
|
|
103
|
-
```ts
|
|
104
|
-
{{hlinefull 14 " placeholder: \"DO NOT SHIP\","}}
|
|
105
|
-
```
|
|
106
|
-
```
|
|
107
|
-
{
|
|
108
|
-
path: "…",
|
|
109
|
-
edits: [{
|
|
110
|
-
op: "replace",
|
|
111
|
-
pos: {{hlinejsonref 14 " placeholder: \"DO NOT SHIP\","}},
|
|
112
|
-
lines: [""]
|
|
113
|
-
}]
|
|
114
|
-
}
|
|
75
|
+
{ op: "replace", pos: {{hlinejsonref 80 " // TODO: remove after migration"}}, end: {{hlinejsonref 83 " }"}}, lines: null }
|
|
115
76
|
```
|
|
116
77
|
</example>
|
|
117
78
|
|
|
@@ -124,47 +85,35 @@ Range — add `end`:
|
|
|
124
85
|
```
|
|
125
86
|
Include the closing `}` in the replaced range — stopping one line short orphans the brace or duplicates it.
|
|
126
87
|
```
|
|
127
|
-
{
|
|
128
|
-
path: "…",
|
|
129
|
-
edits: [{
|
|
130
|
-
op: "replace",
|
|
131
|
-
pos: {{hlinejsonref 61 " console.error(err);"}},
|
|
132
|
-
end: {{hlinejsonref 63 " }"}},
|
|
133
|
-
lines: [
|
|
134
|
-
" if (isEnoent(err)) return null;",
|
|
135
|
-
" throw err;",
|
|
136
|
-
" }"
|
|
137
|
-
]
|
|
138
|
-
}]
|
|
139
|
-
}
|
|
88
|
+
{ op: "replace", pos: {{hlinejsonref 61 " console.error(err);"}}, end: {{hlinejsonref 63 " }"}}, lines: [" if (isEnoent(err)) return null;", " throw err;", " }"] }
|
|
140
89
|
```
|
|
141
90
|
</example>
|
|
142
91
|
|
|
143
|
-
<example name="
|
|
92
|
+
<example name="insert inside a block (good vs bad)">
|
|
93
|
+
Adding a method inside a class — anchor on the **closing brace**, not after it.
|
|
144
94
|
```ts
|
|
145
|
-
{{hlinefull
|
|
146
|
-
{{hlinefull
|
|
147
|
-
{{hlinefull
|
|
148
|
-
{{hlinefull
|
|
95
|
+
{{hlinefull 20 " greet() {"}}
|
|
96
|
+
{{hlinefull 21 " return \"hi\";"}}
|
|
97
|
+
{{hlinefull 22 " }"}}
|
|
98
|
+
{{hlinefull 23 "}"}}
|
|
99
|
+
{{hlinefull 24 ""}}
|
|
100
|
+
{{hlinefull 25 "function other() {"}}
|
|
101
|
+
```
|
|
102
|
+
Bad — appends **after** closing `}` (method lands outside the class):
|
|
103
|
+
```
|
|
104
|
+
{ op: "append", pos: {{hlinejsonref 23 "}"}}, lines: [" newMethod() {", " return 1;", " }"] }
|
|
149
105
|
```
|
|
150
|
-
|
|
106
|
+
Result — `newMethod` is a **top-level function**, not a class method:
|
|
151
107
|
```
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
"\t\tauditLog(id);",
|
|
161
|
-
"\t\tdeleteRecord(id);",
|
|
162
|
-
"\t}"
|
|
163
|
-
]
|
|
164
|
-
}]
|
|
165
|
-
}
|
|
108
|
+
} ← class closes here
|
|
109
|
+
newMethod() {
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
Good — prepends **before** closing `}` (method stays inside the class):
|
|
114
|
+
```
|
|
115
|
+
{ op: "prepend", pos: {{hlinejsonref 23 "}"}}, lines: [" newMethod() {", " return 1;", " }"] }
|
|
166
116
|
```
|
|
167
|
-
Also apply the same rule to `);`, `],`, and `},` closers: if replacement includes the closer token, `end` must include the original closer line.
|
|
168
117
|
</example>
|
|
169
118
|
|
|
170
119
|
<example name="insert between sibling declarations">
|
|
@@ -179,71 +128,24 @@ Also apply the same rule to `);`, `],`, and `},` closers: if replacement include
|
|
|
179
128
|
```
|
|
180
129
|
Use a trailing `""` to preserve the blank line between top-level sibling declarations.
|
|
181
130
|
```
|
|
182
|
-
{
|
|
183
|
-
path: "…",
|
|
184
|
-
edits: [{
|
|
185
|
-
op: "prepend",
|
|
186
|
-
pos: {{hlinejsonref 48 "function y() {"}},
|
|
187
|
-
lines: [
|
|
188
|
-
"function z() {",
|
|
189
|
-
" runZ();",
|
|
190
|
-
"}",
|
|
191
|
-
""
|
|
192
|
-
]
|
|
193
|
-
}]
|
|
194
|
-
}
|
|
131
|
+
{ op: "prepend", pos: {{hlinejsonref 48 "function y() {"}}, lines: ["function z() {", " runZ();", "}", ""] }
|
|
195
132
|
```
|
|
196
133
|
</example>
|
|
197
134
|
|
|
198
|
-
<example name="
|
|
199
|
-
|
|
135
|
+
<example name="disambiguate anchors">
|
|
136
|
+
Blank lines and repeated patterns (`}`, `return null;`) appear many times — never anchor on them when a unique line exists nearby.
|
|
200
137
|
```ts
|
|
201
|
-
{{hlinefull
|
|
202
|
-
{{hlinefull
|
|
203
|
-
{{hlinefull
|
|
204
|
-
```
|
|
205
|
-
Bad — append after "}"
|
|
206
|
-
Good — anchors to structural line:
|
|
138
|
+
{{hlinefull 46 "}"}}
|
|
139
|
+
{{hlinefull 47 ""}}
|
|
140
|
+
{{hlinefull 48 "function processItem(item: Item) {"}}
|
|
207
141
|
```
|
|
208
|
-
|
|
209
|
-
path: "…",
|
|
210
|
-
edits: [{
|
|
211
|
-
op: "prepend",
|
|
212
|
-
pos: {{hlinejsonref 103 "export function serialize(data: unknown): string {"}},
|
|
213
|
-
lines: [
|
|
214
|
-
"function validate(data: unknown): boolean {",
|
|
215
|
-
" return data != null && typeof data === \"object\";",
|
|
216
|
-
"}",
|
|
217
|
-
""
|
|
218
|
-
]
|
|
219
|
-
}]
|
|
220
|
-
}
|
|
142
|
+
Bad — anchoring on the blank line (ambiguous, may shift):
|
|
221
143
|
```
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
<example name="indentation must match context">
|
|
225
|
-
Leading whitespace in `lines` **MUST** be copied from the `read` output, not reconstructed from memory. If the file uses tabs, use `\t` in JSON — you **MUST NOT** use `\\t`, which produces a literal backslash-t in the file.
|
|
226
|
-
```ts
|
|
227
|
-
{{hlinefull 10 "class Foo {"}}
|
|
228
|
-
{{hlinefull 11 "\tbar() {"}}
|
|
229
|
-
{{hlinefull 12 "\t\treturn 1;"}}
|
|
230
|
-
{{hlinefull 13 "\t}"}}
|
|
231
|
-
{{hlinefull 14 "}"}}
|
|
144
|
+
{ op: "append", pos: {{hlinejsonref 47 ""}}, lines: ["function helper() { }"] }
|
|
232
145
|
```
|
|
233
|
-
Good —
|
|
146
|
+
Good — anchor on the unique declaration line:
|
|
234
147
|
```
|
|
235
|
-
{
|
|
236
|
-
path: "…",
|
|
237
|
-
edits: [{
|
|
238
|
-
op: "prepend",
|
|
239
|
-
pos: {{hlinejsonref 14 "}"}},
|
|
240
|
-
lines: [
|
|
241
|
-
"\tbaz() {",
|
|
242
|
-
"\t\treturn 2;",
|
|
243
|
-
"\t}"
|
|
244
|
-
]
|
|
245
|
-
}]
|
|
246
|
-
}
|
|
148
|
+
{ op: "prepend", pos: {{hlinejsonref 48 "function processItem(item: Item) {"}}, lines: ["function helper() { }", ""] }
|
|
247
149
|
```
|
|
248
150
|
</example>
|
|
249
151
|
|
|
@@ -1137,6 +1137,56 @@ async function collectSessionsFromFiles(files: string[], storage: SessionStorage
|
|
|
1137
1137
|
return sessions;
|
|
1138
1138
|
}
|
|
1139
1139
|
|
|
1140
|
+
export interface ResolvedSessionMatch {
|
|
1141
|
+
session: SessionInfo;
|
|
1142
|
+
scope: "local" | "global";
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function sessionMatchesResumeArg(session: SessionInfo, sessionArg: string): boolean {
|
|
1146
|
+
const normalizedArg = sessionArg.toLowerCase();
|
|
1147
|
+
const normalizedId = session.id.toLowerCase();
|
|
1148
|
+
if (normalizedId.startsWith(normalizedArg)) {
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const fileName = path.basename(session.path, ".jsonl").toLowerCase();
|
|
1153
|
+
if (fileName.startsWith(normalizedArg)) {
|
|
1154
|
+
return true;
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const separator = fileName.lastIndexOf("_");
|
|
1158
|
+
if (separator < 0) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const fileSessionId = fileName.slice(separator + 1);
|
|
1163
|
+
return fileSessionId.startsWith(normalizedArg);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
export async function resolveResumableSession(
|
|
1167
|
+
sessionArg: string,
|
|
1168
|
+
cwd: string,
|
|
1169
|
+
sessionDir?: string,
|
|
1170
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
1171
|
+
): Promise<ResolvedSessionMatch | undefined> {
|
|
1172
|
+
const localSessions = await SessionManager.list(cwd, sessionDir, storage);
|
|
1173
|
+
const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
|
|
1174
|
+
if (localMatch) {
|
|
1175
|
+
return { session: localMatch, scope: "local" };
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (sessionDir) {
|
|
1179
|
+
return undefined;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const globalSessions = await SessionManager.listAll(storage);
|
|
1183
|
+
const globalMatch = globalSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
|
|
1184
|
+
if (!globalMatch) {
|
|
1185
|
+
return undefined;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
return { session: globalMatch, scope: "global" };
|
|
1189
|
+
}
|
|
1140
1190
|
export class SessionManager {
|
|
1141
1191
|
#sessionId: string = "";
|
|
1142
1192
|
#sessionName: string | undefined;
|