@gajae-code/coding-agent 0.1.1 → 0.1.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 +4 -0
- package/dist/types/config/model-registry.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +26 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +44 -0
- package/dist/types/goals/tools/goal-tool.d.ts +4 -4
- package/dist/types/hooks/skill-state.d.ts +3 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -4
- package/dist/types/skill-state/deep-interview-mutation-guard.d.ts +28 -0
- package/package.json +11 -7
- package/src/config/model-registry.ts +41 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +30 -30
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +51 -21
- package/src/gjc-runtime/ultragoal-guard.ts +239 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +318 -4
- package/src/goals/tools/goal-tool.ts +10 -4
- package/src/hooks/native-skill-hook.ts +26 -0
- package/src/hooks/skill-state.ts +59 -0
- package/src/main.ts +1 -17
- package/src/modes/components/model-selector.ts +120 -28
- package/src/modes/controllers/selector-controller.ts +16 -3
- package/src/modes/prompt-action-autocomplete.ts +40 -15
- package/src/session/agent-session.ts +31 -1
- package/src/setup/model-onboarding-guidance.ts +5 -3
- package/src/skill-state/deep-interview-mutation-guard.ts +303 -0
- package/src/slash-commands/builtin-registry.ts +130 -11
- package/src/tools/ask.ts +55 -17
- package/src/tools/ast-edit.ts +7 -0
- package/src/tools/bash.ts +2 -1
- package/src/tools/gh.ts +37 -9
- package/src/tools/path-utils.ts +1 -0
|
@@ -12,9 +12,9 @@ import {
|
|
|
12
12
|
Text,
|
|
13
13
|
type TUI,
|
|
14
14
|
} from "@gajae-code/tui";
|
|
15
|
-
import type { ModelRegistry } from "../../config/model-registry";
|
|
16
|
-
import {
|
|
17
|
-
import { resolveModelRoleValue } from "../../config/model-resolver";
|
|
15
|
+
import type { GjcModelAssignmentTargetId, ModelRegistry } from "../../config/model-registry";
|
|
16
|
+
import { GJC_MODEL_ASSIGNMENT_TARGET_IDS, GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
|
|
17
|
+
import { formatModelSelectorValue, resolveModelRoleValue } from "../../config/model-resolver";
|
|
18
18
|
import type { Settings } from "../../config/settings";
|
|
19
19
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
20
20
|
import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
|
|
@@ -53,6 +53,7 @@ interface ModelItem {
|
|
|
53
53
|
id: string;
|
|
54
54
|
model: Model;
|
|
55
55
|
selector: string;
|
|
56
|
+
thinkingLevel?: ThinkingLevel;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
interface CanonicalModelItem {
|
|
@@ -64,11 +65,12 @@ interface CanonicalModelItem {
|
|
|
64
65
|
searchText: string;
|
|
65
66
|
normalizedSearchText: string;
|
|
66
67
|
compactSearchText: string;
|
|
68
|
+
thinkingLevel?: ThinkingLevel;
|
|
67
69
|
}
|
|
68
70
|
|
|
69
71
|
interface ScopedModelItem {
|
|
70
72
|
model: Model;
|
|
71
|
-
thinkingLevel?:
|
|
73
|
+
thinkingLevel?: ThinkingLevel;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
interface RoleAssignment {
|
|
@@ -78,7 +80,7 @@ interface RoleAssignment {
|
|
|
78
80
|
|
|
79
81
|
type RoleSelectCallback = (
|
|
80
82
|
model: Model,
|
|
81
|
-
role:
|
|
83
|
+
role: GjcModelAssignmentTargetId | null,
|
|
82
84
|
thinkingLevel?: ThinkingLevel,
|
|
83
85
|
selector?: string,
|
|
84
86
|
) => void;
|
|
@@ -108,7 +110,7 @@ function createProviderTab(providerId: string): ProviderTabState {
|
|
|
108
110
|
* Component that renders a canonical model selector with provider tabs.
|
|
109
111
|
* - Tab/Arrow Left/Right: Switch between provider tabs
|
|
110
112
|
* - Arrow Up/Down: Navigate model list
|
|
111
|
-
* - Enter:
|
|
113
|
+
* - Enter: Open assignment actions for default plus GJC role-agent models
|
|
112
114
|
* - Escape: Close selector
|
|
113
115
|
*/
|
|
114
116
|
export class ModelSelectorComponent extends Container {
|
|
@@ -130,6 +132,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
130
132
|
#tui: TUI;
|
|
131
133
|
#scopedModels: ReadonlyArray<ScopedModelItem>;
|
|
132
134
|
#temporaryOnly: boolean;
|
|
135
|
+
#pendingActionItem?: ModelItem | CanonicalModelItem;
|
|
136
|
+
#selectedActionIndex: number = 0;
|
|
133
137
|
|
|
134
138
|
// Tab state
|
|
135
139
|
#providers: ProviderTabState[] = STATIC_PROVIDER_TABS;
|
|
@@ -141,7 +145,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
141
145
|
settings: Settings,
|
|
142
146
|
modelRegistry: ModelRegistry,
|
|
143
147
|
scopedModels: ReadonlyArray<ScopedModelItem>,
|
|
144
|
-
onSelect: (
|
|
148
|
+
onSelect: (
|
|
149
|
+
model: Model,
|
|
150
|
+
role: GjcModelAssignmentTargetId | null,
|
|
151
|
+
thinkingLevel?: ThinkingLevel,
|
|
152
|
+
selector?: string,
|
|
153
|
+
) => void,
|
|
145
154
|
onCancel: () => void,
|
|
146
155
|
options?: { temporaryOnly?: boolean; initialSearchInput?: string },
|
|
147
156
|
) {
|
|
@@ -183,7 +192,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
183
192
|
this.#searchInput.onSubmit = () => {
|
|
184
193
|
const selectedItem = this.#getSelectedItem();
|
|
185
194
|
if (selectedItem) {
|
|
186
|
-
this.#
|
|
195
|
+
this.#beginActionMenuOrSelect(selectedItem);
|
|
187
196
|
}
|
|
188
197
|
};
|
|
189
198
|
this.addChild(this.#searchInput);
|
|
@@ -219,8 +228,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
219
228
|
#loadRoleModels(): void {
|
|
220
229
|
const allModels = this.#modelRegistry.getAll();
|
|
221
230
|
const matchPreferences = { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() };
|
|
222
|
-
|
|
223
|
-
|
|
231
|
+
const agentModelOverrides = this.#settings.get("task.agentModelOverrides");
|
|
232
|
+
for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
|
|
233
|
+
const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
234
|
+
const roleValue =
|
|
235
|
+
target.settingsPath === "modelRoles" ? this.#settings.getModelRole(role) : agentModelOverrides[role];
|
|
224
236
|
if (!roleValue) continue;
|
|
225
237
|
|
|
226
238
|
const resolved = resolveModelRoleValue(roleValue, allModels, {
|
|
@@ -336,6 +348,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
336
348
|
id: scoped.model.id,
|
|
337
349
|
model: scoped.model,
|
|
338
350
|
selector: `${scoped.model.provider}/${scoped.model.id}`,
|
|
351
|
+
thinkingLevel: scoped.thinkingLevel,
|
|
339
352
|
}));
|
|
340
353
|
} else {
|
|
341
354
|
// Reload config and cached discovery state without blocking on live provider refresh
|
|
@@ -369,17 +382,20 @@ export class ModelSelectorComponent extends Container {
|
|
|
369
382
|
}
|
|
370
383
|
}
|
|
371
384
|
|
|
385
|
+
const candidateModels = models.map(item => item.model);
|
|
372
386
|
const canonicalRecords = this.#modelRegistry.getCanonicalModels({
|
|
373
387
|
availableOnly: this.#scopedModels.length === 0,
|
|
374
|
-
candidates:
|
|
388
|
+
candidates: candidateModels,
|
|
375
389
|
});
|
|
390
|
+
const scopedThinkingBySelector = new Map(models.map(item => [item.selector, item.thinkingLevel]));
|
|
376
391
|
const canonicalModels = canonicalRecords
|
|
377
|
-
.map(record => {
|
|
392
|
+
.map((record): CanonicalModelItem | undefined => {
|
|
378
393
|
const selectedModel = this.#modelRegistry.resolveCanonicalModel(record.id, {
|
|
379
394
|
availableOnly: this.#scopedModels.length === 0,
|
|
380
|
-
candidates:
|
|
395
|
+
candidates: candidateModels,
|
|
381
396
|
});
|
|
382
397
|
if (!selectedModel) return undefined;
|
|
398
|
+
const selectedSelector = `${selectedModel.provider}/${selectedModel.id}`;
|
|
383
399
|
const searchText = [
|
|
384
400
|
record.id,
|
|
385
401
|
record.name,
|
|
@@ -388,8 +404,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
388
404
|
selectedModel.name,
|
|
389
405
|
...record.variants.flatMap(variant => [variant.selector, variant.model.name]),
|
|
390
406
|
].join(" ");
|
|
391
|
-
|
|
392
|
-
kind: "canonical"
|
|
407
|
+
const item: CanonicalModelItem = {
|
|
408
|
+
kind: "canonical",
|
|
393
409
|
id: record.id,
|
|
394
410
|
model: selectedModel,
|
|
395
411
|
selector: record.id,
|
|
@@ -398,6 +414,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
398
414
|
normalizedSearchText: normalizeSearchText(searchText),
|
|
399
415
|
compactSearchText: compactSearchText(searchText),
|
|
400
416
|
};
|
|
417
|
+
const scopedThinkingLevel = scopedThinkingBySelector.get(selectedSelector);
|
|
418
|
+
if (scopedThinkingLevel !== undefined) {
|
|
419
|
+
item.thinkingLevel = scopedThinkingLevel;
|
|
420
|
+
}
|
|
421
|
+
return item;
|
|
401
422
|
})
|
|
402
423
|
.filter((item): item is CanonicalModelItem => item !== undefined);
|
|
403
424
|
|
|
@@ -627,12 +648,14 @@ export class ModelSelectorComponent extends Container {
|
|
|
627
648
|
|
|
628
649
|
// Build role badges (inverted: color as background, black text)
|
|
629
650
|
const roleBadgeTokens: string[] = [];
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
651
|
+
for (const role of GJC_MODEL_ASSIGNMENT_TARGET_IDS) {
|
|
652
|
+
const roleInfo = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
653
|
+
const assigned = this.#roles[role];
|
|
654
|
+
if (roleInfo.tag && assigned && modelsAreEqual(assigned.model, item.model)) {
|
|
655
|
+
const badge = makeInvertedBadge(roleInfo.tag, roleInfo.color ?? "muted");
|
|
656
|
+
const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
|
|
657
|
+
roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
|
|
658
|
+
}
|
|
636
659
|
}
|
|
637
660
|
const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
|
|
638
661
|
|
|
@@ -699,8 +722,26 @@ export class ModelSelectorComponent extends Container {
|
|
|
699
722
|
this.#listContainer.addChild(
|
|
700
723
|
new Text(theme.fg("muted", ` Model Name: ${selected.model.name}${suffix}`), 0, 0),
|
|
701
724
|
);
|
|
725
|
+
if (this.#pendingActionItem) {
|
|
726
|
+
this.#renderActionMenu(this.#pendingActionItem);
|
|
727
|
+
}
|
|
702
728
|
}
|
|
703
729
|
}
|
|
730
|
+
#renderActionMenu(item: ModelItem | CanonicalModelItem): void {
|
|
731
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
732
|
+
this.#listContainer.addChild(new Text(theme.fg("muted", ` Action for: ${item.model.id}`), 0, 0));
|
|
733
|
+
this.#listContainer.addChild(new Spacer(1));
|
|
734
|
+
for (let i = 0; i < GJC_MODEL_ASSIGNMENT_TARGET_IDS.length; i++) {
|
|
735
|
+
const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[i];
|
|
736
|
+
const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
737
|
+
const prefix = i === this.#selectedActionIndex ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
|
|
738
|
+
const label = `Set as ${target.tag ?? role.toUpperCase()} (${target.name})`;
|
|
739
|
+
this.#listContainer.addChild(
|
|
740
|
+
new Text(`${prefix}${i === this.#selectedActionIndex ? theme.fg("accent", label) : label}`, 0, 0),
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
704
745
|
#getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
|
|
705
746
|
return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
|
|
706
747
|
}
|
|
@@ -712,6 +753,11 @@ export class ModelSelectorComponent extends Container {
|
|
|
712
753
|
}
|
|
713
754
|
|
|
714
755
|
handleInput(keyData: string): void {
|
|
756
|
+
if (this.#pendingActionItem) {
|
|
757
|
+
this.#handleActionMenuInput(keyData);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
|
|
715
761
|
// Tab bar navigation
|
|
716
762
|
if (this.#tabBar?.handleInput(keyData)) {
|
|
717
763
|
return;
|
|
@@ -735,12 +781,12 @@ export class ModelSelectorComponent extends Container {
|
|
|
735
781
|
return;
|
|
736
782
|
}
|
|
737
783
|
|
|
738
|
-
// Enter
|
|
739
|
-
//
|
|
784
|
+
// Enter opens the persistent assignment menu. Temporary-only mode keeps the
|
|
785
|
+
// existing non-persistent quick-switch behavior.
|
|
740
786
|
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
741
787
|
const selectedItem = this.#getSelectedItem();
|
|
742
788
|
if (selectedItem) {
|
|
743
|
-
this.#
|
|
789
|
+
this.#beginActionMenuOrSelect(selectedItem);
|
|
744
790
|
}
|
|
745
791
|
return;
|
|
746
792
|
}
|
|
@@ -755,20 +801,66 @@ export class ModelSelectorComponent extends Container {
|
|
|
755
801
|
this.#searchInput.handleInput(keyData);
|
|
756
802
|
this.#filterModels(this.#searchInput.getValue());
|
|
757
803
|
}
|
|
758
|
-
#
|
|
804
|
+
#beginActionMenuOrSelect(item: ModelItem | CanonicalModelItem): void {
|
|
805
|
+
if (this.#temporaryOnly) {
|
|
806
|
+
this.#handleSelect(item, null);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
this.#pendingActionItem = item;
|
|
810
|
+
this.#selectedActionIndex = 0;
|
|
811
|
+
this.#updateList();
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
#handleActionMenuInput(keyData: string): void {
|
|
815
|
+
if (matchesKey(keyData, "up")) {
|
|
816
|
+
this.#selectedActionIndex =
|
|
817
|
+
this.#selectedActionIndex === 0
|
|
818
|
+
? GJC_MODEL_ASSIGNMENT_TARGET_IDS.length - 1
|
|
819
|
+
: this.#selectedActionIndex - 1;
|
|
820
|
+
this.#updateList();
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
if (matchesKey(keyData, "down")) {
|
|
824
|
+
this.#selectedActionIndex = (this.#selectedActionIndex + 1) % GJC_MODEL_ASSIGNMENT_TARGET_IDS.length;
|
|
825
|
+
this.#updateList();
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
|
|
829
|
+
const item = this.#pendingActionItem;
|
|
830
|
+
if (!item) return;
|
|
831
|
+
const role = GJC_MODEL_ASSIGNMENT_TARGET_IDS[this.#selectedActionIndex];
|
|
832
|
+
this.#pendingActionItem = undefined;
|
|
833
|
+
this.#handleSelect(item, role);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (getKeybindings().matches(keyData, "tui.select.cancel")) {
|
|
837
|
+
this.#pendingActionItem = undefined;
|
|
838
|
+
this.#updateList();
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
#handleSelect(
|
|
843
|
+
item: ModelItem | CanonicalModelItem,
|
|
844
|
+
role: GjcModelAssignmentTargetId | null,
|
|
845
|
+
thinkingLevel?: ThinkingLevel,
|
|
846
|
+
): void {
|
|
847
|
+
const itemThinkingLevel = thinkingLevel ?? item.thinkingLevel;
|
|
848
|
+
|
|
759
849
|
// For temporary role, don't save to settings - just notify caller
|
|
760
850
|
if (role === null) {
|
|
761
|
-
this.#onSelectCallback(item.model, null,
|
|
851
|
+
this.#onSelectCallback(item.model, null, itemThinkingLevel, item.selector);
|
|
762
852
|
return;
|
|
763
853
|
}
|
|
764
854
|
|
|
765
|
-
const selectedThinkingLevel =
|
|
855
|
+
const selectedThinkingLevel = itemThinkingLevel ?? this.#getCurrentRoleThinkingLevel(role);
|
|
856
|
+
const selectorValue =
|
|
857
|
+
role === "default" ? item.selector : formatModelSelectorValue(item.selector, selectedThinkingLevel);
|
|
766
858
|
|
|
767
859
|
// Update local state for UI
|
|
768
860
|
this.#roles[role] = { model: item.model, thinkingLevel: selectedThinkingLevel };
|
|
769
861
|
|
|
770
862
|
// Notify caller (for updating agent state if needed)
|
|
771
|
-
this.#onSelectCallback(item.model, role, selectedThinkingLevel,
|
|
863
|
+
this.#onSelectCallback(item.model, role, selectedThinkingLevel, selectorValue);
|
|
772
864
|
|
|
773
865
|
// Update list to show new badges
|
|
774
866
|
this.#updateList();
|
|
@@ -435,14 +435,14 @@ export class SelectorController {
|
|
|
435
435
|
try {
|
|
436
436
|
if (role === null) {
|
|
437
437
|
// Temporary: update agent state but don't persist to settings
|
|
438
|
-
await this.ctx.session.setModelTemporary(model);
|
|
438
|
+
await this.ctx.session.setModelTemporary(model, thinkingLevel);
|
|
439
439
|
this.ctx.statusLine.invalidate();
|
|
440
440
|
this.ctx.updateEditorBorderColor();
|
|
441
441
|
this.ctx.showStatus(`Temporary model: ${selector ?? model.id}`);
|
|
442
442
|
done();
|
|
443
443
|
this.ctx.ui.requestRender();
|
|
444
|
-
} else {
|
|
445
|
-
// Default: update agent state and persist
|
|
444
|
+
} else if (role === "default") {
|
|
445
|
+
// Default: update agent state and persist as the active default model.
|
|
446
446
|
await this.ctx.session.setModel(model, role, {
|
|
447
447
|
selector,
|
|
448
448
|
thinkingLevel,
|
|
@@ -455,6 +455,19 @@ export class SelectorController {
|
|
|
455
455
|
this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
|
|
456
456
|
done();
|
|
457
457
|
this.ctx.ui.requestRender();
|
|
458
|
+
} else {
|
|
459
|
+
// Role-agent assignments configure Task dispatch and must not switch the active chat model.
|
|
460
|
+
const apiKey = await this.ctx.session.modelRegistry.getApiKey(model, this.ctx.session.sessionId);
|
|
461
|
+
if (!apiKey) {
|
|
462
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
463
|
+
}
|
|
464
|
+
const overrides = this.ctx.settings.get("task.agentModelOverrides");
|
|
465
|
+
const value = selector ?? `${model.provider}/${model.id}`;
|
|
466
|
+
this.ctx.settings.set("task.agentModelOverrides", { ...overrides, [role]: value });
|
|
467
|
+
this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
468
|
+
this.ctx.showStatus(`${role} agent model: ${value}`);
|
|
469
|
+
done();
|
|
470
|
+
this.ctx.ui.requestRender();
|
|
458
471
|
}
|
|
459
472
|
} catch (error) {
|
|
460
473
|
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
@@ -84,6 +84,34 @@ function isSkillCommandAutocompleteItem(item: AutocompleteItem): item is SkillCo
|
|
|
84
84
|
return "normalizedSkillCommand" in item && item.normalizedSkillCommand === true;
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
function mergeAutocompleteSuggestions(
|
|
88
|
+
primary: { items: AutocompleteItem[]; prefix: string } | null,
|
|
89
|
+
secondary: { items: AutocompleteItem[]; prefix: string } | null,
|
|
90
|
+
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
91
|
+
if (!primary) return secondary;
|
|
92
|
+
if (!secondary) return primary;
|
|
93
|
+
if (primary.prefix !== secondary.prefix) return primary;
|
|
94
|
+
|
|
95
|
+
const seen = new Set<string>();
|
|
96
|
+
const items: AutocompleteItem[] = [];
|
|
97
|
+
for (const item of [...primary.items, ...secondary.items]) {
|
|
98
|
+
const key = `${item.value}\0${item.label}`;
|
|
99
|
+
if (seen.has(key)) continue;
|
|
100
|
+
seen.add(key);
|
|
101
|
+
items.push(item);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { items, prefix: primary.prefix };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function withoutSkillCommandSuggestions(
|
|
108
|
+
suggestions: { items: AutocompleteItem[]; prefix: string } | null,
|
|
109
|
+
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
110
|
+
if (!suggestions) return null;
|
|
111
|
+
const items = suggestions.items.filter(item => !item.value.startsWith("skill:"));
|
|
112
|
+
return items.length > 0 ? { ...suggestions, items } : null;
|
|
113
|
+
}
|
|
114
|
+
|
|
87
115
|
function getPromptActionPrefix(textBeforeCursor: string): string | null {
|
|
88
116
|
const hashIndex = textBeforeCursor.lastIndexOf("#");
|
|
89
117
|
if (hashIndex === -1) return null;
|
|
@@ -148,8 +176,14 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
148
176
|
}
|
|
149
177
|
}
|
|
150
178
|
|
|
151
|
-
const
|
|
152
|
-
if (
|
|
179
|
+
const slashPrefix = getSlashTokenPrefix(textBeforeCursor);
|
|
180
|
+
if (slashPrefix) {
|
|
181
|
+
const baseSuggestions = withoutSkillCommandSuggestions(
|
|
182
|
+
await this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol),
|
|
183
|
+
);
|
|
184
|
+
const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
|
|
185
|
+
return mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions);
|
|
186
|
+
}
|
|
153
187
|
|
|
154
188
|
if (!isSettingsInitialized() || settings.get("emojiAutocomplete")) {
|
|
155
189
|
const emojiSuggestions = getEmojiSuggestions(textBeforeCursor);
|
|
@@ -215,9 +249,11 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
215
249
|
return this.#baseProvider.getInlineHint?.(lines, cursorLine, cursorCol) ?? null;
|
|
216
250
|
}
|
|
217
251
|
trySyncSlashCompletion(textBeforeCursor: string): { items: AutocompleteItem[]; prefix: string } | null {
|
|
252
|
+
const baseSuggestions = withoutSkillCommandSuggestions(
|
|
253
|
+
this.#baseProvider.trySyncSlashCompletion?.(textBeforeCursor) ?? null,
|
|
254
|
+
);
|
|
218
255
|
const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor);
|
|
219
|
-
|
|
220
|
-
return this.#baseProvider.trySyncSlashCompletion?.(textBeforeCursor) ?? null;
|
|
256
|
+
return mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions);
|
|
221
257
|
}
|
|
222
258
|
trySyncInlineReplace(textBeforeCursor: string): { replaceLen: number; insert: string } | null {
|
|
223
259
|
if (isSettingsInitialized() && !settings.get("emojiAutocomplete")) return null;
|
|
@@ -233,17 +269,6 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
233
269
|
const exactNonSkillCommand = this.#commands.some(
|
|
234
270
|
command => command.name === query && !command.name.startsWith("skill:"),
|
|
235
271
|
);
|
|
236
|
-
if (exactNonSkillCommand) {
|
|
237
|
-
const command = this.#commands.find(
|
|
238
|
-
candidate => candidate.name === query && !candidate.name.startsWith("skill:"),
|
|
239
|
-
);
|
|
240
|
-
if (command) {
|
|
241
|
-
return {
|
|
242
|
-
items: [{ value: command.name, label: command.name, description: command.description }],
|
|
243
|
-
prefix,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
272
|
const items = this.#commands
|
|
248
273
|
.filter(command => command.name.startsWith("skill:"))
|
|
249
274
|
.map(command => {
|
|
@@ -167,6 +167,7 @@ import {
|
|
|
167
167
|
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
|
168
168
|
import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
|
|
169
169
|
import { isCanonicalGjcWorkflowSkill, syncSkillActiveState } from "../skill-state/active-state";
|
|
170
|
+
import { assertDeepInterviewMutationAllowed } from "../skill-state/deep-interview-mutation-guard";
|
|
170
171
|
import { invalidateHostMetadata } from "../ssh/connection-manager";
|
|
171
172
|
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
|
172
173
|
import {
|
|
@@ -3273,6 +3274,35 @@ export class AgentSession {
|
|
|
3273
3274
|
}) as T;
|
|
3274
3275
|
}
|
|
3275
3276
|
|
|
3277
|
+
/**
|
|
3278
|
+
* Wrap a tool with the deep-interview mutation guard. This guard is intentionally
|
|
3279
|
+
* outermost so active interviews reject product-code mutation before ACP permission
|
|
3280
|
+
* prompts or tool execution can run.
|
|
3281
|
+
*/
|
|
3282
|
+
#wrapToolForDeepInterviewMutationGuard<T extends AgentTool>(tool: T): T {
|
|
3283
|
+
if (!["edit", "write", "ast_edit"].includes(tool.name)) return tool;
|
|
3284
|
+
return new Proxy(tool, {
|
|
3285
|
+
get: (target, prop) => {
|
|
3286
|
+
if (prop !== "execute") return Reflect.get(target, prop, target);
|
|
3287
|
+
return async (
|
|
3288
|
+
toolCallId: string,
|
|
3289
|
+
args: unknown,
|
|
3290
|
+
signal: AbortSignal | undefined,
|
|
3291
|
+
onUpdate: never,
|
|
3292
|
+
ctx: never,
|
|
3293
|
+
) => {
|
|
3294
|
+
await assertDeepInterviewMutationAllowed({
|
|
3295
|
+
cwd: this.sessionManager.getCwd(),
|
|
3296
|
+
sessionId: this.sessionManager.getSessionId(),
|
|
3297
|
+
tool: target,
|
|
3298
|
+
args,
|
|
3299
|
+
});
|
|
3300
|
+
return await target.execute(toolCallId, args as never, signal, onUpdate, ctx);
|
|
3301
|
+
};
|
|
3302
|
+
},
|
|
3303
|
+
}) as T;
|
|
3304
|
+
}
|
|
3305
|
+
|
|
3276
3306
|
async #applyActiveToolsByName(
|
|
3277
3307
|
toolNames: string[],
|
|
3278
3308
|
options?: { persistMCPSelection?: boolean; previousSelectedMCPToolNames?: string[] },
|
|
@@ -3284,7 +3314,7 @@ export class AgentSession {
|
|
|
3284
3314
|
for (const name of toolNames) {
|
|
3285
3315
|
const tool = this.#toolRegistry.get(name);
|
|
3286
3316
|
if (tool) {
|
|
3287
|
-
tools.push(this.#wrapToolForAcpPermission(tool));
|
|
3317
|
+
tools.push(this.#wrapToolForDeepInterviewMutationGuard(this.#wrapToolForAcpPermission(tool)));
|
|
3288
3318
|
validToolNames.push(name);
|
|
3289
3319
|
}
|
|
3290
3320
|
}
|
|
@@ -7,14 +7,16 @@ export const MODEL_ONBOARDING_OAUTH_COMMAND = "/provider login [provider-id] or
|
|
|
7
7
|
export function formatModelOnboardingGuidance(): string {
|
|
8
8
|
return [
|
|
9
9
|
"Model selection only shows configured providers.",
|
|
10
|
+
"Assignment targets are DEFAULT plus the GJC role agents: EXECUTOR, ARCHITECT, PLANNER, and CRITIC.",
|
|
11
|
+
"Legacy model-role aliases are compatibility-only and are not shown as assignment targets.",
|
|
10
12
|
`API-compatible providers: ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
|
|
11
13
|
`OAuth/subscription providers: ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
|
|
12
|
-
"Then run /model to select a configured model.",
|
|
14
|
+
"Then run /model to select a configured model or assign it to a target.",
|
|
13
15
|
].join("\n");
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export function formatModelOnboardingInlineHint(): string {
|
|
17
|
-
return `Add API-compatible providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model.`;
|
|
19
|
+
return `Add API-compatible providers with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}); OAuth/subscription with ${MODEL_ONBOARDING_OAUTH_COMMAND}; then run /model for DEFAULT, EXECUTOR, ARCHITECT, PLANNER, and CRITIC.`;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
export function formatNoModelOnboardingError(): string {
|
|
@@ -27,7 +29,7 @@ export function formatNoCredentialOnboardingError(providerId: string): string {
|
|
|
27
29
|
"",
|
|
28
30
|
`For API-compatible providers, configure credentials with ${MODEL_ONBOARDING_API_PROVIDER_COMMAND} (or ${MODEL_ONBOARDING_SETUP_COMMAND}).`,
|
|
29
31
|
`For OAuth/subscription providers, use ${MODEL_ONBOARDING_OAUTH_COMMAND}.`,
|
|
30
|
-
"Then run /model to select a configured model.",
|
|
32
|
+
"Then run /model to select a configured model or assign it to DEFAULT, EXECUTOR, ARCHITECT, PLANNER, or CRITIC.",
|
|
31
33
|
].join("\n");
|
|
32
34
|
}
|
|
33
35
|
|