@oh-my-pi/pi-coding-agent 15.10.1 → 15.10.2
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 +67 -0
- package/dist/types/cli/startup-cwd.d.ts +2 -0
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-provider-priority.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +4 -1
- package/dist/types/config/settings.d.ts +7 -2
- package/dist/types/debug/report-bundle.d.ts +3 -0
- package/dist/types/edit/file-snapshot-store.d.ts +18 -10
- package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -1
- package/dist/types/lsp/client.d.ts +10 -0
- package/dist/types/main.d.ts +3 -9
- package/dist/types/mcp/tool-bridge.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -1
- package/dist/types/modes/components/status-line.d.ts +2 -0
- package/dist/types/modes/controllers/event-controller.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keywords.d.ts +1 -1
- package/dist/types/modes/markdown-prose.d.ts +1 -1
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/workflow.d.ts +3 -3
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +5 -2
- package/dist/types/task/executor.d.ts +10 -0
- package/dist/types/tools/eval.d.ts +8 -0
- package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
- package/dist/types/tools/github-cache.d.ts +12 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/search.d.ts +2 -2
- package/dist/types/tools/yield.d.ts +8 -0
- package/package.json +9 -9
- package/src/cli/args.ts +3 -1
- package/src/cli/dry-balance-cli.ts +2 -4
- package/src/cli/startup-cwd.ts +68 -0
- package/src/commands/launch.ts +3 -0
- package/src/commit/model-selection.ts +3 -2
- package/src/config/model-provider-priority.ts +55 -0
- package/src/config/model-registry.ts +4 -22
- package/src/config/model-resolver.ts +39 -7
- package/src/config/settings.ts +86 -41
- package/src/debug/index.ts +8 -0
- package/src/debug/raw-sse-buffer.ts +7 -4
- package/src/debug/report-bundle.ts +9 -0
- package/src/edit/file-snapshot-store.ts +33 -1
- package/src/edit/hashline/filesystem.ts +2 -1
- package/src/eval/__tests__/llm-bridge.test.ts +20 -0
- package/src/eval/js/context-manager.ts +32 -15
- package/src/eval/llm-bridge.ts +14 -3
- package/src/eval/py/__tests__/prelude.test.ts +19 -0
- package/src/eval/py/executor.ts +23 -11
- package/src/eval/py/prelude.py +1 -1
- package/src/extensibility/extensions/types.ts +10 -1
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/lsp/client.ts +23 -11
- package/src/lsp/config.ts +11 -1
- package/src/lsp/index.ts +61 -9
- package/src/main.ts +91 -65
- package/src/mcp/tool-bridge.ts +2 -0
- package/src/memories/index.ts +2 -2
- package/src/modes/components/custom-editor.ts +143 -111
- package/src/modes/components/model-selector.ts +59 -13
- package/src/modes/components/oauth-selector.ts +33 -7
- package/src/modes/components/status-line.ts +19 -4
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/user-message.ts +1 -1
- package/src/modes/controllers/event-controller.ts +26 -0
- package/src/modes/controllers/input-controller.ts +46 -7
- package/src/modes/interactive-mode.ts +107 -20
- package/src/modes/magic-keywords.ts +1 -1
- package/src/modes/markdown-prose.ts +1 -1
- package/src/modes/theme/shimmer.ts +20 -9
- package/src/modes/types.ts +3 -0
- package/src/modes/workflow.ts +10 -10
- package/src/prompts/system/workflow-notice.md +1 -1
- package/src/prompts/tools/bash.md +9 -0
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/eval.md +2 -1
- package/src/prompts/tools/read.md +2 -2
- package/src/sdk.ts +26 -9
- package/src/session/agent-session.ts +37 -12
- package/src/session/auth-storage.ts +2 -0
- package/src/session/session-manager.ts +96 -23
- package/src/task/executor.ts +71 -36
- package/src/task/render.ts +3 -4
- package/src/tools/bash.ts +7 -0
- package/src/tools/browser/tab-supervisor.ts +13 -1
- package/src/tools/browser/tab-worker.ts +33 -4
- package/src/tools/eval.ts +13 -2
- package/src/tools/find.ts +7 -0
- package/src/tools/gh-cache-invalidation.ts +200 -0
- package/src/tools/github-cache.ts +25 -0
- package/src/tools/inspect-image.ts +2 -2
- package/src/tools/path-utils.ts +28 -2
- package/src/tools/plan-mode-guard.ts +52 -7
- package/src/tools/read.ts +25 -12
- package/src/tools/search.ts +38 -3
- package/src/tools/write.ts +2 -2
- package/src/tools/yield.ts +10 -1
- package/src/utils/commit-message-generator.ts +2 -2
- package/src/utils/enhanced-paste.ts +30 -2
- package/src/web/search/providers/codex.ts +37 -8
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Editor, type KeyId,
|
|
1
|
+
import { addKeyAliases, canonicalKeyId, Editor, type KeyId, parseKey, parseKittySequence } from "@oh-my-pi/pi-tui";
|
|
2
2
|
import type { AppKeybinding } from "../../config/keybindings";
|
|
3
3
|
import { imageReferenceHyperlink, renderImageReferences } from "../image-references";
|
|
4
4
|
import { highlightMagicKeywords } from "../magic-keywords";
|
|
@@ -47,6 +47,14 @@ const DEFAULT_ACTION_KEYS: Record<ConfigurableEditorAction, KeyId[]> = {
|
|
|
47
47
|
"app.clipboard.copyPrompt": ["alt+shift+c"],
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
function buildMatchKeys(keys: readonly KeyId[]): Set<string> {
|
|
51
|
+
const matchKeys = new Set<string>();
|
|
52
|
+
for (const key of keys) {
|
|
53
|
+
addKeyAliases(matchKeys, key);
|
|
54
|
+
}
|
|
55
|
+
return matchKeys;
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
const BRACKETED_PASTE_START = "\x1b[200~";
|
|
51
59
|
const BRACKETED_PASTE_END = "\x1b[201~";
|
|
52
60
|
const BRACKETED_IMAGE_PATH_REGEX = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
@@ -68,7 +76,7 @@ export function extractBracketedImagePastePath(data: string): string | undefined
|
|
|
68
76
|
export class CustomEditor extends Editor {
|
|
69
77
|
imageLinks?: readonly (string | undefined)[];
|
|
70
78
|
|
|
71
|
-
/** Gradient-highlight the "ultrathink" / "orchestrate" / "
|
|
79
|
+
/** Gradient-highlight the "ultrathink" / "orchestrate" / "workflowz" keywords as the user types
|
|
72
80
|
* them, skipping any occurrence inside code spans, fenced blocks, or XML sections. Also make
|
|
73
81
|
* pasted image placeholders visually distinct and hyperlink them once their blob file exists. */
|
|
74
82
|
decorateText = (text: string): string =>
|
|
@@ -108,21 +116,38 @@ export class CustomEditor extends Editor {
|
|
|
108
116
|
|
|
109
117
|
/** Custom key handlers from extensions and non-built-in app actions. */
|
|
110
118
|
#customKeyHandlers = new Map<KeyId, () => void>();
|
|
119
|
+
#customMatchKeys = new Map<string, () => void>();
|
|
111
120
|
#actionKeys = new Map<ConfigurableEditorAction, KeyId[]>(
|
|
112
121
|
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [action as ConfigurableEditorAction, [...keys]]),
|
|
113
122
|
);
|
|
123
|
+
#actionMatchKeys = new Map<ConfigurableEditorAction, Set<string>>(
|
|
124
|
+
Object.entries(DEFAULT_ACTION_KEYS).map(([action, keys]) => [
|
|
125
|
+
action as ConfigurableEditorAction,
|
|
126
|
+
buildMatchKeys(keys),
|
|
127
|
+
]),
|
|
128
|
+
);
|
|
114
129
|
|
|
115
130
|
setActionKeys(action: ConfigurableEditorAction, keys: KeyId[]): void {
|
|
116
131
|
this.#actionKeys.set(action, [...keys]);
|
|
132
|
+
this.#rebuildActionMatchKeys(action);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#rebuildActionMatchKeys(action: ConfigurableEditorAction): void {
|
|
136
|
+
this.#actionMatchKeys.set(action, buildMatchKeys(this.#actionKeys.get(action) ?? []));
|
|
117
137
|
}
|
|
118
138
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
139
|
+
#rebuildCustomMatchKeys(): void {
|
|
140
|
+
this.#customMatchKeys.clear();
|
|
141
|
+
for (const [keyId, handler] of this.#customKeyHandlers) {
|
|
142
|
+
for (const alias of buildMatchKeys([keyId])) {
|
|
143
|
+
// Preserve current iteration behavior: the first registered handler for colliding aliases wins.
|
|
144
|
+
if (!this.#customMatchKeys.has(alias)) this.#customMatchKeys.set(alias, handler);
|
|
145
|
+
}
|
|
124
146
|
}
|
|
125
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#matchesAction(canonical: string | undefined, action: ConfigurableEditorAction): boolean {
|
|
150
|
+
return canonical !== undefined && (this.#actionMatchKeys.get(action)?.has(canonical) ?? false);
|
|
126
151
|
}
|
|
127
152
|
|
|
128
153
|
/**
|
|
@@ -130,6 +155,7 @@ export class CustomEditor extends Editor {
|
|
|
130
155
|
*/
|
|
131
156
|
setCustomKeyHandler(key: KeyId, handler: () => void): void {
|
|
132
157
|
this.#customKeyHandlers.set(key, handler);
|
|
158
|
+
this.#rebuildCustomMatchKeys();
|
|
133
159
|
}
|
|
134
160
|
|
|
135
161
|
/**
|
|
@@ -137,6 +163,7 @@ export class CustomEditor extends Editor {
|
|
|
137
163
|
*/
|
|
138
164
|
removeCustomKeyHandler(key: KeyId): void {
|
|
139
165
|
this.#customKeyHandlers.delete(key);
|
|
166
|
+
this.#rebuildCustomMatchKeys();
|
|
140
167
|
}
|
|
141
168
|
|
|
142
169
|
/**
|
|
@@ -144,11 +171,12 @@ export class CustomEditor extends Editor {
|
|
|
144
171
|
*/
|
|
145
172
|
clearCustomKeyHandlers(): void {
|
|
146
173
|
this.#customKeyHandlers.clear();
|
|
174
|
+
this.#rebuildCustomMatchKeys();
|
|
147
175
|
}
|
|
148
176
|
|
|
149
177
|
handleInput(data: string): void {
|
|
150
|
-
const
|
|
151
|
-
if (
|
|
178
|
+
const kittyParsed = parseKittySequence(data);
|
|
179
|
+
if (kittyParsed && (kittyParsed.modifier & 64) !== 0 && this.onCapsLock) {
|
|
152
180
|
// Caps Lock is modifier bit 64
|
|
153
181
|
this.onCapsLock();
|
|
154
182
|
return;
|
|
@@ -160,125 +188,129 @@ export class CustomEditor extends Editor {
|
|
|
160
188
|
return;
|
|
161
189
|
}
|
|
162
190
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
void this.onPasteImage();
|
|
166
|
-
return;
|
|
167
|
-
}
|
|
191
|
+
const parsedKey = parseKey(data);
|
|
192
|
+
const canonical = parsedKey !== undefined ? canonicalKeyId(parsedKey) : undefined;
|
|
168
193
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
this.
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
if (canonical !== undefined) {
|
|
195
|
+
// Intercept configured image paste (async - fires and handles result)
|
|
196
|
+
if (this.#matchesAction(canonical, "app.clipboard.pasteImage") && this.onPasteImage) {
|
|
197
|
+
void this.onPasteImage();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
174
200
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
201
|
+
// Intercept configured raw text paste (fires and handles result)
|
|
202
|
+
if (this.#matchesAction(canonical, "app.clipboard.pasteTextRaw") && this.onPasteTextRaw) {
|
|
203
|
+
this.onPasteTextRaw();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
180
206
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
207
|
+
// Intercept configured external editor shortcut
|
|
208
|
+
if (this.#matchesAction(canonical, "app.editor.external") && this.onExternalEditor) {
|
|
209
|
+
this.onExternalEditor();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
186
212
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
213
|
+
// Intercept configured temporary model selector shortcut
|
|
214
|
+
if (this.#matchesAction(canonical, "app.model.selectTemporary") && this.onSelectModelTemporary) {
|
|
215
|
+
this.onSelectModelTemporary();
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
192
218
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
219
|
+
// Intercept configured display reset shortcut
|
|
220
|
+
if (this.#matchesAction(canonical, "app.display.reset") && this.onDisplayReset) {
|
|
221
|
+
this.onDisplayReset();
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
198
224
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
225
|
+
// Intercept configured suspend shortcut
|
|
226
|
+
if (this.#matchesAction(canonical, "app.suspend") && this.onSuspend) {
|
|
227
|
+
this.onSuspend();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
204
230
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
231
|
+
// Intercept configured thinking block visibility toggle
|
|
232
|
+
if (this.#matchesAction(canonical, "app.thinking.toggle") && this.onToggleThinking) {
|
|
233
|
+
this.onToggleThinking();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
210
236
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
237
|
+
// Intercept configured model selector shortcut
|
|
238
|
+
if (this.#matchesAction(canonical, "app.model.select") && this.onSelectModel) {
|
|
239
|
+
this.onSelectModel();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
216
242
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
243
|
+
// Intercept configured history search shortcut
|
|
244
|
+
if (this.#matchesAction(canonical, "app.history.search") && this.onHistorySearch) {
|
|
245
|
+
this.onHistorySearch();
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
222
248
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
249
|
+
// Intercept configured tool output expansion shortcut
|
|
250
|
+
if (this.#matchesAction(canonical, "app.tools.expand") && this.onExpandTools) {
|
|
251
|
+
this.onExpandTools();
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
228
254
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
255
|
+
// Intercept configured backward model cycling (check before forward cycling)
|
|
256
|
+
if (this.#matchesAction(canonical, "app.model.cycleBackward") && this.onCycleModelBackward) {
|
|
257
|
+
this.onCycleModelBackward();
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
234
260
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
261
|
+
// Intercept configured forward model cycling
|
|
262
|
+
if (this.#matchesAction(canonical, "app.model.cycleForward") && this.onCycleModelForward) {
|
|
263
|
+
this.onCycleModelForward();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
240
266
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// single ESC from both closing an @ completion and aborting an active
|
|
247
|
-
// agent run (#1655).
|
|
248
|
-
if (this.#matchesAction(data, "app.interrupt") && this.onEscape && !this.isShowingAutocomplete()) {
|
|
249
|
-
this.onEscape();
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
267
|
+
// Intercept configured thinking level cycling
|
|
268
|
+
if (this.#matchesAction(canonical, "app.thinking.cycle") && this.onCycleThinkingLevel) {
|
|
269
|
+
this.onCycleThinkingLevel();
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
252
272
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
273
|
+
// Intercept configured interrupt shortcut.
|
|
274
|
+
// When the autocomplete popup is visible, ESC's first job is to dismiss
|
|
275
|
+
// the popup — let super.handleInput() route it to #cancelAutocomplete().
|
|
276
|
+
// The user can press ESC again afterward to fire the global interrupt
|
|
277
|
+
// handler. This matches the standard TUI/IDE pattern and prevents a
|
|
278
|
+
// single ESC from both closing an @ completion and aborting an active
|
|
279
|
+
// agent run (#1655).
|
|
280
|
+
if (this.#matchesAction(canonical, "app.interrupt") && this.onEscape && !this.isShowingAutocomplete()) {
|
|
281
|
+
this.onEscape();
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
258
284
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
285
|
+
// Intercept configured clear shortcut
|
|
286
|
+
if (this.#matchesAction(canonical, "app.clear") && this.onClear) {
|
|
287
|
+
this.onClear();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
266
290
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
291
|
+
// Intercept configured exit shortcut. Always consume the shortcut so it
|
|
292
|
+
// never reaches the parent handler; firing onExit is the controller's
|
|
293
|
+
// chance to snapshot the current text as a draft before shutting down.
|
|
294
|
+
if (this.#matchesAction(canonical, "app.exit")) {
|
|
295
|
+
this.onExit?.();
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
272
298
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
299
|
+
// Intercept configured dequeue shortcut (restore queued message to editor)
|
|
300
|
+
if (this.#matchesAction(canonical, "app.message.dequeue") && this.onDequeue) {
|
|
301
|
+
this.onDequeue();
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
278
304
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
305
|
+
// Intercept configured copy-prompt shortcut
|
|
306
|
+
if (this.#matchesAction(canonical, "app.clipboard.copyPrompt") && this.onCopyPrompt) {
|
|
307
|
+
this.onCopyPrompt();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check custom key handlers (extensions)
|
|
312
|
+
const handler = this.#customMatchKeys.get(canonical);
|
|
313
|
+
if (handler) {
|
|
282
314
|
handler();
|
|
283
315
|
return;
|
|
284
316
|
}
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import { formatNumber } from "@oh-my-pi/pi-utils";
|
|
18
18
|
import type { ModelRegistry } from "../../config/model-registry";
|
|
19
19
|
import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../../config/model-registry";
|
|
20
|
-
import { resolveModelRoleValue } from "../../config/model-resolver";
|
|
20
|
+
import { getModelMatchPreferences, resolveModelRoleValue } from "../../config/model-resolver";
|
|
21
21
|
import type { Settings } from "../../config/settings";
|
|
22
22
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
23
23
|
import { matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
@@ -31,6 +31,25 @@ function makeInvertedBadge(label: string, color: ThemeColor): string {
|
|
|
31
31
|
return `${bgAnsi}\x1b[30m ${label} \x1b[39m\x1b[49m`;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
function makeAutoSelectedBadge(label: string, color: ThemeColor): string {
|
|
35
|
+
return `${theme.fg("dim", "[")}${theme.fg(color, label)}${theme.fg("dim", " auto]")}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function makeRoleBadgeToken(label: string, color: ThemeColor, assigned: RoleAssignment): string {
|
|
39
|
+
if (assigned.autoSelected) {
|
|
40
|
+
const badge = makeAutoSelectedBadge(label, color);
|
|
41
|
+
if (assigned.thinkingLevel === ThinkingLevel.Inherit) {
|
|
42
|
+
return badge;
|
|
43
|
+
}
|
|
44
|
+
const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
|
|
45
|
+
return `${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const badge = makeInvertedBadge(label, color);
|
|
49
|
+
const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
|
|
50
|
+
return `${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
34
53
|
function normalizeSearchText(value: string): string {
|
|
35
54
|
return value
|
|
36
55
|
.toLowerCase()
|
|
@@ -86,6 +105,7 @@ interface ScopedModelItem {
|
|
|
86
105
|
interface RoleAssignment {
|
|
87
106
|
model: Model;
|
|
88
107
|
thinkingLevel: ConfiguredThinkingLevel;
|
|
108
|
+
autoSelected: boolean;
|
|
89
109
|
}
|
|
90
110
|
|
|
91
111
|
type RoleSelectCallback = (
|
|
@@ -271,12 +291,17 @@ export class ModelSelectorComponent extends Container {
|
|
|
271
291
|
});
|
|
272
292
|
}
|
|
273
293
|
|
|
274
|
-
#loadRoleModels(): void {
|
|
294
|
+
#loadRoleModels(autoCandidateModels?: ReadonlyArray<Model>): void {
|
|
295
|
+
const nextRoles = {} as Record<string, RoleAssignment | undefined>;
|
|
275
296
|
const allModels = this.#modelRegistry.getAll();
|
|
276
|
-
const matchPreferences =
|
|
277
|
-
|
|
297
|
+
const matchPreferences = getModelMatchPreferences(this.#settings);
|
|
298
|
+
const knownRoles = getKnownRoleIds(this.#settings);
|
|
299
|
+
const configuredRoles = new Set<string>();
|
|
300
|
+
|
|
301
|
+
for (const role of knownRoles) {
|
|
278
302
|
const roleValue = this.#settings.getModelRole(role);
|
|
279
303
|
if (!roleValue) continue;
|
|
304
|
+
configuredRoles.add(role);
|
|
280
305
|
|
|
281
306
|
const resolved = resolveModelRoleValue(roleValue, allModels, {
|
|
282
307
|
settings: this.#settings,
|
|
@@ -284,15 +309,39 @@ export class ModelSelectorComponent extends Container {
|
|
|
284
309
|
modelRegistry: this.#modelRegistry,
|
|
285
310
|
});
|
|
286
311
|
if (resolved.model) {
|
|
287
|
-
|
|
312
|
+
nextRoles[role] = {
|
|
288
313
|
model: resolved.model,
|
|
289
314
|
thinkingLevel:
|
|
290
315
|
resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined
|
|
291
316
|
? resolved.thinkingLevel
|
|
292
317
|
: ThinkingLevel.Inherit,
|
|
318
|
+
autoSelected: false,
|
|
293
319
|
};
|
|
294
320
|
}
|
|
295
321
|
}
|
|
322
|
+
|
|
323
|
+
if (autoCandidateModels && autoCandidateModels.length > 0) {
|
|
324
|
+
const candidates = [...autoCandidateModels];
|
|
325
|
+
for (const role of knownRoles) {
|
|
326
|
+
if (configuredRoles.has(role)) continue;
|
|
327
|
+
const resolved = resolveModelRoleValue(`pi/${role}`, candidates, {
|
|
328
|
+
settings: this.#settings,
|
|
329
|
+
matchPreferences,
|
|
330
|
+
modelRegistry: this.#modelRegistry,
|
|
331
|
+
});
|
|
332
|
+
if (!resolved.model) continue;
|
|
333
|
+
nextRoles[role] = {
|
|
334
|
+
model: resolved.model,
|
|
335
|
+
thinkingLevel:
|
|
336
|
+
resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined
|
|
337
|
+
? resolved.thinkingLevel
|
|
338
|
+
: ThinkingLevel.Inherit,
|
|
339
|
+
autoSelected: true,
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this.#roles = nextRoles;
|
|
296
345
|
}
|
|
297
346
|
|
|
298
347
|
/**
|
|
@@ -427,6 +476,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
427
476
|
}
|
|
428
477
|
|
|
429
478
|
const candidates = models.map(item => item.model);
|
|
479
|
+
this.#loadRoleModels(candidates);
|
|
430
480
|
const canonicalRecords = this.#modelRegistry.getCanonicalModels({
|
|
431
481
|
availableOnly: this.#scopedModels.length === 0,
|
|
432
482
|
candidates,
|
|
@@ -871,25 +921,21 @@ export class ModelSelectorComponent extends Container {
|
|
|
871
921
|
const isDisabled = this.#isItemDisabled(item);
|
|
872
922
|
const disabledSuffix = this.#formatContextLimitSuffix(item.model);
|
|
873
923
|
|
|
874
|
-
// Build role badges
|
|
924
|
+
// Build role badges. Solid badges are configured; outlined badges are auto-selected defaults.
|
|
875
925
|
const roleBadgeTokens: string[] = [];
|
|
876
926
|
for (const role of MODEL_ROLE_IDS) {
|
|
877
927
|
const { tag, color } = getRoleInfo(role, this.#settings);
|
|
878
928
|
const assigned = this.#roles[role];
|
|
879
929
|
if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
|
|
880
930
|
|
|
881
|
-
|
|
882
|
-
const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
|
|
883
|
-
roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
|
|
931
|
+
roleBadgeTokens.push(makeRoleBadgeToken(tag, color ?? "success", assigned));
|
|
884
932
|
}
|
|
885
933
|
// Custom role badges
|
|
886
934
|
for (const [role, assigned] of Object.entries(this.#roles)) {
|
|
887
935
|
if (role in MODEL_ROLES || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
|
|
888
936
|
const roleInfo = getRoleInfo(role, this.#settings);
|
|
889
937
|
const badgeLabel = roleInfo.tag ?? roleInfo.name;
|
|
890
|
-
|
|
891
|
-
const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
|
|
892
|
-
roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
|
|
938
|
+
roleBadgeTokens.push(makeRoleBadgeToken(badgeLabel, roleInfo.color ?? "muted", assigned));
|
|
893
939
|
}
|
|
894
940
|
const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
|
|
895
941
|
|
|
@@ -1184,7 +1230,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
1184
1230
|
const selectedThinkingLevel = thinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
|
|
1185
1231
|
|
|
1186
1232
|
// Update local state for UI
|
|
1187
|
-
this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
|
|
1233
|
+
this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel, autoSelected: false };
|
|
1188
1234
|
|
|
1189
1235
|
// Notify caller (for updating agent state if needed)
|
|
1190
1236
|
this.#onSelectCallback(item.model, role, selectedThinkingLevel, item.selector);
|
|
@@ -11,10 +11,20 @@ import {
|
|
|
11
11
|
} from "@oh-my-pi/pi-tui";
|
|
12
12
|
import { theme } from "../../modes/theme/theme";
|
|
13
13
|
import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
|
|
14
|
-
import type { AuthStorage } from "../../session/auth-storage";
|
|
14
|
+
import type { AuthStorage, CredentialOriginKind } from "../../session/auth-storage";
|
|
15
15
|
import { DynamicBorder } from "./dynamic-border";
|
|
16
16
|
|
|
17
17
|
const OAUTH_SELECTOR_MAX_VISIBLE = 10;
|
|
18
|
+
|
|
19
|
+
/** Compact, human-readable tag for each credential-origin leg. */
|
|
20
|
+
const ORIGIN_LABELS: Record<CredentialOriginKind, string> = {
|
|
21
|
+
runtime: "--api-key",
|
|
22
|
+
config: "config",
|
|
23
|
+
oauth: "login",
|
|
24
|
+
api_key: "api key",
|
|
25
|
+
env: "env",
|
|
26
|
+
fallback: "custom provider",
|
|
27
|
+
};
|
|
18
28
|
/**
|
|
19
29
|
* Component that renders an OAuth provider selector.
|
|
20
30
|
*/
|
|
@@ -146,20 +156,34 @@ export class OAuthSelectorComponent extends Container {
|
|
|
146
156
|
}
|
|
147
157
|
}
|
|
148
158
|
|
|
159
|
+
/**
|
|
160
|
+
* Muted provenance suffix (" (env: COPILOT_GITHUB_TOKEN)", " (login)", …) so
|
|
161
|
+
* the list distinguishes a real login from an env var aliasing the provider.
|
|
162
|
+
*/
|
|
163
|
+
#getSourceLabel(providerId: string): string {
|
|
164
|
+
const origin = this.#authStorage.getCredentialOrigin(providerId);
|
|
165
|
+
if (!origin) return "";
|
|
166
|
+
const detail = origin.kind === "env" && origin.envVar ? `env: ${origin.envVar}` : ORIGIN_LABELS[origin.kind];
|
|
167
|
+
return theme.fg("muted", ` (${detail})`);
|
|
168
|
+
}
|
|
169
|
+
|
|
149
170
|
#getStatusIndicator(providerId: string): string {
|
|
150
171
|
const state = this.#authState.get(providerId);
|
|
172
|
+
const source = this.#getSourceLabel(providerId);
|
|
151
173
|
if (state === "checking") {
|
|
152
174
|
const frameCount = theme.spinnerFrames.length;
|
|
153
175
|
const spinner = frameCount > 0 ? theme.spinnerFrames[this.#spinnerFrame % frameCount] : theme.status.pending;
|
|
154
|
-
return theme.fg("warning", ` ${spinner} checking`);
|
|
176
|
+
return theme.fg("warning", ` ${spinner} checking`) + source;
|
|
155
177
|
}
|
|
156
178
|
if (state === "invalid") {
|
|
157
|
-
return theme.fg("error", ` ${theme.status.error} invalid`);
|
|
179
|
+
return theme.fg("error", ` ${theme.status.error} invalid`) + source;
|
|
158
180
|
}
|
|
159
181
|
if (state === "valid") {
|
|
160
|
-
return theme.fg("success", ` ${theme.status.success} logged in`);
|
|
182
|
+
return theme.fg("success", ` ${theme.status.success} logged in`) + source;
|
|
161
183
|
}
|
|
162
|
-
return this.#hasSelectableAuth(providerId)
|
|
184
|
+
return this.#hasSelectableAuth(providerId)
|
|
185
|
+
? theme.fg("success", ` ${theme.status.success} logged in`) + source
|
|
186
|
+
: "";
|
|
163
187
|
}
|
|
164
188
|
|
|
165
189
|
#isSearchEnabled(): boolean {
|
|
@@ -178,8 +202,10 @@ export class OAuthSelectorComponent extends Container {
|
|
|
178
202
|
|
|
179
203
|
#getProviderSearchText(provider: OAuthProviderInfo): string {
|
|
180
204
|
let text = `${provider.name} ${provider.id}`;
|
|
181
|
-
|
|
182
|
-
|
|
205
|
+
const origin = this.#authStorage.getCredentialOrigin(provider.id);
|
|
206
|
+
if (origin) {
|
|
207
|
+
text += ` logged in authenticated ${ORIGIN_LABELS[origin.kind]}`;
|
|
208
|
+
if (origin.envVar) text += ` ${origin.envVar}`;
|
|
183
209
|
}
|
|
184
210
|
if (!provider.available) {
|
|
185
211
|
text += " unavailable";
|
|
@@ -40,6 +40,11 @@ export interface StatusLineSettings {
|
|
|
40
40
|
sessionAccent?: boolean;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export type EffectiveStatusLineSettings = Required<
|
|
44
|
+
Pick<StatusLineSettings, "leftSegments" | "rightSegments" | "separator" | "segmentOptions">
|
|
45
|
+
> &
|
|
46
|
+
StatusLineSettings;
|
|
47
|
+
|
|
43
48
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
44
49
|
// Per-message token cache
|
|
45
50
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -143,6 +148,7 @@ function tokensForMessage(msg: AgentMessage): number {
|
|
|
143
148
|
|
|
144
149
|
export class StatusLineComponent implements Component {
|
|
145
150
|
#settings: StatusLineSettings = {};
|
|
151
|
+
#effectiveSettings: EffectiveStatusLineSettings | undefined;
|
|
146
152
|
#cachedBranch: string | null | undefined = undefined;
|
|
147
153
|
#cachedBranchRepoId: string | null | undefined = undefined;
|
|
148
154
|
#gitWatcher: fs.FSWatcher | null = null;
|
|
@@ -204,6 +210,11 @@ export class StatusLineComponent implements Component {
|
|
|
204
210
|
|
|
205
211
|
updateSettings(settings: StatusLineSettings): void {
|
|
206
212
|
this.#settings = settings;
|
|
213
|
+
this.#effectiveSettings = undefined;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getEffectiveSettingsForTest(): EffectiveStatusLineSettings {
|
|
217
|
+
return this.#resolveSettings();
|
|
207
218
|
}
|
|
208
219
|
|
|
209
220
|
setAutoCompactEnabled(enabled: boolean): void {
|
|
@@ -594,10 +605,14 @@ export class StatusLineComponent implements Component {
|
|
|
594
605
|
};
|
|
595
606
|
}
|
|
596
607
|
|
|
597
|
-
#resolveSettings():
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
608
|
+
#resolveSettings(): EffectiveStatusLineSettings {
|
|
609
|
+
if (this.#effectiveSettings === undefined) {
|
|
610
|
+
this.#effectiveSettings = this.#computeEffectiveSettings();
|
|
611
|
+
}
|
|
612
|
+
return this.#effectiveSettings;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
#computeEffectiveSettings(): EffectiveStatusLineSettings {
|
|
601
616
|
const preset = this.#settings.preset ?? "default";
|
|
602
617
|
const presetDef = getPreset(preset);
|
|
603
618
|
const useCustomSegments = preset === "custom";
|
|
@@ -9,7 +9,7 @@ Spaghetti code? Try complaining with /omfg
|
|
|
9
9
|
Did you know? Each kitty/tmux/cmux split keeps its own session — `omp -c` resumes the right one
|
|
10
10
|
Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
|
|
11
11
|
Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
|
|
12
|
-
Say `
|
|
12
|
+
Say `workflowz` in your message to drive the task with parallel subagents in eval — watch it glow as you type
|
|
13
13
|
Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically
|
|
14
14
|
Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
|
|
15
15
|
Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc
|
|
@@ -15,7 +15,7 @@ export class UserMessageComponent extends Container {
|
|
|
15
15
|
constructor(text: string, synthetic = false, imageLinks?: readonly (string | undefined)[]) {
|
|
16
16
|
super();
|
|
17
17
|
const bgColor = (value: string) => theme.bg("userMessageBg", value);
|
|
18
|
-
// Paint the magic keywords ("ultrathink"/"orchestrate"/"
|
|
18
|
+
// Paint the magic keywords ("ultrathink"/"orchestrate"/"workflowz") inside the rendered
|
|
19
19
|
// bubble too — matching the live editor glow. The Markdown component routes code spans and
|
|
20
20
|
// fenced blocks through its own code styling (never `color`), so those are already excluded;
|
|
21
21
|
// `highlightMagicKeywords` additionally restores the bubble's own foreground after each
|