@gajae-code/coding-agent 0.8.1 → 0.8.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 +34 -0
- package/dist/types/cli/completion-cli.d.ts +41 -0
- package/dist/types/cli/config-cli.d.ts +1 -0
- package/dist/types/cli/notify-cli.d.ts +7 -0
- package/dist/types/cli.d.ts +124 -1
- package/dist/types/commands/completion.d.ts +29 -0
- package/dist/types/commands/config.d.ts +3 -0
- package/dist/types/commands/deep-interview.d.ts +3 -0
- package/dist/types/config/file-lock.d.ts +7 -6
- package/dist/types/config/keybindings.d.ts +2 -2
- package/dist/types/config/model-profile-activation.d.ts +6 -0
- package/dist/types/config/settings.d.ts +8 -0
- package/dist/types/extensibility/extensions/runner.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +8 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +39 -0
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
- package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/main.d.ts +4 -1
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +12 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +1 -3
- package/dist/types/modes/print-mode.d.ts +13 -1
- package/dist/types/modes/prompt-action-autocomplete.d.ts +1 -1
- package/dist/types/notifications/config.d.ts +8 -1
- package/dist/types/notifications/daemon-paths.d.ts +1 -0
- package/dist/types/notifications/index.d.ts +6 -3
- package/dist/types/notifications/lifecycle-commands.d.ts +11 -3
- package/dist/types/notifications/telegram-daemon.d.ts +15 -3
- package/dist/types/notifications/telegram-reference.d.ts +0 -5
- package/dist/types/runtime-api-key.d.ts +7 -0
- package/dist/types/session/agent-session.d.ts +10 -0
- package/dist/types/session/agent-storage.d.ts +1 -0
- package/dist/types/task/executor.d.ts +2 -0
- package/dist/types/tools/computer.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-diff.d.ts +8 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +50 -0
- package/dist/types/workflow/workflow-intent-report-schema.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-report.d.ts +48 -0
- package/package.json +7 -7
- package/src/cli/completion-cli.ts +234 -0
- package/src/cli/config-cli.ts +37 -9
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/notify-cli.ts +91 -7
- package/src/cli.ts +3 -1
- package/src/commands/completion.ts +84 -0
- package/src/commands/config.ts +2 -0
- package/src/commands/deep-interview.ts +2 -1
- package/src/config/file-lock.ts +73 -34
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-profile-activation.ts +65 -0
- package/src/config/model-registry.ts +14 -2
- package/src/config/settings.ts +22 -0
- package/src/coordinator-mcp/server.ts +34 -8
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +38 -5
- package/src/defaults/gjc/skills/ralplan/SKILL.md +15 -11
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/extensibility/extensions/types.ts +9 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +206 -0
- package/src/gjc-runtime/deep-interview-recorder.ts +62 -8
- package/src/gjc-runtime/deep-interview-runtime.ts +210 -2
- package/src/gjc-runtime/deep-interview-state.ts +13 -0
- package/src/gjc-runtime/session-state-sidecar.ts +59 -11
- package/src/gjc-runtime/state-runtime.ts +12 -2
- package/src/gjc-runtime/team-runtime.ts +23 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +73 -19
- package/src/hooks/native-skill-hook.ts +29 -0
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/main.ts +52 -41
- package/src/modes/components/model-selector.ts +54 -11
- package/src/modes/components/queued-message-selector.ts +92 -0
- package/src/modes/components/settings-selector.ts +4 -0
- package/src/modes/components/welcome.ts +160 -44
- package/src/modes/controllers/command-controller.ts +1 -2
- package/src/modes/controllers/extension-ui-controller.ts +4 -10
- package/src/modes/controllers/input-controller.ts +179 -17
- package/src/modes/controllers/selector-controller.ts +122 -27
- package/src/modes/interactive-mode.ts +43 -16
- package/src/modes/print-mode.ts +43 -4
- package/src/modes/prompt-action-autocomplete.ts +21 -20
- package/src/modes/utils/hotkeys-markdown.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +2 -2
- package/src/notifications/config.ts +9 -1
- package/src/notifications/daemon-paths.ts +2 -0
- package/src/notifications/index.ts +39 -9
- package/src/notifications/lifecycle-commands.ts +37 -7
- package/src/notifications/lifecycle-orchestrator.ts +19 -6
- package/src/notifications/telegram-daemon-control.ts +3 -1
- package/src/notifications/telegram-daemon.ts +217 -43
- package/src/notifications/telegram-reference.ts +0 -16
- package/src/prompts/system/system-prompt.md +3 -0
- package/src/prompts/tools/computer.md +2 -2
- package/src/runtime-api-key.ts +16 -0
- package/src/sdk.ts +20 -6
- package/src/session/agent-session.ts +225 -31
- package/src/session/agent-storage.ts +4 -0
- package/src/session/session-manager.ts +44 -2
- package/src/slash-commands/builtin-registry.ts +153 -71
- package/src/task/executor.ts +7 -4
- package/src/task/index.ts +9 -3
- package/src/task/output-manager.ts +16 -6
- package/src/task/receipt.ts +26 -7
- package/src/tools/computer.ts +246 -43
- package/src/tools/telegram-send.ts +6 -0
- package/src/workflow/workflow-intent-diff.ts +29 -6
- package/src/workflow/workflow-intent-report-consumer.ts +221 -0
- package/src/workflow/workflow-intent-report-schema.ts +129 -0
- package/src/workflow/workflow-intent-report.ts +135 -0
|
@@ -8,11 +8,12 @@ import {
|
|
|
8
8
|
activateModelProfile,
|
|
9
9
|
type MaterializeModelProfileForDeletionResult,
|
|
10
10
|
materializeActiveModelProfileAssignment,
|
|
11
|
+
materializeActiveModelProfileAssignments,
|
|
11
12
|
materializeModelProfileForDeletion,
|
|
12
13
|
restoreMaterializedModelProfileForDeletion,
|
|
13
14
|
} from "../../config/model-profile-activation";
|
|
14
15
|
import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
15
|
-
import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
|
|
16
|
+
import { GJC_MODEL_ASSIGNMENT_TARGETS, type GjcModelAssignmentTargetId } from "../../config/model-registry";
|
|
16
17
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
17
18
|
import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
18
19
|
import { type Settings, settings } from "../../config/settings";
|
|
@@ -438,6 +439,7 @@ export class SelectorController {
|
|
|
438
439
|
availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
|
|
439
440
|
thinkingLevel: this.ctx.session.thinkingLevel,
|
|
440
441
|
availableThemes,
|
|
442
|
+
availableModelProfiles: [...this.ctx.session.modelRegistry.getModelProfiles().keys()],
|
|
441
443
|
cwd: getProjectDir(),
|
|
442
444
|
},
|
|
443
445
|
{
|
|
@@ -634,6 +636,18 @@ export class SelectorController {
|
|
|
634
636
|
this.ctx.updateEditorBorderColor();
|
|
635
637
|
break;
|
|
636
638
|
|
|
639
|
+
case "modelProfile.default": {
|
|
640
|
+
// Applying the default profile live mirrors the /model preset flow so the
|
|
641
|
+
// running session switches immediately, not only on next startup.
|
|
642
|
+
const profileName = typeof value === "string" ? value : "";
|
|
643
|
+
if (!profileName) break;
|
|
644
|
+
this.#applyModelProfile(profileName, true)
|
|
645
|
+
.then(() => this.ctx.ui.requestRender())
|
|
646
|
+
.catch(error => {
|
|
647
|
+
this.ctx.showError(error instanceof Error ? error.message : String(error));
|
|
648
|
+
});
|
|
649
|
+
break;
|
|
650
|
+
}
|
|
637
651
|
case "clearOnShrink":
|
|
638
652
|
this.ctx.ui.setClearOnShrink(value as boolean);
|
|
639
653
|
break;
|
|
@@ -784,6 +798,29 @@ export class SelectorController {
|
|
|
784
798
|
}
|
|
785
799
|
}
|
|
786
800
|
|
|
801
|
+
/**
|
|
802
|
+
* Activate a model profile through the shared /model + /settings path: swap the
|
|
803
|
+
* live session model (and, when persistDefault, persist it as the startup
|
|
804
|
+
* default) then refresh the status surfaces. Rethrows so callers surface errors.
|
|
805
|
+
*/
|
|
806
|
+
async #applyModelProfile(profileName: string, persistDefault: boolean): Promise<void> {
|
|
807
|
+
const profileLabel = formatModelProfileDisplayLabel(
|
|
808
|
+
this.ctx.session.modelRegistry.getModelProfile(profileName) ?? { name: profileName },
|
|
809
|
+
);
|
|
810
|
+
await activateModelProfile(
|
|
811
|
+
{
|
|
812
|
+
session: this.ctx.session,
|
|
813
|
+
modelRegistry: this.ctx.session.modelRegistry,
|
|
814
|
+
settings: this.ctx.settings,
|
|
815
|
+
profileName,
|
|
816
|
+
},
|
|
817
|
+
{ persistDefault },
|
|
818
|
+
);
|
|
819
|
+
this.ctx.statusLine.invalidate();
|
|
820
|
+
this.ctx.updateEditorBorderColor();
|
|
821
|
+
this.ctx.showStatus(persistDefault ? `Default model profile: ${profileLabel}` : `Model profile: ${profileLabel}`);
|
|
822
|
+
}
|
|
823
|
+
|
|
787
824
|
showModelSelector(options?: { temporaryOnly?: boolean }): void {
|
|
788
825
|
this.showSelector(done => {
|
|
789
826
|
let modelSelector: ModelSelectorComponent;
|
|
@@ -809,27 +846,7 @@ export class SelectorController {
|
|
|
809
846
|
return;
|
|
810
847
|
}
|
|
811
848
|
if (selection.kind === "profile") {
|
|
812
|
-
|
|
813
|
-
this.ctx.session.modelRegistry.getModelProfile(selection.profileName) ?? {
|
|
814
|
-
name: selection.profileName,
|
|
815
|
-
},
|
|
816
|
-
);
|
|
817
|
-
await activateModelProfile(
|
|
818
|
-
{
|
|
819
|
-
session: this.ctx.session,
|
|
820
|
-
modelRegistry: this.ctx.session.modelRegistry,
|
|
821
|
-
settings: this.ctx.settings,
|
|
822
|
-
profileName: selection.profileName,
|
|
823
|
-
},
|
|
824
|
-
{ persistDefault: selection.setDefault },
|
|
825
|
-
);
|
|
826
|
-
this.ctx.statusLine.invalidate();
|
|
827
|
-
this.ctx.updateEditorBorderColor();
|
|
828
|
-
this.ctx.showStatus(
|
|
829
|
-
selection.setDefault
|
|
830
|
-
? `Default model profile: ${profileLabel}`
|
|
831
|
-
: `Model profile: ${profileLabel}`,
|
|
832
|
-
);
|
|
849
|
+
await this.#applyModelProfile(selection.profileName, selection.setDefault);
|
|
833
850
|
done();
|
|
834
851
|
this.ctx.ui.requestRender();
|
|
835
852
|
return;
|
|
@@ -843,6 +860,79 @@ export class SelectorController {
|
|
|
843
860
|
this.ctx.showStatus(`Temporary model: ${selectedSelector ?? model.id}`);
|
|
844
861
|
done();
|
|
845
862
|
this.ctx.ui.requestRender();
|
|
863
|
+
} else if (selection.roles) {
|
|
864
|
+
const targetRoles: readonly GjcModelAssignmentTargetId[] = selection.roles;
|
|
865
|
+
const includesDefault = targetRoles.includes("default");
|
|
866
|
+
const includesRoleAgent = targetRoles.some(targetRole => targetRole !== "default");
|
|
867
|
+
if (includesRoleAgent) {
|
|
868
|
+
const apiKey = await this.ctx.session.modelRegistry.getApiKey(
|
|
869
|
+
model,
|
|
870
|
+
this.ctx.session.sessionId,
|
|
871
|
+
);
|
|
872
|
+
if (!apiKey) {
|
|
873
|
+
throw new Error(`No API key for ${model.provider}/${model.id}`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
const value =
|
|
877
|
+
selectedSelector ?? formatModelSelectorValue(`${model.provider}/${model.id}`, thinkingLevel);
|
|
878
|
+
const assignments = new Map<GjcModelAssignmentTargetId, string>();
|
|
879
|
+
for (const targetRole of targetRoles) assignments.set(targetRole, value);
|
|
880
|
+
const defaultSelector =
|
|
881
|
+
selectedSelector && thinkingLevel && selectedSelector.endsWith(`:${thinkingLevel}`)
|
|
882
|
+
? selectedSelector.slice(0, -thinkingLevel.length - 1)
|
|
883
|
+
: selectedSelector;
|
|
884
|
+
|
|
885
|
+
if (includesDefault) {
|
|
886
|
+
await this.ctx.session.setModel(model, "default", {
|
|
887
|
+
selector: defaultSelector,
|
|
888
|
+
thinkingLevel,
|
|
889
|
+
});
|
|
890
|
+
if (thinkingLevel && thinkingLevel !== ThinkingLevel.Inherit) {
|
|
891
|
+
this.ctx.session.setThinkingLevel(thinkingLevel);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
const materializedProfile = materializeActiveModelProfileAssignments({
|
|
895
|
+
session: this.ctx.session,
|
|
896
|
+
settings: this.ctx.settings,
|
|
897
|
+
assignments,
|
|
898
|
+
});
|
|
899
|
+
if (!materializedProfile) {
|
|
900
|
+
const overrides = this.ctx.settings.get("task.agentModelOverrides");
|
|
901
|
+
const nextOverrides = { ...overrides };
|
|
902
|
+
let writesOverrides = false;
|
|
903
|
+
for (const targetRole of targetRoles) {
|
|
904
|
+
const target = GJC_MODEL_ASSIGNMENT_TARGETS[targetRole];
|
|
905
|
+
if (target.settingsPath === "modelRoles") {
|
|
906
|
+
this.ctx.settings.setModelRole(targetRole, value);
|
|
907
|
+
} else {
|
|
908
|
+
nextOverrides[targetRole] = value;
|
|
909
|
+
writesOverrides = true;
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (writesOverrides) {
|
|
913
|
+
this.ctx.settings.set("task.agentModelOverrides", nextOverrides);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
modelSelector.refreshRoleAssignments({
|
|
917
|
+
currentModel: this.ctx.session.model,
|
|
918
|
+
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
919
|
+
activeModelProfile:
|
|
920
|
+
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
921
|
+
});
|
|
922
|
+
this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
923
|
+
this.ctx.statusLine.invalidate();
|
|
924
|
+
this.ctx.updateEditorBorderColor();
|
|
925
|
+
await this.ctx.notifyConfigChanged?.();
|
|
926
|
+
const labels = targetRoles.map(
|
|
927
|
+
targetRole => GJC_MODEL_ASSIGNMENT_TARGETS[targetRole].tag ?? targetRole.toUpperCase(),
|
|
928
|
+
);
|
|
929
|
+
this.ctx.showStatus(
|
|
930
|
+
includesDefault
|
|
931
|
+
? `All model targets set to ${value} for ${labels.join(", ")}.`
|
|
932
|
+
: `Role-agent models set to ${value} for ${labels.join(", ")}.`,
|
|
933
|
+
);
|
|
934
|
+
done();
|
|
935
|
+
this.ctx.ui.requestRender();
|
|
846
936
|
} else if (role === "default") {
|
|
847
937
|
// Default: update agent state and persist as the active default model.
|
|
848
938
|
await this.ctx.session.setModel(model, role, {
|
|
@@ -880,19 +970,21 @@ export class SelectorController {
|
|
|
880
970
|
}
|
|
881
971
|
const value =
|
|
882
972
|
selectedSelector ?? formatModelSelectorValue(`${model.provider}/${model.id}`, thinkingLevel);
|
|
883
|
-
const
|
|
973
|
+
const assignments = new Map<GjcModelAssignmentTargetId, string>([[role, value]]);
|
|
974
|
+
const materializedProfile = materializeActiveModelProfileAssignments({
|
|
884
975
|
session: this.ctx.session,
|
|
885
976
|
settings: this.ctx.settings,
|
|
886
|
-
|
|
887
|
-
selector: value,
|
|
977
|
+
assignments,
|
|
888
978
|
});
|
|
889
979
|
if (!materializedProfile) {
|
|
890
980
|
const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
|
|
891
981
|
if (target.settingsPath === "modelRoles") {
|
|
892
982
|
this.ctx.settings.setModelRole(role, value);
|
|
893
983
|
} else {
|
|
894
|
-
|
|
895
|
-
|
|
984
|
+
this.ctx.settings.set("task.agentModelOverrides", {
|
|
985
|
+
...this.ctx.settings.get("task.agentModelOverrides"),
|
|
986
|
+
[role]: value,
|
|
987
|
+
});
|
|
896
988
|
}
|
|
897
989
|
}
|
|
898
990
|
modelSelector.refreshRoleAssignments({
|
|
@@ -902,6 +994,9 @@ export class SelectorController {
|
|
|
902
994
|
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
903
995
|
});
|
|
904
996
|
this.ctx.settings.getStorage()?.recordModelUsage(`${model.provider}/${model.id}`);
|
|
997
|
+
this.ctx.statusLine.invalidate();
|
|
998
|
+
this.ctx.updateEditorBorderColor();
|
|
999
|
+
await this.ctx.notifyConfigChanged?.();
|
|
905
1000
|
this.ctx.showStatus(`${role} agent model: ${value}`);
|
|
906
1001
|
done();
|
|
907
1002
|
this.ctx.ui.requestRender();
|
|
@@ -117,12 +117,14 @@ import type { CompactionQueuedMessage, InteractiveModeContext, SubmittedUserInpu
|
|
|
117
117
|
import { UiHelpers } from "./utils/ui-helpers";
|
|
118
118
|
|
|
119
119
|
const INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS = 5_000;
|
|
120
|
-
const
|
|
120
|
+
const COMPOSER_NEWLINE_HINT = process.platform === "win32" ? "Alt+Enter/Ctrl+J" : "Shift+Enter/Ctrl+J";
|
|
121
|
+
const DEFAULT_COMPOSER_PLACEHOLDER = `Type your message... ${COMPOSER_NEWLINE_HINT}: New line · Ctrl+C: Clear · Shift+Tab: Reasoning`;
|
|
122
|
+
const WELCOME_RESERVED_CONTAINER_CHILD_LIMIT = 8;
|
|
121
123
|
const FRIENDLY_KEY_PARTS: Record<string, string> = {
|
|
122
124
|
alt: "Alt",
|
|
123
|
-
cmd: "
|
|
124
|
-
command: "
|
|
125
|
-
ctrl: "
|
|
125
|
+
cmd: "Cmd",
|
|
126
|
+
command: "Cmd",
|
|
127
|
+
ctrl: "Ctrl",
|
|
126
128
|
enter: "Enter",
|
|
127
129
|
meta: process.platform === "darwin" ? "Command" : "Meta",
|
|
128
130
|
option: "Option",
|
|
@@ -132,7 +134,7 @@ const FRIENDLY_KEY_PARTS: Record<string, string> = {
|
|
|
132
134
|
function formatShortcutForPlaceholder(key: string): string {
|
|
133
135
|
return key
|
|
134
136
|
.split("+")
|
|
135
|
-
.map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? part)
|
|
137
|
+
.map(part => FRIENDLY_KEY_PARTS[part.toLowerCase()] ?? (part.length === 1 ? part.toUpperCase() : part))
|
|
136
138
|
.join("+");
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -443,7 +445,6 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
443
445
|
logger.warn("History storage unavailable", { error: String(error) });
|
|
444
446
|
}
|
|
445
447
|
this.hookWidgetContainerAbove = new Container();
|
|
446
|
-
this.hookWidgetContainerAbove.addChild(new Spacer(1));
|
|
447
448
|
this.hookWidgetContainerBelow = new Container();
|
|
448
449
|
this.editorContainer = new Container();
|
|
449
450
|
this.editorContainer.addChild(this.editor);
|
|
@@ -531,13 +532,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
531
532
|
}
|
|
532
533
|
|
|
533
534
|
if (!startupQuiet) {
|
|
534
|
-
const getWelcomeReservedBottomRows = (width: number): number =>
|
|
535
|
-
[
|
|
536
|
-
this.statusLine,
|
|
537
|
-
this.hookWidgetContainerAbove,
|
|
538
|
-
this.editorContainer,
|
|
539
|
-
this.hookWidgetContainerBelow,
|
|
540
|
-
].reduce((rows, component) => rows + component.render(width).length, 0);
|
|
535
|
+
const getWelcomeReservedBottomRows = (width: number): number => this.#getWelcomeReservedRows(width);
|
|
541
536
|
|
|
542
537
|
// Add welcome header
|
|
543
538
|
this.#welcomeComponent = new WelcomeComponent(
|
|
@@ -940,11 +935,37 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
940
935
|
|
|
941
936
|
#getComposerPlaceholder(): string {
|
|
942
937
|
if (!this.#isPromptDeliveryBusy()) return DEFAULT_COMPOSER_PLACEHOLDER;
|
|
943
|
-
const enterAction = this.settings.get("busyPromptMode") === "steer" ? "
|
|
938
|
+
const enterAction = this.settings.get("busyPromptMode") === "steer" ? "Steer" : "Queue";
|
|
944
939
|
const parts = [`Enter: ${enterAction}`];
|
|
945
940
|
const queueKey = this.#getMessageQueueShortcut();
|
|
946
|
-
if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}:
|
|
947
|
-
return `${DEFAULT_COMPOSER_PLACEHOLDER} ${parts.join(" · ")}`;
|
|
941
|
+
if (queueKey) parts.push(`${formatShortcutForPlaceholder(queueKey)}: Queue`);
|
|
942
|
+
return `${DEFAULT_COMPOSER_PLACEHOLDER} · ${parts.join(" · ")}`;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
#getWelcomeReservedRows(width: number): number {
|
|
946
|
+
const transientRows = [
|
|
947
|
+
this.chatContainer,
|
|
948
|
+
this.pendingMessagesContainer,
|
|
949
|
+
this.statusContainer,
|
|
950
|
+
this.todoContainer,
|
|
951
|
+
this.btwContainer,
|
|
952
|
+
].reduce((rows, container) => rows + this.#renderShortContainerRowsForWelcomeReservation(width, container), 0);
|
|
953
|
+
|
|
954
|
+
const pinnedRows = [
|
|
955
|
+
this.statusLine,
|
|
956
|
+
this.hookWidgetContainerAbove,
|
|
957
|
+
this.editorContainer,
|
|
958
|
+
this.hookWidgetContainerBelow,
|
|
959
|
+
].reduce((rows, component) => rows + component.render(width).length, 0);
|
|
960
|
+
|
|
961
|
+
return transientRows + pinnedRows;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
#renderShortContainerRowsForWelcomeReservation(width: number, container: Container): number {
|
|
965
|
+
if (container.children.length === 0 || container.children.length > WELCOME_RESERVED_CONTAINER_CHILD_LIMIT) {
|
|
966
|
+
return 0;
|
|
967
|
+
}
|
|
968
|
+
return container.render(width).length;
|
|
948
969
|
}
|
|
949
970
|
|
|
950
971
|
updateEditorChrome(): void {
|
|
@@ -1351,6 +1372,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
1351
1372
|
return;
|
|
1352
1373
|
}
|
|
1353
1374
|
|
|
1375
|
+
// Leaving plan mode invalidates the plan-mode steering and standing
|
|
1376
|
+
// resolve handler that the in-flight turn may still be acting on. Abort
|
|
1377
|
+
// before clearing that state so the model cannot continue from stale
|
|
1378
|
+
// plan-mode context and retry `resolve` against a retired handler.
|
|
1379
|
+
await this.session.abort({ timeoutMs: INTERACTIVE_ABORT_CLEANUP_TIMEOUT_MS });
|
|
1380
|
+
|
|
1354
1381
|
const previousTools = this.#planModePreviousTools;
|
|
1355
1382
|
if (previousTools && previousTools.length > 0) {
|
|
1356
1383
|
await this.session.setActiveToolsByName(previousTools);
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* - `gjc -p "prompt"` - text output
|
|
6
6
|
* - `gjc --mode json "prompt"` - JSON event stream
|
|
7
7
|
*/
|
|
8
|
-
import type
|
|
8
|
+
import { type AssistantMessage, type ImageContent, isContextOverflow } from "@gajae-code/ai";
|
|
9
9
|
import { logger, sanitizeText } from "@gajae-code/utils";
|
|
10
10
|
import type { AgentSession } from "../session/agent-session";
|
|
11
11
|
import { isSilentAbort } from "../session/messages";
|
|
@@ -31,6 +31,33 @@ export interface PrintModeOptions {
|
|
|
31
31
|
suppressProcessExit?: boolean;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Exit code used when a non-interactive **text-mode** run (`gjc -p`) terminates
|
|
36
|
+
* because the model context window is exhausted and automatic compaction could
|
|
37
|
+
* not bring the request under the limit. Distinct from the generic failure code
|
|
38
|
+
* (1) so text-mode callers can detect context exhaustion specifically instead of
|
|
39
|
+
* parsing the raw provider error string.
|
|
40
|
+
*
|
|
41
|
+
* Scope: text-mode final-response path only. JSON mode (`--mode json`) streams
|
|
42
|
+
* events from the subscription and does not run this terminal-error branch, so
|
|
43
|
+
* it is intentionally NOT covered by this exit code.
|
|
44
|
+
*/
|
|
45
|
+
export const CONTEXT_OVERFLOW_EXIT_CODE = 2;
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Build an actionable stderr diagnostic for a terminal context-overflow error in
|
|
49
|
+
* text mode. The raw provider message is preserved (appended) for debugging, but
|
|
50
|
+
* the leading guidance explains what happened and what the operator can do —
|
|
51
|
+
* tailored to whether auto-compaction was even enabled.
|
|
52
|
+
*/
|
|
53
|
+
function formatContextOverflowError(message: AssistantMessage, autoCompactionEnabled: boolean): string {
|
|
54
|
+
const providerDetail = message.errorMessage ? ` (provider error: ${sanitizeText(message.errorMessage)})` : "";
|
|
55
|
+
const guidance = autoCompactionEnabled
|
|
56
|
+
? "Context window exhausted: automatic compaction ran but could not reduce the request below the model's context limit. Reduce the input size (smaller file reads / tool output), raise the compaction threshold, or switch to a larger-context model."
|
|
57
|
+
: "Context window exhausted and automatic compaction is disabled. Enable it (compaction.enabled=true with a non-off compaction.strategy) so GJC can compact and continue, reduce the input size, or switch to a larger-context model.";
|
|
58
|
+
return `${guidance}${providerDetail}`;
|
|
59
|
+
}
|
|
60
|
+
|
|
34
61
|
/**
|
|
35
62
|
* Run in print (single-shot) mode.
|
|
36
63
|
* Sends prompts to the agent and outputs the result.
|
|
@@ -88,15 +115,27 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
88
115
|
(assistantMsg.stopReason === "error" || assistantMsg.stopReason === "aborted") &&
|
|
89
116
|
!isSilentAbort(assistantMsg.errorMessage)
|
|
90
117
|
) {
|
|
91
|
-
|
|
118
|
+
// Context-overflow is an expected, recoverable-in-principle condition — not
|
|
119
|
+
// an opaque crash. Auto-compaction has already run inside session.prompt();
|
|
120
|
+
// if we still land here the request could not be made to fit. In this
|
|
121
|
+
// text-mode final-response path, surface an actionable diagnostic and a
|
|
122
|
+
// distinct exit code so text-mode (`gjc -p`) callers can detect context
|
|
123
|
+
// exhaustion instead of parsing the raw provider error string. (JSON mode
|
|
124
|
+
// does not reach this branch and is intentionally out of scope.)
|
|
125
|
+
const isOverflow =
|
|
126
|
+
assistantMsg.stopReason === "error" && isContextOverflow(assistantMsg, session.model?.contextWindow);
|
|
127
|
+
const errorLine = isOverflow
|
|
128
|
+
? formatContextOverflowError(assistantMsg, session.autoCompactionEnabled)
|
|
129
|
+
: sanitizeText(assistantMsg.errorMessage || `Request ${assistantMsg.stopReason}`);
|
|
130
|
+
const exitCode = isOverflow ? CONTEXT_OVERFLOW_EXIT_CODE : 1;
|
|
92
131
|
const flushed = process.stderr.write(`${errorLine}\n`);
|
|
93
132
|
// When the caller owns finalization (RLM autonomous), return instead of
|
|
94
133
|
// exiting so its cleanup runs; the caller surfaces a non-zero exit itself.
|
|
95
134
|
if (!options.suppressProcessExit) {
|
|
96
135
|
if (flushed) {
|
|
97
|
-
process.exit(
|
|
136
|
+
process.exit(exitCode);
|
|
98
137
|
} else {
|
|
99
|
-
process.stderr.once("drain", () => process.exit(
|
|
138
|
+
process.stderr.once("drain", () => process.exit(exitCode));
|
|
100
139
|
}
|
|
101
140
|
}
|
|
102
141
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
type AutocompleteItem,
|
|
3
|
+
type AutocompleteProvider,
|
|
4
|
+
CombinedAutocompleteProvider,
|
|
5
|
+
extractSlashCommandTokenPrefix,
|
|
6
|
+
getKeybindings,
|
|
7
|
+
getSlashCommandMatchRank,
|
|
8
|
+
type SlashCommand,
|
|
9
|
+
} from "@gajae-code/tui";
|
|
3
10
|
import type { KeybindingsManager } from "../config/keybindings";
|
|
4
11
|
import { formatKeyHints } from "../config/keybindings";
|
|
5
12
|
import { isSettingsInitialized, settings } from "../config/settings";
|
|
@@ -147,6 +154,10 @@ function sortSlashCommandSuggestions(
|
|
|
147
154
|
return { ...suggestions, items };
|
|
148
155
|
}
|
|
149
156
|
|
|
157
|
+
function isRootPathSuggestionResult(suggestions: { items: AutocompleteItem[]; prefix: string } | null): boolean {
|
|
158
|
+
return suggestions?.prefix.startsWith("/") === true && suggestions.items.some(item => item.value.startsWith("/"));
|
|
159
|
+
}
|
|
160
|
+
|
|
150
161
|
function withoutSkillCommandSuggestions(
|
|
151
162
|
suggestions: { items: AutocompleteItem[]; prefix: string } | null,
|
|
152
163
|
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
@@ -168,18 +179,9 @@ function getPromptActionPrefix(textBeforeCursor: string): string | null {
|
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
function getSlashTokenPrefix(textBeforeCursor: string): string | null {
|
|
171
|
-
|
|
172
|
-
if (slashIndex === -1) return null;
|
|
173
|
-
const beforeSlash = textBeforeCursor.slice(0, slashIndex);
|
|
174
|
-
if (beforeSlash && !/\s$/.test(beforeSlash)) return null;
|
|
175
|
-
const token = textBeforeCursor.slice(slashIndex);
|
|
176
|
-
if (/[\s]/.test(token)) return null;
|
|
177
|
-
return token;
|
|
182
|
+
return extractSlashCommandTokenPrefix(textBeforeCursor);
|
|
178
183
|
}
|
|
179
184
|
|
|
180
|
-
function isLeadingSlashToken(textBeforeCursor: string, slashPrefix: string): boolean {
|
|
181
|
-
return textBeforeCursor.trimStart() === slashPrefix;
|
|
182
|
-
}
|
|
183
185
|
export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
184
186
|
#baseProvider: CombinedAutocompleteProvider;
|
|
185
187
|
#actions: PromptActionDefinition[];
|
|
@@ -226,14 +228,13 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
226
228
|
|
|
227
229
|
const slashPrefix = getSlashTokenPrefix(textBeforeCursor);
|
|
228
230
|
if (slashPrefix) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const skillCommandSuggestions =
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
: null;
|
|
231
|
+
const baseSuggestions = withoutSkillCommandSuggestions(
|
|
232
|
+
await this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol),
|
|
233
|
+
);
|
|
234
|
+
if (isRootPathSuggestionResult(baseSuggestions)) return baseSuggestions;
|
|
235
|
+
const skillCommandSuggestions = this.#getSkillCommandSuggestions(textBeforeCursor, {
|
|
236
|
+
includeEmpty: false,
|
|
237
|
+
});
|
|
237
238
|
return sortSlashCommandSuggestions(
|
|
238
239
|
mergeAutocompleteSuggestions(baseSuggestions, skillCommandSuggestions),
|
|
239
240
|
this.#commands,
|
|
@@ -23,6 +23,7 @@ export function buildHotkeysMarkdown(bindings: HotkeysMarkdownBindings): string
|
|
|
23
23
|
"|-----|--------|",
|
|
24
24
|
"| `Enter` | Send / queue while busy |",
|
|
25
25
|
`| \`${appKey(bindings, "app.message.queue")}\` | Queue message for next turn |`,
|
|
26
|
+
`| \`${appKey(bindings, "app.message.dequeue")}\` | Select queued message to edit |`,
|
|
26
27
|
"| `Shift+Enter` / `Ctrl+J` | New line |",
|
|
27
28
|
"| `Ctrl+W` / `Option+Backspace` | Delete word backwards |",
|
|
28
29
|
"| `Ctrl+U` | Delete to start of line |",
|
|
@@ -599,8 +599,8 @@ export class UiHelpers {
|
|
|
599
599
|
const queuedText = theme.fg("dim", `${entry.label}: ${entry.message}`);
|
|
600
600
|
this.ctx.pendingMessagesContainer.addChild(new TruncatedText(queuedText, 1, 0));
|
|
601
601
|
}
|
|
602
|
-
const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue") || "Alt+Up";
|
|
603
|
-
const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to edit`);
|
|
602
|
+
const dequeueKey = this.ctx.keybindings.getDisplayString("app.message.dequeue") || "Alt+Up/Alt+Down";
|
|
603
|
+
const hintText = theme.fg("dim", `${theme.tree.hook} ${dequeueKey} to select/edit/reorder`);
|
|
604
604
|
this.ctx.pendingMessagesContainer.addChild(new TruncatedText(hintText, 1, 0));
|
|
605
605
|
}
|
|
606
606
|
}
|
|
@@ -52,7 +52,14 @@ export function isGloballyConfigured(cfg: NotificationConfig): boolean {
|
|
|
52
52
|
export function shouldRegisterNotificationsExtension(input: {
|
|
53
53
|
env: NodeJS.ProcessEnv;
|
|
54
54
|
cfg?: NotificationConfig;
|
|
55
|
+
/** Task recursion depth; helper/subagent sessions must not spawn remote surfaces. */
|
|
56
|
+
taskDepth?: number;
|
|
57
|
+
/** Parent subagent id/prefix; present for helper/subagent sessions even when depth is omitted. */
|
|
58
|
+
parentTaskPrefix?: string;
|
|
59
|
+
/** Role-agent type/name; present for task sessions even if depth metadata is lost. */
|
|
60
|
+
currentAgentType?: string;
|
|
55
61
|
}): boolean {
|
|
62
|
+
if ((input.taskDepth ?? 0) > 0 || input.parentTaskPrefix || input.currentAgentType) return false;
|
|
56
63
|
if (input.env.GJC_NOTIFICATIONS === "0") return false;
|
|
57
64
|
if (input.env.GJC_NOTIFICATIONS === "1" || input.env.GJC_NOTIFICATIONS_TOKEN) return true;
|
|
58
65
|
return input.cfg ? isGloballyConfigured(input.cfg) : false;
|
|
@@ -112,7 +119,8 @@ export interface RedactableAction {
|
|
|
112
119
|
* When redact is false, return the action unchanged.
|
|
113
120
|
*
|
|
114
121
|
* Redaction still applies to streamed content frames (turn_stream, context_update,
|
|
115
|
-
* image_attachment) which are suppressed at their emit sites, not here.
|
|
122
|
+
* image_attachment) which are suppressed at their emit sites, not here. Explicit
|
|
123
|
+
* `telegram_send` file attachments are rejected before the file is read or forwarded.
|
|
116
124
|
*/
|
|
117
125
|
export function buildRedactedAction(
|
|
118
126
|
action: RedactableAction,
|
|
@@ -7,6 +7,7 @@ export interface DaemonPaths {
|
|
|
7
7
|
roots: string;
|
|
8
8
|
steal: string;
|
|
9
9
|
aliases: string;
|
|
10
|
+
seenUpdates: string;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
export function daemonPaths(agentDir: string): DaemonPaths {
|
|
@@ -18,5 +19,6 @@ export function daemonPaths(agentDir: string): DaemonPaths {
|
|
|
18
19
|
roots: path.join(dir, "telegram-daemon.roots.json"),
|
|
19
20
|
steal: path.join(dir, "telegram-daemon.steal"),
|
|
20
21
|
aliases: path.join(dir, "telegram-callback-aliases.json"),
|
|
22
|
+
seenUpdates: path.join(dir, "telegram-seen-updates.json"),
|
|
21
23
|
};
|
|
22
24
|
}
|
|
@@ -28,7 +28,7 @@ import type { ImageContent, TextContent } from "@gajae-code/ai";
|
|
|
28
28
|
import { NotificationServer } from "@gajae-code/natives";
|
|
29
29
|
import { logger, postmortem } from "@gajae-code/utils";
|
|
30
30
|
import { Settings } from "../config/settings";
|
|
31
|
-
import type { ExtensionCommandContext, ExtensionContext
|
|
31
|
+
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../extensibility/extensions";
|
|
32
32
|
import { registerAskAnswerSource } from "../tools/ask-answer-registry";
|
|
33
33
|
import { registerTelegramFileSink } from "./attachment-registry";
|
|
34
34
|
import {
|
|
@@ -100,7 +100,7 @@ export interface SessionCloseFrame {
|
|
|
100
100
|
chatId: string;
|
|
101
101
|
token: string;
|
|
102
102
|
target: SessionCloseTarget;
|
|
103
|
-
/**
|
|
103
|
+
/** Required force-only close flag; false/omitted is rejected by daemon policy. */
|
|
104
104
|
force?: boolean;
|
|
105
105
|
}
|
|
106
106
|
|
|
@@ -336,6 +336,8 @@ interface SessionRuntime {
|
|
|
336
336
|
disposeAnswerSource: () => void;
|
|
337
337
|
/** Deregisters this session's Telegram file sink. */
|
|
338
338
|
disposeFileSink: () => void;
|
|
339
|
+
/** Deregisters this session's unattended workflow-gate listener. */
|
|
340
|
+
disposeGateListener: () => void;
|
|
339
341
|
redact: boolean;
|
|
340
342
|
verbosity: "lean" | "verbose";
|
|
341
343
|
sessionTag: string;
|
|
@@ -358,6 +360,8 @@ interface ResolvedSettings {
|
|
|
358
360
|
settingsAvailable: boolean;
|
|
359
361
|
}
|
|
360
362
|
|
|
363
|
+
const TELEGRAM_FILE_REDACTION_ERROR = "Telegram file attachments are disabled while notifications redaction is on.";
|
|
364
|
+
|
|
361
365
|
const defaultConfig: NotificationConfig = {
|
|
362
366
|
enabled: false,
|
|
363
367
|
botToken: undefined,
|
|
@@ -379,7 +383,9 @@ export function notificationsEnabled(): boolean {
|
|
|
379
383
|
return process.env.GJC_NOTIFICATIONS === "1" || Boolean(process.env.GJC_NOTIFICATIONS_TOKEN);
|
|
380
384
|
}
|
|
381
385
|
|
|
382
|
-
function resolveSettings(): ResolvedSettings {
|
|
386
|
+
function resolveSettings(settingsOverride?: Settings): ResolvedSettings {
|
|
387
|
+
if (settingsOverride)
|
|
388
|
+
return { settings: settingsOverride, cfg: getNotificationConfig(settingsOverride), settingsAvailable: true };
|
|
383
389
|
try {
|
|
384
390
|
const settings = Settings.instance;
|
|
385
391
|
return { settings, cfg: getNotificationConfig(settings), settingsAvailable: true };
|
|
@@ -491,7 +497,7 @@ function sessionIdFromFile(file: string | undefined): string | undefined {
|
|
|
491
497
|
return underscore >= 0 ? base.slice(underscore + 1) : undefined;
|
|
492
498
|
}
|
|
493
499
|
|
|
494
|
-
export
|
|
500
|
+
export function createNotificationsExtension(api: ExtensionAPI, options: { settings?: Settings } = {}): void {
|
|
495
501
|
const runtimes = new Map<string, SessionRuntime>();
|
|
496
502
|
const disabledSessions = new Set<string>();
|
|
497
503
|
const sessionId = (ctx: ExtensionContext): string => ctx.sessionManager.getSessionId();
|
|
@@ -503,9 +509,16 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
503
509
|
runtimes.delete(id);
|
|
504
510
|
try {
|
|
505
511
|
rt.cancelPostmortemCleanup();
|
|
512
|
+
} catch {}
|
|
513
|
+
try {
|
|
506
514
|
rt.disposeAnswerSource();
|
|
515
|
+
} catch {}
|
|
516
|
+
try {
|
|
507
517
|
rt.disposeFileSink();
|
|
508
518
|
} catch {}
|
|
519
|
+
try {
|
|
520
|
+
rt.disposeGateListener();
|
|
521
|
+
} catch {}
|
|
509
522
|
// Resolve any still-pending interactive asks so the ask tool is not left hanging.
|
|
510
523
|
for (const pending of rt.pendingInteractive.values()) pending.resolve(undefined);
|
|
511
524
|
rt.pendingInteractive.clear();
|
|
@@ -529,10 +542,14 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
529
542
|
return isSessionNotificationsEnabled({ cfg, env: process.env, sessionDisabled: disabledSessions.has(id) });
|
|
530
543
|
}
|
|
531
544
|
|
|
545
|
+
function isNotificationEligibleContext(ctx: ExtensionContext): boolean {
|
|
546
|
+
return ctx.sessionMetadata?.kind !== "sub";
|
|
547
|
+
}
|
|
548
|
+
|
|
532
549
|
async function startSession(ctx: ExtensionContext): Promise<"started" | "already" | "disabled" | "failed"> {
|
|
533
550
|
const id = sessionId(ctx);
|
|
534
|
-
const { settings, cfg, settingsAvailable } = resolveSettings();
|
|
535
|
-
if (!isEnabledForSession(id, cfg)) return "disabled";
|
|
551
|
+
const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
|
|
552
|
+
if (!isNotificationEligibleContext(ctx) || !isEnabledForSession(id, cfg)) return "disabled";
|
|
536
553
|
if (runtimes.has(id)) return "already";
|
|
537
554
|
|
|
538
555
|
const stateRoot = path.join(ctx.cwd, ".gjc", "state");
|
|
@@ -660,6 +677,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
660
677
|
tag,
|
|
661
678
|
);
|
|
662
679
|
const disposeFileSink = registerTelegramFileSink(id, async file => {
|
|
680
|
+
if (runtime?.redact ?? redact) {
|
|
681
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
682
|
+
}
|
|
683
|
+
|
|
663
684
|
try {
|
|
664
685
|
const data = await fs.promises.readFile(file.path);
|
|
665
686
|
server.pushFrame(
|
|
@@ -683,6 +704,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
683
704
|
pendingInteractive,
|
|
684
705
|
disposeAnswerSource,
|
|
685
706
|
disposeFileSink,
|
|
707
|
+
disposeGateListener: () => {},
|
|
686
708
|
cancelPostmortemCleanup: () => {},
|
|
687
709
|
redact,
|
|
688
710
|
verbosity,
|
|
@@ -726,7 +748,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
726
748
|
|
|
727
749
|
// Unattended: a real ask emits a workflow gate; register it repliable by gate_id.
|
|
728
750
|
if (unattended && gate?.onGateEmitted) {
|
|
729
|
-
gate.onGateEmitted(g => {
|
|
751
|
+
runtime.disposeGateListener = gate.onGateEmitted(g => {
|
|
730
752
|
const options = (g.options ?? []).map(o => String((o as { label?: unknown }).label ?? ""));
|
|
731
753
|
gateOptions.set(g.gate_id, options);
|
|
732
754
|
const promptCtx = g.context as { prompt?: unknown; title?: unknown } | undefined;
|
|
@@ -761,7 +783,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
761
783
|
async handler(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
762
784
|
const id = sessionId(ctx);
|
|
763
785
|
const command = args.trim().split(/\s+/, 1)[0]?.toLowerCase() || "status";
|
|
764
|
-
const resolved = resolveSettings();
|
|
786
|
+
const resolved = resolveSettings(options.settings);
|
|
765
787
|
const enabledWithoutLocalOff = isSessionNotificationsEnabled({
|
|
766
788
|
cfg: resolved.cfg,
|
|
767
789
|
env: process.env,
|
|
@@ -781,6 +803,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
781
803
|
}
|
|
782
804
|
|
|
783
805
|
if (command === "on") {
|
|
806
|
+
if (!isNotificationEligibleContext(ctx)) {
|
|
807
|
+
ctx.ui.notify("Notifications are disabled for subagent sessions.", "warning");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
784
810
|
if (process.env.GJC_NOTIFICATIONS === "0") {
|
|
785
811
|
ctx.ui.notify(
|
|
786
812
|
"Notifications remain disabled: GJC_NOTIFICATIONS=0 is an authoritative opt-out.",
|
|
@@ -878,6 +904,10 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
878
904
|
rt.sessionTag,
|
|
879
905
|
);
|
|
880
906
|
rt.disposeFileSink = registerTelegramFileSink(newId, async file => {
|
|
907
|
+
if (rt.redact) {
|
|
908
|
+
return { ok: false, error: TELEGRAM_FILE_REDACTION_ERROR };
|
|
909
|
+
}
|
|
910
|
+
|
|
881
911
|
try {
|
|
882
912
|
const data = await fs.promises.readFile(file.path);
|
|
883
913
|
rt.server.pushFrame(
|
|
@@ -1099,4 +1129,4 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
1099
1129
|
api.on("session_shutdown", async (_event, ctx) => {
|
|
1100
1130
|
await stopSession(sessionId(ctx));
|
|
1101
1131
|
});
|
|
1102
|
-
}
|
|
1132
|
+
}
|