@oh-my-pi/pi-coding-agent 13.6.2 → 13.7.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.
@@ -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", ` (~/.omp/mcp.json):`));
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", ` (.omp/mcp.json):`));
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
- ? undefined
125
- : "value" in response
126
- ? response.value
127
- : undefined,
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
- ? false
139
- : "confirmed" in response
140
- ? response.confirmed
141
- : false,
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
- ? undefined
157
- : "value" in response
158
- ? response.value
159
- : undefined,
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. **Prefer insertion over neighbor rewrites:** You **SHOULD** anchor on structural boundaries (`}`, `]`, `},`), not interior lines.
47
- 3. **Range end tag (inclusive):** `end` is inclusive and **MUST** point to the final line being replaced.
48
- - If `lines` includes a closing boundary token (`}`, `]`, `)`, `);`, `},`), `end` **MUST** include the original boundary line.
49
- - You **MUST NOT** set `end` to an interior line and then re-add the boundary token in `lines`; that duplicates the next surviving line.
50
- - To remove a line while keeping its neighbors, **delete** it (`lines: null`). You **MUST NOT** replace it with the content of an adjacent line that line still exists and will be duplicated.
51
- 4. **Match surrounding indentation:** Leading whitespace in `lines` **MUST** be copied verbatim from adjacent lines in the `read` output. Do not infer or reconstruct indentation from memory — count the actual leading spaces on the lines immediately above and below the insertion or replacement point.
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="inclusive end avoids duplicate boundary">
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 70 "\tif (user.isAdmin) {"}}
146
- {{hlinefull 71 "\t\tdeleteRecord(id);"}}
147
- {{hlinefull 72 "\t}"}}
148
- {{hlinefull 73 "\tafter();"}}
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
- The block grows by one line and the condition changes two single-line ops would be needed otherwise. Since `}` appears in `lines`, `end` must include `72`:
106
+ Result — `newMethod` is a **top-level function**, not a class method:
151
107
  ```
152
- {
153
- path: "…",
154
- edits: [{
155
- op: "replace",
156
- pos: {{hlinejsonref 70 "\tif (user.isAdmin) {"}},
157
- end: {{hlinejsonref 72 "\t}"}},
158
- lines: [
159
- "\tif (user.isAdmin && confirmed) {",
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="anchor to structure, not whitespace">
199
- Trailing `""` in `lines` preserves blank-line separators. Anchor to the structural line, not the blank line above blank lines are ambiguous and shift.
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 101 "}"}}
202
- {{hlinefull 102 ""}}
203
- {{hlinefull 103 "export function serialize(data: unknown): string {"}}
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
- </example>
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 — `\t` in JSON is a real tab, matching the file's indentation:
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;