@oh-my-pi/pi-coding-agent 14.5.6 → 14.5.7
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 +5 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +1 -1
- package/src/config/settings.ts +16 -0
- package/src/edit/modes/atom.ts +3 -5
- package/src/modes/components/hook-editor.ts +2 -2
- package/src/modes/components/status-line/presets.ts +7 -7
- package/src/modes/components/status-line/segments.ts +16 -10
- package/src/modes/components/status-line/types.ts +3 -0
- package/src/modes/components/status-line-segment-editor.ts +1 -1
- package/src/modes/components/status-line.ts +6 -0
- package/src/modes/controllers/input-controller.ts +9 -0
- package/src/modes/interactive-mode.ts +58 -0
- package/src/modes/theme/defaults/dark-poimandres.json +1 -0
- package/src/modes/theme/defaults/light-poimandres.json +1 -0
- package/src/modes/theme/theme.ts +6 -0
- package/src/modes/types.ts +4 -0
- package/src/slash-commands/builtin-registry.ts +10 -0
package/CHANGELOG.md
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.5.
|
|
4
|
+
"version": "14.5.7",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.20.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.5.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.5.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.5.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.5.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.5.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.5.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.5.7",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.5.7",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.5.7",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.5.7",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.5.7",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.5.7",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@xterm/headless": "^6.0.0",
|
package/src/config/settings.ts
CHANGED
|
@@ -532,6 +532,22 @@ export class Settings {
|
|
|
532
532
|
delete isolationObj.enabled;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
// statusLine: rename "plan_mode" segment to "mode"
|
|
536
|
+
const statusLineObj = raw.statusLine as Record<string, unknown> | undefined;
|
|
537
|
+
if (statusLineObj) {
|
|
538
|
+
for (const key of ["leftSegments", "rightSegments"] as const) {
|
|
539
|
+
const segments = statusLineObj[key];
|
|
540
|
+
if (Array.isArray(segments)) {
|
|
541
|
+
statusLineObj[key] = segments.map(seg => (seg === "plan_mode" ? "mode" : seg));
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
const segmentOptions = statusLineObj.segmentOptions as Record<string, unknown> | undefined;
|
|
545
|
+
if (segmentOptions && "plan_mode" in segmentOptions && !("mode" in segmentOptions)) {
|
|
546
|
+
segmentOptions.mode = segmentOptions.plan_mode;
|
|
547
|
+
delete segmentOptions.plan_mode;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
535
551
|
return raw;
|
|
536
552
|
}
|
|
537
553
|
|
package/src/edit/modes/atom.ts
CHANGED
|
@@ -821,11 +821,9 @@ export function applyAtomEdits(text: string, edits: AtomEdit[]): AtomApplyResult
|
|
|
821
821
|
anchorMutated = true;
|
|
822
822
|
break;
|
|
823
823
|
case "delete":
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
);
|
|
828
|
-
}
|
|
824
|
+
// `-Lid|OLD` / `-Lid=OLD`: the OLD payload is informational only.
|
|
825
|
+
// The Lid hash already validates the line content (and auto-rebases
|
|
826
|
+
// when lines have shifted), so we ignore any OLD mismatch here.
|
|
829
827
|
replacement = [];
|
|
830
828
|
replacementSet = true;
|
|
831
829
|
anchorMutated = true;
|
|
@@ -104,8 +104,8 @@ export class HookEditorComponent extends Container {
|
|
|
104
104
|
|
|
105
105
|
/** Hook-style: Enter=newline, Ctrl+Enter=submit (original behavior) */
|
|
106
106
|
#handleHookStyleInput(keyData: string): void {
|
|
107
|
-
// Ctrl+Enter to submit
|
|
108
|
-
if (keyData
|
|
107
|
+
// Ctrl+Enter to submit. Use key matching so lock-key and keypad Enter variants work.
|
|
108
|
+
if (matchesKey(keyData, "ctrl+enter")) {
|
|
109
109
|
this.#onSubmitCallback(this.#editor.getText());
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
@@ -2,7 +2,7 @@ import type { PresetDef, StatusLinePreset } from "./types";
|
|
|
2
2
|
|
|
3
3
|
export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
4
4
|
default: {
|
|
5
|
-
leftSegments: ["pi", "model", "
|
|
5
|
+
leftSegments: ["pi", "model", "mode", "path", "git", "pr", "context_pct", "token_total", "cost"],
|
|
6
6
|
rightSegments: ["session_name"],
|
|
7
7
|
separator: "powerline-thin",
|
|
8
8
|
segmentOptions: {
|
|
@@ -14,7 +14,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
14
14
|
|
|
15
15
|
minimal: {
|
|
16
16
|
leftSegments: ["path", "git"],
|
|
17
|
-
rightSegments: ["session_name", "
|
|
17
|
+
rightSegments: ["session_name", "mode", "context_pct"],
|
|
18
18
|
separator: "slash",
|
|
19
19
|
segmentOptions: {
|
|
20
20
|
path: { abbreviate: true, maxLength: 30 },
|
|
@@ -23,7 +23,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
23
23
|
},
|
|
24
24
|
|
|
25
25
|
compact: {
|
|
26
|
-
leftSegments: ["model", "
|
|
26
|
+
leftSegments: ["model", "mode", "git", "pr"],
|
|
27
27
|
rightSegments: ["session_name", "cost", "context_pct"],
|
|
28
28
|
separator: "powerline-thin",
|
|
29
29
|
segmentOptions: {
|
|
@@ -33,7 +33,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
33
33
|
},
|
|
34
34
|
|
|
35
35
|
full: {
|
|
36
|
-
leftSegments: ["pi", "hostname", "model", "
|
|
36
|
+
leftSegments: ["pi", "hostname", "model", "mode", "path", "git", "pr", "subagents"],
|
|
37
37
|
rightSegments: [
|
|
38
38
|
"session_name",
|
|
39
39
|
"token_in",
|
|
@@ -56,7 +56,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
56
56
|
|
|
57
57
|
nerd: {
|
|
58
58
|
// Full preset with all Nerd Font icons
|
|
59
|
-
leftSegments: ["pi", "hostname", "model", "
|
|
59
|
+
leftSegments: ["pi", "hostname", "model", "mode", "path", "git", "pr", "session", "subagents"],
|
|
60
60
|
rightSegments: [
|
|
61
61
|
"session_name",
|
|
62
62
|
"token_in",
|
|
@@ -81,7 +81,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
81
81
|
|
|
82
82
|
ascii: {
|
|
83
83
|
// No Nerd Font dependencies
|
|
84
|
-
leftSegments: ["model", "
|
|
84
|
+
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
85
85
|
rightSegments: ["session_name", "token_total", "cost", "context_pct"],
|
|
86
86
|
separator: "ascii",
|
|
87
87
|
segmentOptions: {
|
|
@@ -93,7 +93,7 @@ export const STATUS_LINE_PRESETS: Record<StatusLinePreset, PresetDef> = {
|
|
|
93
93
|
|
|
94
94
|
custom: {
|
|
95
95
|
// User-defined - these are just defaults that get overridden
|
|
96
|
-
leftSegments: ["model", "
|
|
96
|
+
leftSegments: ["model", "mode", "path", "git", "pr"],
|
|
97
97
|
rightSegments: ["session_name", "token_total", "cost", "context_pct"],
|
|
98
98
|
separator: "powerline-thin",
|
|
99
99
|
segmentOptions: {},
|
|
@@ -76,18 +76,24 @@ const modelSegment: StatusLineSegment = {
|
|
|
76
76
|
},
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const
|
|
80
|
-
id: "
|
|
79
|
+
const modeSegment: StatusLineSegment = {
|
|
80
|
+
id: "mode",
|
|
81
81
|
render(ctx) {
|
|
82
|
-
const
|
|
83
|
-
if (
|
|
84
|
-
|
|
82
|
+
const plan = ctx.planMode;
|
|
83
|
+
if (plan && (plan.enabled || plan.paused)) {
|
|
84
|
+
const label = plan.paused ? "Plan ⏸" : "Plan";
|
|
85
|
+
const content = withIcon(theme.icon.plan, label);
|
|
86
|
+
const color = plan.paused ? "warning" : "accent";
|
|
87
|
+
return { content: theme.fg(color, content), visible: true };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const loop = ctx.loopMode;
|
|
91
|
+
if (loop?.enabled) {
|
|
92
|
+
const content = withIcon(theme.icon.loop, "Loop");
|
|
93
|
+
return { content: theme.fg("customMessageLabel", content), visible: true };
|
|
85
94
|
}
|
|
86
95
|
|
|
87
|
-
|
|
88
|
-
const content = withIcon(theme.icon.plan, label);
|
|
89
|
-
const color = status.paused ? "warning" : "accent";
|
|
90
|
-
return { content: theme.fg(color, content), visible: true };
|
|
96
|
+
return { content: "", visible: false };
|
|
91
97
|
},
|
|
92
98
|
};
|
|
93
99
|
|
|
@@ -375,7 +381,7 @@ const sessionNameSegment: StatusLineSegment = {
|
|
|
375
381
|
export const SEGMENTS: Record<StatusLineSegmentId, StatusLineSegment> = {
|
|
376
382
|
pi: piSegment,
|
|
377
383
|
model: modelSegment,
|
|
378
|
-
|
|
384
|
+
mode: modeSegment,
|
|
379
385
|
path: pathSegment,
|
|
380
386
|
git: gitSegment,
|
|
381
387
|
pr: prSegment,
|
|
@@ -18,7 +18,7 @@ import { ALL_SEGMENT_IDS } from "./status-line/segments";
|
|
|
18
18
|
const SEGMENT_INFO: Record<StatusLineSegmentId, { label: string; short: string }> = {
|
|
19
19
|
pi: { label: "Pi", short: "π icon" },
|
|
20
20
|
model: { label: "Model", short: "model name" },
|
|
21
|
-
|
|
21
|
+
mode: { label: "Mode", short: "plan/loop status" },
|
|
22
22
|
path: { label: "Path", short: "working dir" },
|
|
23
23
|
git: { label: "Git", short: "branch/status" },
|
|
24
24
|
pr: { label: "PR", short: "pull request" },
|
|
@@ -57,6 +57,7 @@ export class StatusLineComponent implements Component {
|
|
|
57
57
|
#subagentCount: number = 0;
|
|
58
58
|
#sessionStartTime: number = Date.now();
|
|
59
59
|
#planModeStatus: { enabled: boolean; paused: boolean } | null = null;
|
|
60
|
+
#loopModeStatus: { enabled: boolean } | null = null;
|
|
60
61
|
|
|
61
62
|
// Git status caching (1s TTL)
|
|
62
63
|
#cachedGitStatus: { staged: number; unstaged: number; untracked: number } | null = null;
|
|
@@ -102,6 +103,10 @@ export class StatusLineComponent implements Component {
|
|
|
102
103
|
this.#planModeStatus = status ?? null;
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
setLoopModeStatus(status: { enabled: boolean } | undefined): void {
|
|
107
|
+
this.#loopModeStatus = status ?? null;
|
|
108
|
+
}
|
|
109
|
+
|
|
105
110
|
setHookStatus(key: string, text: string | undefined): void {
|
|
106
111
|
if (text === undefined) {
|
|
107
112
|
this.#hookStatuses.delete(key);
|
|
@@ -326,6 +331,7 @@ export class StatusLineComponent implements Component {
|
|
|
326
331
|
width,
|
|
327
332
|
options: this.#resolveSettings().segmentOptions ?? {},
|
|
328
333
|
planMode: this.#planModeStatus,
|
|
334
|
+
loopMode: this.#loopModeStatus,
|
|
329
335
|
usageStats,
|
|
330
336
|
contextPercent,
|
|
331
337
|
contextWindow,
|
|
@@ -44,6 +44,15 @@ export class InputController {
|
|
|
44
44
|
this.ctx.retryEscapeHandler,
|
|
45
45
|
);
|
|
46
46
|
this.ctx.editor.onEscape = () => {
|
|
47
|
+
if (this.ctx.loopModeEnabled) {
|
|
48
|
+
this.ctx.disableLoopMode();
|
|
49
|
+
if (this.ctx.session.isStreaming) {
|
|
50
|
+
void this.ctx.session.abort();
|
|
51
|
+
} else {
|
|
52
|
+
this.ctx.cancelPendingSubmission();
|
|
53
|
+
}
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
47
56
|
if (this.ctx.hasActiveBtw() && this.ctx.handleBtwEscape()) {
|
|
48
57
|
return;
|
|
49
58
|
}
|
|
@@ -145,6 +145,9 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
145
145
|
planModeEnabled = false;
|
|
146
146
|
planModePaused = false;
|
|
147
147
|
planModePlanFilePath: string | undefined = undefined;
|
|
148
|
+
loopModeEnabled = false;
|
|
149
|
+
loopPrompt: string | undefined = undefined;
|
|
150
|
+
#loopAutoSubmitTimer: NodeJS.Timeout | undefined;
|
|
148
151
|
todoPhases: TodoPhase[] = [];
|
|
149
152
|
hideThinkingBlock = false;
|
|
150
153
|
pendingImages: ImageContent[] = [];
|
|
@@ -483,9 +486,64 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
483
486
|
this.onInputCallback = undefined;
|
|
484
487
|
resolve(input);
|
|
485
488
|
};
|
|
489
|
+
this.#scheduleLoopAutoSubmit();
|
|
486
490
|
return promise;
|
|
487
491
|
}
|
|
488
492
|
|
|
493
|
+
#scheduleLoopAutoSubmit(): void {
|
|
494
|
+
if (this.#loopAutoSubmitTimer) {
|
|
495
|
+
clearTimeout(this.#loopAutoSubmitTimer);
|
|
496
|
+
this.#loopAutoSubmitTimer = undefined;
|
|
497
|
+
}
|
|
498
|
+
if (!this.loopModeEnabled || !this.loopPrompt) return;
|
|
499
|
+
const prompt = this.loopPrompt;
|
|
500
|
+
// Brief delay so the user has a chance to press Esc between iterations.
|
|
501
|
+
this.#loopAutoSubmitTimer = setTimeout(() => {
|
|
502
|
+
this.#loopAutoSubmitTimer = undefined;
|
|
503
|
+
if (!this.loopModeEnabled || !this.onInputCallback) return;
|
|
504
|
+
this.onInputCallback(this.startPendingSubmission({ text: prompt }));
|
|
505
|
+
}, 800);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
disableLoopMode(options?: { silent?: boolean }): void {
|
|
509
|
+
const wasEnabled = this.loopModeEnabled;
|
|
510
|
+
this.loopModeEnabled = false;
|
|
511
|
+
this.loopPrompt = undefined;
|
|
512
|
+
if (this.#loopAutoSubmitTimer) {
|
|
513
|
+
clearTimeout(this.#loopAutoSubmitTimer);
|
|
514
|
+
this.#loopAutoSubmitTimer = undefined;
|
|
515
|
+
}
|
|
516
|
+
this.statusLine.setLoopModeStatus(undefined);
|
|
517
|
+
this.updateEditorTopBorder();
|
|
518
|
+
this.ui.requestRender();
|
|
519
|
+
if (wasEnabled && !options?.silent) {
|
|
520
|
+
this.showStatus("Loop mode disabled.");
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async handleLoopCommand(prompt?: string): Promise<void> {
|
|
525
|
+
if (this.loopModeEnabled) {
|
|
526
|
+
this.disableLoopMode();
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const trimmed = prompt?.trim();
|
|
530
|
+
if (!trimmed) {
|
|
531
|
+
this.showError("Usage: /loop <prompt>");
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
this.loopModeEnabled = true;
|
|
535
|
+
this.loopPrompt = trimmed;
|
|
536
|
+
this.statusLine.setLoopModeStatus({ enabled: true });
|
|
537
|
+
this.updateEditorTopBorder();
|
|
538
|
+
this.ui.requestRender();
|
|
539
|
+
this.showStatus("Loop mode enabled. Esc to stop.");
|
|
540
|
+
|
|
541
|
+
// Submit the first iteration immediately so the loop kicks off.
|
|
542
|
+
if (this.onInputCallback) {
|
|
543
|
+
this.onInputCallback(this.startPendingSubmission({ text: trimmed }));
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
489
547
|
startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput {
|
|
490
548
|
const submission: SubmittedUserInput = {
|
|
491
549
|
text: input.text,
|
package/src/modes/theme/theme.ts
CHANGED
|
@@ -91,6 +91,7 @@ export type SymbolKey =
|
|
|
91
91
|
// Icons
|
|
92
92
|
| "icon.model"
|
|
93
93
|
| "icon.plan"
|
|
94
|
+
| "icon.loop"
|
|
94
95
|
| "icon.folder"
|
|
95
96
|
| "icon.file"
|
|
96
97
|
| "icon.git"
|
|
@@ -250,6 +251,7 @@ const UNICODE_SYMBOLS: SymbolMap = {
|
|
|
250
251
|
// Icons
|
|
251
252
|
"icon.model": "⬢",
|
|
252
253
|
"icon.plan": "🗺",
|
|
254
|
+
"icon.loop": "↻",
|
|
253
255
|
"icon.folder": "📁",
|
|
254
256
|
"icon.file": "📄",
|
|
255
257
|
"icon.git": "⎇",
|
|
@@ -460,6 +462,8 @@ const NERD_SYMBOLS: SymbolMap = {
|
|
|
460
462
|
"icon.model": "\uec19",
|
|
461
463
|
// pick: | alt:
|
|
462
464
|
"icon.plan": "\uf2d2",
|
|
465
|
+
// pick: ↻ | alt: ⟳
|
|
466
|
+
"icon.loop": "\uf021",
|
|
463
467
|
// pick: | alt:
|
|
464
468
|
"icon.folder": "\uf115",
|
|
465
469
|
// pick: | alt:
|
|
@@ -659,6 +663,7 @@ const ASCII_SYMBOLS: SymbolMap = {
|
|
|
659
663
|
// Icons
|
|
660
664
|
"icon.model": "[M]",
|
|
661
665
|
"icon.plan": "plan",
|
|
666
|
+
"icon.loop": "loop",
|
|
662
667
|
"icon.folder": "[D]",
|
|
663
668
|
"icon.file": "[F]",
|
|
664
669
|
"icon.git": "git:",
|
|
@@ -1434,6 +1439,7 @@ export class Theme {
|
|
|
1434
1439
|
return {
|
|
1435
1440
|
model: this.#symbols["icon.model"],
|
|
1436
1441
|
plan: this.#symbols["icon.plan"],
|
|
1442
|
+
loop: this.#symbols["icon.loop"],
|
|
1437
1443
|
folder: this.#symbols["icon.folder"],
|
|
1438
1444
|
file: this.#symbols["icon.file"],
|
|
1439
1445
|
git: this.#symbols["icon.git"],
|
package/src/modes/types.ts
CHANGED
|
@@ -86,6 +86,8 @@ export interface InteractiveModeContext {
|
|
|
86
86
|
toolOutputExpanded: boolean;
|
|
87
87
|
todoExpanded: boolean;
|
|
88
88
|
planModeEnabled: boolean;
|
|
89
|
+
loopModeEnabled: boolean;
|
|
90
|
+
loopPrompt?: string;
|
|
89
91
|
planModePlanFilePath?: string;
|
|
90
92
|
hideThinkingBlock: boolean;
|
|
91
93
|
pendingImages: ImageContent[];
|
|
@@ -233,6 +235,8 @@ export interface InteractiveModeContext {
|
|
|
233
235
|
openExternalEditor(): void;
|
|
234
236
|
registerExtensionShortcuts(): void;
|
|
235
237
|
handlePlanModeCommand(initialPrompt?: string): Promise<void>;
|
|
238
|
+
handleLoopCommand(prompt?: string): Promise<void>;
|
|
239
|
+
disableLoopMode(options?: { silent?: boolean }): void;
|
|
236
240
|
handleExitPlanModeTool(details: ExitPlanModeDetails): Promise<void>;
|
|
237
241
|
|
|
238
242
|
// Hook UI methods
|
|
@@ -121,6 +121,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
121
121
|
runtime.ctx.editor.setText("");
|
|
122
122
|
},
|
|
123
123
|
},
|
|
124
|
+
{
|
|
125
|
+
name: "loop",
|
|
126
|
+
description: "Loop the agent: re-submit the same prompt every time it yields (Esc to stop)",
|
|
127
|
+
inlineHint: "<prompt>",
|
|
128
|
+
allowArgs: true,
|
|
129
|
+
handle: async (command, runtime) => {
|
|
130
|
+
await runtime.ctx.handleLoopCommand(command.args || undefined);
|
|
131
|
+
runtime.ctx.editor.setText("");
|
|
132
|
+
},
|
|
133
|
+
},
|
|
124
134
|
{
|
|
125
135
|
name: "model",
|
|
126
136
|
aliases: ["models"],
|