@gajae-code/coding-agent 0.7.8 → 0.7.10
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 +40 -0
- package/README.md +8 -0
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/model-profile-activation.d.ts +19 -2
- package/dist/types/config/model-profiles.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +19 -3
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +0 -4
- package/dist/types/modes/components/hook-input.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +5 -0
- package/dist/types/modes/components/model-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line.d.ts +3 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +3 -1
- package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
- package/dist/types/modes/types.d.ts +3 -1
- package/dist/types/notifications/daemon-paths.d.ts +9 -0
- package/dist/types/notifications/html-format.d.ts +2 -0
- package/dist/types/notifications/recent-activity.d.ts +5 -1
- package/dist/types/notifications/telegram-daemon-cli.d.ts +8 -2
- package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
- package/dist/types/notifications/telegram-daemon.d.ts +1 -10
- package/dist/types/notifications/telegram-reference.d.ts +4 -0
- package/dist/types/notifications/threaded-render.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +26 -0
- package/dist/types/tools/ultragoal-ask-guard.d.ts +9 -2
- package/dist/types/utils/cmux-workspace.d.ts +23 -0
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +5 -0
- package/dist/types/web/search/render.d.ts +1 -0
- package/package.json +7 -7
- package/src/cli/notify-cli.ts +1 -1
- package/src/cli/web-search-cli.ts +3 -2
- package/src/cli.ts +1 -0
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profile-activation.ts +92 -11
- package/src/config/model-profiles.ts +15 -2
- package/src/config/model-registry.ts +81 -5
- package/src/config/settings-schema.ts +14 -3
- package/src/coordinator-mcp/server.ts +4 -2
- package/src/deep-interview/render-middleware.ts +1 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +40 -8
- package/src/defaults/gjc/skills/ralplan/SKILL.md +14 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -112
- package/src/extensibility/extensions/types.ts +11 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +1 -1
- package/src/gjc-runtime/launch-tmux.ts +366 -48
- package/src/gjc-runtime/team-runtime.ts +69 -4
- package/src/gjc-runtime/ultragoal-guard.ts +150 -24
- package/src/gjc-runtime/ultragoal-runtime.ts +16 -124
- package/src/gjc-runtime/workflow-manifest.generated.json +0 -8
- package/src/gjc-runtime/workflow-manifest.ts +0 -1
- package/src/hooks/skill-state.ts +33 -73
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/modes/components/custom-model-preset-wizard.ts +2 -4
- package/src/modes/components/hook-editor.ts +7 -4
- package/src/modes/components/hook-input.ts +4 -0
- package/src/modes/components/hook-selector.ts +21 -3
- package/src/modes/components/model-selector.ts +84 -13
- package/src/modes/components/settings-selector.ts +83 -24
- package/src/modes/components/status-line/presets.ts +11 -0
- package/src/modes/components/status-line/segments.ts +10 -2
- package/src/modes/components/status-line.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +130 -7
- package/src/modes/interactive-mode.ts +7 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +64 -13
- package/src/modes/types.ts +6 -1
- package/src/notifications/daemon-paths.ts +22 -0
- package/src/notifications/html-format.ts +75 -1
- package/src/notifications/index.ts +12 -1
- package/src/notifications/recent-activity.ts +32 -8
- package/src/notifications/telegram-daemon-cli.ts +13 -4
- package/src/notifications/telegram-daemon-control.ts +9 -0
- package/src/notifications/telegram-daemon.ts +69 -45
- package/src/notifications/telegram-reference.ts +36 -14
- package/src/notifications/threaded-render.ts +14 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/sdk.ts +13 -6
- package/src/session/agent-session.ts +10 -1
- package/src/tools/ask.ts +147 -17
- package/src/tools/eval.ts +2 -2
- package/src/tools/render-utils.ts +2 -0
- package/src/tools/ultragoal-ask-guard.ts +39 -5
- package/src/utils/cmux-workspace.ts +106 -0
- package/src/utils/title-generator.ts +2 -0
- package/src/web/search/index.ts +22 -10
- package/src/web/search/provider.ts +15 -0
- package/src/web/search/render.ts +3 -0
|
@@ -4,8 +4,14 @@ import type { OAuthProvider } from "@gajae-code/ai/utils/oauth/types";
|
|
|
4
4
|
import type { Component, OverlayHandle } from "@gajae-code/tui";
|
|
5
5
|
import { Input, Loader, Spacer, Text } from "@gajae-code/tui";
|
|
6
6
|
import { getAgentDbPath, getProjectDir } from "@gajae-code/utils";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
activateModelProfile,
|
|
9
|
+
type MaterializeModelProfileForDeletionResult,
|
|
10
|
+
materializeActiveModelProfileAssignment,
|
|
11
|
+
materializeModelProfileForDeletion,
|
|
12
|
+
restoreMaterializedModelProfileForDeletion,
|
|
13
|
+
} from "../../config/model-profile-activation";
|
|
14
|
+
import { formatModelProfileDisplayLabel, recommendModelProfileForProvider } from "../../config/model-profiles";
|
|
9
15
|
import { GJC_MODEL_ASSIGNMENT_TARGETS } from "../../config/model-registry";
|
|
10
16
|
import { formatModelSelectorValue } from "../../config/model-resolver";
|
|
11
17
|
import type { ModelProfileConfig } from "../../config/models-config-schema";
|
|
@@ -39,7 +45,11 @@ import {
|
|
|
39
45
|
CREDENTIAL_AUTO_IMPORT_ROTATION_WARNING,
|
|
40
46
|
runExternalCredentialAutoImport,
|
|
41
47
|
} from "../../setup/credential-auto-import";
|
|
42
|
-
import {
|
|
48
|
+
import {
|
|
49
|
+
filterAutoImportOAuthCredentials,
|
|
50
|
+
formatDiscoverySummary,
|
|
51
|
+
type ImportableCredential,
|
|
52
|
+
} from "../../setup/credential-import";
|
|
43
53
|
import {
|
|
44
54
|
MODEL_ONBOARDING_API_PROVIDER_COMMAND,
|
|
45
55
|
MODEL_ONBOARDING_PROVIDER_PRESET_COMMAND,
|
|
@@ -248,7 +258,7 @@ export class SelectorController {
|
|
|
248
258
|
const profile = await this.ctx.session.modelRegistry.saveCustomModelProfile(input.name, input.profile);
|
|
249
259
|
await this.ctx.session.modelRegistry.refresh("offline");
|
|
250
260
|
await this.ctx.notifyConfigChanged?.();
|
|
251
|
-
this.ctx.showStatus(`Custom model preset created: ${profile
|
|
261
|
+
this.ctx.showStatus(`Custom model preset created: ${formatModelProfileDisplayLabel(profile)}`);
|
|
252
262
|
done();
|
|
253
263
|
this.ctx.ui.requestRender();
|
|
254
264
|
} catch (err) {
|
|
@@ -271,6 +281,106 @@ export class SelectorController {
|
|
|
271
281
|
});
|
|
272
282
|
}
|
|
273
283
|
|
|
284
|
+
async #renameCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
|
|
285
|
+
const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
|
|
286
|
+
const currentName = profile ? formatModelProfileDisplayLabel(profile) : profileName;
|
|
287
|
+
const input = await this.ctx.showHookInput(`Rename custom model preset: ${currentName}`, undefined, undefined, {
|
|
288
|
+
initialValue: currentName,
|
|
289
|
+
});
|
|
290
|
+
if (input === undefined) {
|
|
291
|
+
this.ctx.showStatus("Preset rename cancelled.");
|
|
292
|
+
this.ctx.ui.requestRender();
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const renamed = await this.ctx.session.modelRegistry.renameCustomModelProfile(profileName, input);
|
|
297
|
+
await this.ctx.session.modelRegistry.refresh("offline");
|
|
298
|
+
await this.ctx.notifyConfigChanged?.();
|
|
299
|
+
modelSelector.refreshPresetProfiles(renamed.name);
|
|
300
|
+
this.ctx.showStatus(`Custom model preset renamed: ${formatModelProfileDisplayLabel(renamed)}`);
|
|
301
|
+
this.ctx.ui.requestRender();
|
|
302
|
+
} catch (err) {
|
|
303
|
+
this.ctx.showError(`Preset rename failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async #deleteCustomModelPreset(profileName: string, modelSelector: ModelSelectorComponent): Promise<void> {
|
|
308
|
+
const profile = this.ctx.session.modelRegistry.getModelProfile(profileName);
|
|
309
|
+
const profileLabel = profile ? formatModelProfileDisplayLabel(profile) : profileName;
|
|
310
|
+
const confirmed = await this.ctx.showHookConfirm(
|
|
311
|
+
`Delete custom model preset: ${profileLabel}`,
|
|
312
|
+
"This removes the preset entry after preserving current role model settings when this preset is active/default.",
|
|
313
|
+
);
|
|
314
|
+
if (!confirmed) {
|
|
315
|
+
this.ctx.showStatus("Preset delete cancelled.");
|
|
316
|
+
this.ctx.ui.requestRender();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const activeProfile = this.ctx.session.getActiveModelProfile?.();
|
|
321
|
+
const defaultProfile = this.ctx.settings.get("modelProfile.default");
|
|
322
|
+
let snapshot: MaterializeModelProfileForDeletionResult | undefined;
|
|
323
|
+
let deletedProfile: ModelProfileConfig | undefined;
|
|
324
|
+
const refreshSelectorState = (refreshedProfileName?: string): void => {
|
|
325
|
+
modelSelector.refreshRoleAssignments({
|
|
326
|
+
currentModel: this.ctx.session.model,
|
|
327
|
+
currentThinkingLevel: this.ctx.session.thinkingLevel,
|
|
328
|
+
activeModelProfile:
|
|
329
|
+
this.ctx.session.getActiveModelProfile?.() ?? this.ctx.settings.get("modelProfile.default"),
|
|
330
|
+
});
|
|
331
|
+
modelSelector.refreshPresetProfiles(refreshedProfileName);
|
|
332
|
+
};
|
|
333
|
+
try {
|
|
334
|
+
if (activeProfile === profileName || defaultProfile === profileName) {
|
|
335
|
+
snapshot = await materializeModelProfileForDeletion({
|
|
336
|
+
session: this.ctx.session,
|
|
337
|
+
modelRegistry: this.ctx.session.modelRegistry,
|
|
338
|
+
settings: this.ctx.settings,
|
|
339
|
+
profileName,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
deletedProfile = await this.ctx.session.modelRegistry.deleteCustomModelProfile(profileName);
|
|
343
|
+
await this.ctx.session.modelRegistry.refresh("offline");
|
|
344
|
+
await this.ctx.notifyConfigChanged?.();
|
|
345
|
+
refreshSelectorState();
|
|
346
|
+
this.ctx.showStatus(`Custom model preset deleted: ${profileLabel}`);
|
|
347
|
+
this.ctx.ui.requestRender();
|
|
348
|
+
} catch (err) {
|
|
349
|
+
let presetRestoreError: unknown;
|
|
350
|
+
if (deletedProfile) {
|
|
351
|
+
try {
|
|
352
|
+
await this.ctx.session.modelRegistry.saveCustomModelProfile(profileName, deletedProfile);
|
|
353
|
+
await this.ctx.session.modelRegistry.refresh("offline");
|
|
354
|
+
} catch (restoreErr) {
|
|
355
|
+
presetRestoreError = restoreErr;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (snapshot) {
|
|
359
|
+
try {
|
|
360
|
+
await restoreMaterializedModelProfileForDeletion({
|
|
361
|
+
settings: this.ctx.settings,
|
|
362
|
+
session: this.ctx.session,
|
|
363
|
+
snapshot,
|
|
364
|
+
});
|
|
365
|
+
} catch (restoreErr) {
|
|
366
|
+
refreshSelectorState(deletedProfile ? profileName : undefined);
|
|
367
|
+
this.ctx.showError(
|
|
368
|
+
`Preset delete failed and settings rollback failed: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`,
|
|
369
|
+
);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (deletedProfile) refreshSelectorState(profileName);
|
|
374
|
+
if (presetRestoreError) {
|
|
375
|
+
this.ctx.showError(
|
|
376
|
+
`Preset delete failed and preset restore failed: ${presetRestoreError instanceof Error ? presetRestoreError.message : String(presetRestoreError)}`,
|
|
377
|
+
);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
this.ctx.showError(`Preset delete failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
274
384
|
showCustomProviderWizard(): void {
|
|
275
385
|
this.showSelector(done => {
|
|
276
386
|
let wizard: CustomProviderWizardComponent;
|
|
@@ -693,7 +803,20 @@ export class SelectorController {
|
|
|
693
803
|
this.showCustomModelPresetWizard(selection.profile);
|
|
694
804
|
return;
|
|
695
805
|
}
|
|
806
|
+
if (selection.kind === "renameProfile") {
|
|
807
|
+
await this.#renameCustomModelPreset(selection.profileName, modelSelector);
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
if (selection.kind === "deleteProfile") {
|
|
811
|
+
await this.#deleteCustomModelPreset(selection.profileName, modelSelector);
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
696
814
|
if (selection.kind === "profile") {
|
|
815
|
+
const profileLabel = formatModelProfileDisplayLabel(
|
|
816
|
+
this.ctx.session.modelRegistry.getModelProfile(selection.profileName) ?? {
|
|
817
|
+
name: selection.profileName,
|
|
818
|
+
},
|
|
819
|
+
);
|
|
697
820
|
await activateModelProfile(
|
|
698
821
|
{
|
|
699
822
|
session: this.ctx.session,
|
|
@@ -707,8 +830,8 @@ export class SelectorController {
|
|
|
707
830
|
this.ctx.updateEditorBorderColor();
|
|
708
831
|
this.ctx.showStatus(
|
|
709
832
|
selection.setDefault
|
|
710
|
-
? `Default model profile: ${
|
|
711
|
-
: `Model profile: ${
|
|
833
|
+
? `Default model profile: ${profileLabel}`
|
|
834
|
+
: `Model profile: ${profileLabel}`,
|
|
712
835
|
);
|
|
713
836
|
done();
|
|
714
837
|
this.ctx.ui.requestRender();
|
|
@@ -1338,7 +1461,7 @@ export class SelectorController {
|
|
|
1338
1461
|
}
|
|
1339
1462
|
}
|
|
1340
1463
|
|
|
1341
|
-
let externalCredentialCandidates:
|
|
1464
|
+
let externalCredentialCandidates: ImportableCredential[] = [];
|
|
1342
1465
|
if (
|
|
1343
1466
|
mode === "login" &&
|
|
1344
1467
|
providerId === undefined &&
|
|
@@ -2684,8 +2684,13 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2684
2684
|
this.#extensionUiController.hideHookSelector();
|
|
2685
2685
|
}
|
|
2686
2686
|
|
|
2687
|
-
showHookInput(
|
|
2688
|
-
|
|
2687
|
+
showHookInput(
|
|
2688
|
+
title: string,
|
|
2689
|
+
placeholder?: string,
|
|
2690
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
2691
|
+
inputOptions?: { readonly initialValue?: string },
|
|
2692
|
+
): Promise<string | undefined> {
|
|
2693
|
+
return this.#extensionUiController.showHookInput(title, placeholder, dialogOptions, inputOptions);
|
|
2689
2694
|
}
|
|
2690
2695
|
|
|
2691
2696
|
hideHookInput(): void {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* the interactive select/editor UI) when an unattended controller + gate broker
|
|
12
12
|
* are attached is wired with the transport in #321 and exercised by #323.
|
|
13
13
|
*/
|
|
14
|
-
import type { RpcJsonSchema } from "../../rpc/rpc-types";
|
|
14
|
+
import type { RpcJsonSchema, RpcWorkflowGateKind, RpcWorkflowStage } from "../../rpc/rpc-types";
|
|
15
15
|
import type { OpenGateInput } from "./workflow-gate-broker";
|
|
16
16
|
|
|
17
17
|
/** "Other (type your own)" sentinel, mirroring the interactive ask tool. */
|
|
@@ -26,6 +26,11 @@ export interface AskGateDeepInterviewState {
|
|
|
26
26
|
ambiguity: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
export interface AskGateWorkflowGateMeta {
|
|
30
|
+
stage: RpcWorkflowStage;
|
|
31
|
+
kind: RpcWorkflowGateKind;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
export interface AskGateQuestion {
|
|
30
35
|
id: string;
|
|
31
36
|
question: string;
|
|
@@ -37,6 +42,8 @@ export interface AskGateQuestion {
|
|
|
37
42
|
* `stage_state`; when absent, the question text is regex-parsed as a fallback.
|
|
38
43
|
*/
|
|
39
44
|
deepInterview?: AskGateDeepInterviewState;
|
|
45
|
+
/** Override the emitted workflow gate address for non-deep-interview ask prompts. */
|
|
46
|
+
workflowGate?: AskGateWorkflowGateMeta;
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
export interface AskGateResult {
|
|
@@ -46,6 +53,7 @@ export interface AskGateResult {
|
|
|
46
53
|
multi: boolean;
|
|
47
54
|
selectedOptions: string[];
|
|
48
55
|
customInput?: string;
|
|
56
|
+
clarificationQuestion?: string;
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
/**
|
|
@@ -54,12 +62,15 @@ export interface AskGateResult {
|
|
|
54
62
|
* `selected` are picked option labels; free text is conveyed by `other: true`
|
|
55
63
|
* plus `custom`, encoded separately from `selected` so a real option whose label
|
|
56
64
|
* happens to equal the display sentinel can never collide with the free-text path.
|
|
65
|
+
*
|
|
66
|
+
* `action: "clarify"` is a non-answer. It lets a Deep Interview user ask about
|
|
67
|
+
* the presented choices without advancing the round, scoring ambiguity, or
|
|
68
|
+
* recording selection state.
|
|
57
69
|
*/
|
|
58
|
-
export
|
|
59
|
-
selected: string[];
|
|
60
|
-
other?:
|
|
61
|
-
|
|
62
|
-
}
|
|
70
|
+
export type DeepInterviewGateAnswer =
|
|
71
|
+
| { selected: string[]; other?: false; custom?: undefined; action?: "answer" }
|
|
72
|
+
| { selected: string[]; other: true; custom: string; action?: "answer" }
|
|
73
|
+
| { action: "clarify"; question: string };
|
|
63
74
|
|
|
64
75
|
export class DeepInterviewGateError extends Error {
|
|
65
76
|
constructor(
|
|
@@ -68,6 +79,7 @@ export class DeepInterviewGateError extends Error {
|
|
|
68
79
|
| "unknown_option"
|
|
69
80
|
| "multi_not_allowed"
|
|
70
81
|
| "missing_custom"
|
|
82
|
+
| "missing_clarification"
|
|
71
83
|
| "empty_selection"
|
|
72
84
|
| "duplicate_selection",
|
|
73
85
|
message: string,
|
|
@@ -120,13 +132,22 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
|
|
|
120
132
|
selected: selectedBase,
|
|
121
133
|
other: { type: "boolean", description: "set true to provide a free-text answer in `custom`" },
|
|
122
134
|
custom: { type: "string", minLength: 1, description: "free-text answer; required when `other` is true" },
|
|
135
|
+
action: {
|
|
136
|
+
type: "string",
|
|
137
|
+
enum: ["answer", "clarify"],
|
|
138
|
+
description: "set to `clarify` to ask about the choices without answering the round",
|
|
139
|
+
},
|
|
140
|
+
question: {
|
|
141
|
+
type: "string",
|
|
142
|
+
minLength: 1,
|
|
143
|
+
description: "clarification question; required when action is `clarify`",
|
|
144
|
+
},
|
|
123
145
|
},
|
|
124
|
-
required: ["selected"],
|
|
125
146
|
additionalProperties: false,
|
|
126
147
|
anyOf: [
|
|
127
148
|
{
|
|
128
149
|
type: "object",
|
|
129
|
-
properties: { selected: selectedOnly, other: { const: false } },
|
|
150
|
+
properties: { selected: selectedOnly, other: { const: false }, action: { const: "answer" } },
|
|
130
151
|
required: ["selected"],
|
|
131
152
|
additionalProperties: false,
|
|
132
153
|
},
|
|
@@ -136,10 +157,20 @@ function questionAnswerSchema(question: AskGateQuestion, labels: string[]): RpcJ
|
|
|
136
157
|
selected: selectedWithOther,
|
|
137
158
|
other: { const: true },
|
|
138
159
|
custom: { type: "string", minLength: 1 },
|
|
160
|
+
action: { const: "answer" },
|
|
139
161
|
},
|
|
140
162
|
required: ["selected", "other", "custom"],
|
|
141
163
|
additionalProperties: false,
|
|
142
164
|
},
|
|
165
|
+
{
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
action: { const: "clarify" },
|
|
169
|
+
question: { type: "string", minLength: 1 },
|
|
170
|
+
},
|
|
171
|
+
required: ["action", "question"],
|
|
172
|
+
additionalProperties: false,
|
|
173
|
+
},
|
|
143
174
|
],
|
|
144
175
|
};
|
|
145
176
|
}
|
|
@@ -157,13 +188,13 @@ function structuredDeepInterviewState(meta: AskGateDeepInterviewState): Record<s
|
|
|
157
188
|
return state;
|
|
158
189
|
}
|
|
159
190
|
|
|
160
|
-
/** Build the `workflow_gate` open-input for one
|
|
191
|
+
/** Build the `workflow_gate` open-input for one ask question. */
|
|
161
192
|
export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
162
193
|
const labels = question.options.map(o => o.label);
|
|
163
194
|
const schema = questionAnswerSchema(question, labels);
|
|
164
195
|
return {
|
|
165
|
-
stage: "deep-interview",
|
|
166
|
-
kind: "question",
|
|
196
|
+
stage: question.workflowGate?.stage ?? "deep-interview",
|
|
197
|
+
kind: question.workflowGate?.kind ?? "question",
|
|
167
198
|
schema,
|
|
168
199
|
options: question.options.map((o, i) => ({
|
|
169
200
|
value: o.label,
|
|
@@ -178,6 +209,7 @@ export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
|
178
209
|
multi: question.multi ?? false,
|
|
179
210
|
options: labels,
|
|
180
211
|
other_option: GATE_OTHER_OPTION,
|
|
212
|
+
clarification_action: "clarify",
|
|
181
213
|
...(question.deepInterview
|
|
182
214
|
? structuredDeepInterviewState(question.deepInterview)
|
|
183
215
|
: deepInterviewQuestionState(question.question)),
|
|
@@ -188,8 +220,14 @@ export function questionToGate(question: AskGateQuestion): OpenGateInput {
|
|
|
188
220
|
|
|
189
221
|
function isAnswer(value: unknown): value is DeepInterviewGateAnswer {
|
|
190
222
|
if (typeof value !== "object" || value === null) return false;
|
|
191
|
-
const v = value as
|
|
223
|
+
const v = value as Record<string, unknown>;
|
|
224
|
+
if (v.action === "clarify") {
|
|
225
|
+
return (
|
|
226
|
+
typeof v.question === "string" && v.selected === undefined && v.other === undefined && v.custom === undefined
|
|
227
|
+
);
|
|
228
|
+
}
|
|
192
229
|
return (
|
|
230
|
+
(v.action === undefined || v.action === "answer") &&
|
|
193
231
|
Array.isArray(v.selected) &&
|
|
194
232
|
v.selected.every(s => typeof s === "string") &&
|
|
195
233
|
(v.other === undefined || typeof v.other === "boolean") &&
|
|
@@ -207,11 +245,24 @@ export function gateAnswerToResult(question: AskGateQuestion, answer: unknown):
|
|
|
207
245
|
if (!isAnswer(answer)) {
|
|
208
246
|
throw new DeepInterviewGateError(
|
|
209
247
|
"invalid_answer_shape",
|
|
210
|
-
|
|
248
|
+
'answer must be an answer ({ selected: string[]; other?: boolean; custom?: string }) or clarification ({ action: "clarify"; question: string })',
|
|
211
249
|
);
|
|
212
250
|
}
|
|
213
251
|
const labels = question.options.map(o => o.label);
|
|
214
252
|
const multi = question.multi ?? false;
|
|
253
|
+
if (answer.action === "clarify") {
|
|
254
|
+
if (answer.question.trim() === "") {
|
|
255
|
+
throw new DeepInterviewGateError("missing_clarification", "clarification question is required");
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
id: question.id,
|
|
259
|
+
question: question.question,
|
|
260
|
+
options: labels,
|
|
261
|
+
multi,
|
|
262
|
+
selectedOptions: [],
|
|
263
|
+
clarificationQuestion: answer.question,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
215
266
|
const valid = new Set(labels);
|
|
216
267
|
for (const sel of answer.selected) {
|
|
217
268
|
if (!valid.has(sel)) throw new DeepInterviewGateError("unknown_option", `unknown option: ${sel}`);
|
package/src/modes/types.ts
CHANGED
|
@@ -290,7 +290,12 @@ export interface InteractiveModeContext {
|
|
|
290
290
|
dialogOptions?: ExtensionUIDialogOptions,
|
|
291
291
|
): Promise<string | undefined>;
|
|
292
292
|
hideHookSelector(): void;
|
|
293
|
-
showHookInput(
|
|
293
|
+
showHookInput(
|
|
294
|
+
title: string,
|
|
295
|
+
placeholder?: string,
|
|
296
|
+
dialogOptions?: ExtensionUIDialogOptions,
|
|
297
|
+
inputOptions?: { readonly initialValue?: string },
|
|
298
|
+
): Promise<string | undefined>;
|
|
294
299
|
hideHookInput(): void;
|
|
295
300
|
showHookEditor(
|
|
296
301
|
title: string,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface DaemonPaths {
|
|
4
|
+
dir: string;
|
|
5
|
+
lock: string;
|
|
6
|
+
state: string;
|
|
7
|
+
roots: string;
|
|
8
|
+
steal: string;
|
|
9
|
+
aliases: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function daemonPaths(agentDir: string): DaemonPaths {
|
|
13
|
+
const dir = path.join(agentDir, "notifications");
|
|
14
|
+
return {
|
|
15
|
+
dir,
|
|
16
|
+
lock: path.join(dir, "telegram-daemon.lock"),
|
|
17
|
+
state: path.join(dir, "telegram-daemon.state.json"),
|
|
18
|
+
roots: path.join(dir, "telegram-daemon.roots.json"),
|
|
19
|
+
steal: path.join(dir, "telegram-daemon.steal"),
|
|
20
|
+
aliases: path.join(dir, "telegram-callback-aliases.json"),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -248,6 +248,8 @@ interface Token {
|
|
|
248
248
|
value: string;
|
|
249
249
|
/** Tag name if this token opens a tag, else undefined. */
|
|
250
250
|
open?: string;
|
|
251
|
+
/** Exact opening tag text, including attributes, for safe chunk reopening. */
|
|
252
|
+
openTag?: string;
|
|
251
253
|
/** Tag name if this token closes a tag, else undefined. */
|
|
252
254
|
close?: string;
|
|
253
255
|
}
|
|
@@ -266,8 +268,10 @@ function tokenize(html: string): Token[] {
|
|
|
266
268
|
const openMatch = /^<([a-z-]+)(?:\s[^>]*)?>$/i.exec(raw);
|
|
267
269
|
const token: Token = { value: raw };
|
|
268
270
|
if (close && ALLOWED_TAGS.has(close[1]!.toLowerCase())) token.close = close[1]!.toLowerCase();
|
|
269
|
-
else if (openMatch && ALLOWED_TAGS.has(openMatch[1]!.toLowerCase()))
|
|
271
|
+
else if (openMatch && ALLOWED_TAGS.has(openMatch[1]!.toLowerCase())) {
|
|
270
272
|
token.open = openMatch[1]!.toLowerCase();
|
|
273
|
+
token.openTag = raw;
|
|
274
|
+
}
|
|
271
275
|
tokens.push(token);
|
|
272
276
|
i = end + 1;
|
|
273
277
|
continue;
|
|
@@ -330,6 +334,76 @@ export function truncateTelegramHtml(message: string, max = TELEGRAM_MESSAGE_LIM
|
|
|
330
334
|
return out + closersFor(stack) + effectiveMarker;
|
|
331
335
|
}
|
|
332
336
|
|
|
337
|
+
interface OpenTag {
|
|
338
|
+
name: string;
|
|
339
|
+
tag: string;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Split a finished Telegram HTML message into ordered chunks without splitting tags or entities. */
|
|
343
|
+
export function splitTelegramHtml(message: string, max = TELEGRAM_MESSAGE_LIMIT): string[] {
|
|
344
|
+
if (message.length <= max) return [message];
|
|
345
|
+
const chunks: string[] = [];
|
|
346
|
+
const tokens = tokenize(message);
|
|
347
|
+
const stack: OpenTag[] = [];
|
|
348
|
+
let out = "";
|
|
349
|
+
let chunkHasBody = false;
|
|
350
|
+
|
|
351
|
+
const closersFor = (s: OpenTag[]): string =>
|
|
352
|
+
s
|
|
353
|
+
.map(t => `</${t.name}>`)
|
|
354
|
+
.reverse()
|
|
355
|
+
.join("");
|
|
356
|
+
const openersFor = (s: OpenTag[]): string => s.map(t => t.tag).join("");
|
|
357
|
+
const minimumChunkLengthFor = (s: OpenTag[]): number => openersFor(s).length + closersFor(s).length + 1;
|
|
358
|
+
const flush = (): void => {
|
|
359
|
+
if (!chunkHasBody) return;
|
|
360
|
+
chunks.push(out + closersFor(stack));
|
|
361
|
+
out = openersFor(stack);
|
|
362
|
+
chunkHasBody = false;
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
const updateStackForClose = (name: string): boolean => {
|
|
366
|
+
const idx = stack.findLastIndex(t => t.name === name);
|
|
367
|
+
if (idx === -1) return false;
|
|
368
|
+
stack.splice(idx, 1);
|
|
369
|
+
return true;
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
for (const token of tokens) {
|
|
373
|
+
if (token.open) {
|
|
374
|
+
const nextStack = [...stack, { name: token.open, tag: token.openTag ?? token.value }];
|
|
375
|
+
if (minimumChunkLengthFor(nextStack) > max) continue;
|
|
376
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(nextStack).length > max) flush();
|
|
377
|
+
if (out.length + token.value.length + closersFor(nextStack).length > max) continue;
|
|
378
|
+
out += token.value;
|
|
379
|
+
chunkHasBody = true;
|
|
380
|
+
stack.push({ name: token.open, tag: token.openTag ?? token.value });
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (token.close) {
|
|
385
|
+
const idx = stack.findLastIndex(t => t.name === token.close);
|
|
386
|
+
if (idx === -1) continue;
|
|
387
|
+
const nextStack = stack.toSpliced(idx, 1);
|
|
388
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(nextStack).length > max) flush();
|
|
389
|
+
if (out.length + token.value.length + closersFor(nextStack).length > max) {
|
|
390
|
+
updateStackForClose(token.close);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
out += token.value;
|
|
394
|
+
chunkHasBody = true;
|
|
395
|
+
updateStackForClose(token.close);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (chunkHasBody && out.length + token.value.length + closersFor(stack).length > max) flush();
|
|
400
|
+
out += token.value;
|
|
401
|
+
chunkHasBody = true;
|
|
402
|
+
}
|
|
403
|
+
if (chunkHasBody) chunks.push(out + closersFor(stack));
|
|
404
|
+
return chunks;
|
|
405
|
+
}
|
|
406
|
+
|
|
333
407
|
/** Finalize an optional message: undefined passthrough, else safe-truncate. */
|
|
334
408
|
export function finalizeTelegramHtml(message?: string): string | undefined {
|
|
335
409
|
if (message === undefined) return undefined;
|
|
@@ -297,6 +297,15 @@ function buildIdentity(
|
|
|
297
297
|
return { repo, branch, machine: os.hostname(), title: sessionName };
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/** Compact cwd label for remote session identity; never emits the full host path by default. */
|
|
301
|
+
function compactCwd(cwd: string): string | undefined {
|
|
302
|
+
const home = os.homedir();
|
|
303
|
+
const resolved = path.resolve(cwd);
|
|
304
|
+
if (resolved === home) return "~";
|
|
305
|
+
const base = path.basename(resolved);
|
|
306
|
+
return base || path.parse(resolved).root || undefined;
|
|
307
|
+
}
|
|
308
|
+
|
|
300
309
|
const execFileAsync = promisify(execFile);
|
|
301
310
|
|
|
302
311
|
/** Best-effort working-tree diff stat for the context update (no throw). */
|
|
@@ -973,7 +982,8 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
973
982
|
const tokenUsage = usage && usage.tokens != null ? `${usage.tokens}/${usage.contextWindow}` : undefined;
|
|
974
983
|
const modelId = model?.id;
|
|
975
984
|
void readGitDiffStat(ctx.cwd).then(diff => {
|
|
976
|
-
|
|
985
|
+
const cwd = compactCwd(ctx.cwd);
|
|
986
|
+
if (!diff && !tokenUsage && !modelId && !cwd) return;
|
|
977
987
|
try {
|
|
978
988
|
rt.server.pushFrame(
|
|
979
989
|
JSON.stringify({
|
|
@@ -982,6 +992,7 @@ export const createNotificationsExtension: ExtensionFactory = api => {
|
|
|
982
992
|
tokenUsage,
|
|
983
993
|
model: modelId,
|
|
984
994
|
diff,
|
|
995
|
+
cwd,
|
|
985
996
|
}),
|
|
986
997
|
);
|
|
987
998
|
} catch (e) {
|
|
@@ -25,6 +25,8 @@ export interface RecentSessionEntry {
|
|
|
25
25
|
mtimeMs: number;
|
|
26
26
|
/** True when a terminal breadcrumb points at this session file. */
|
|
27
27
|
currentTerminal?: boolean;
|
|
28
|
+
/** True when this history is an internal helper/sub-agent session. */
|
|
29
|
+
internal?: boolean;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
export interface RecentActivityDeps {
|
|
@@ -34,17 +36,18 @@ export interface RecentActivityDeps {
|
|
|
34
36
|
breadcrumbPaths?: string[];
|
|
35
37
|
/** Max entries to return (default 20). */
|
|
36
38
|
limit?: number;
|
|
39
|
+
/** Include internal helper/sub-agent sessions (default true). */
|
|
40
|
+
includeInternal?: boolean;
|
|
37
41
|
/** Injection seam for tests. */
|
|
38
|
-
|
|
42
|
+
readInitialLines?: (file: string, maxLines: number) => string[];
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
function
|
|
45
|
+
function defaultReadInitialLines(file: string, maxLines: number): string[] {
|
|
42
46
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
return nl === -1 ? buf : buf.slice(0, nl);
|
|
47
|
+
const lines = fs.readFileSync(file, "utf8").split("\n");
|
|
48
|
+
return lines.slice(0, maxLines);
|
|
46
49
|
} catch {
|
|
47
|
-
return
|
|
50
|
+
return [];
|
|
48
51
|
}
|
|
49
52
|
}
|
|
50
53
|
|
|
@@ -65,6 +68,22 @@ function headerMeta(line: string | undefined): { id?: string; path?: string; bra
|
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
|
|
71
|
+
/** Detect task-tool helper sessions from the durable early session_init metadata entry. */
|
|
72
|
+
function isInternalSession(lines: readonly string[]): boolean {
|
|
73
|
+
for (const line of lines.slice(1)) {
|
|
74
|
+
if (!line.trim()) continue;
|
|
75
|
+
try {
|
|
76
|
+
const obj = JSON.parse(line) as unknown;
|
|
77
|
+
if (typeof obj === "object" && obj !== null && (obj as { type?: unknown }).type === "session_init") {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore malformed JSONL entries; classification is best-effort.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
68
87
|
/**
|
|
69
88
|
* The authoritative session id for a history file: the header `id` when present,
|
|
70
89
|
* else the filename stem with a leading `<timestamp>_` prefix stripped (matching
|
|
@@ -85,7 +104,8 @@ function sessionIdForFile(stem: string, headerId: string | undefined): string {
|
|
|
85
104
|
*/
|
|
86
105
|
export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry[] {
|
|
87
106
|
const limit = deps.limit ?? 20;
|
|
88
|
-
const
|
|
107
|
+
const includeInternal = deps.includeInternal ?? true;
|
|
108
|
+
const readInitialLines = deps.readInitialLines ?? defaultReadInitialLines;
|
|
89
109
|
const breadcrumbs = new Set((deps.breadcrumbPaths ?? []).map(p => path.resolve(p)));
|
|
90
110
|
|
|
91
111
|
let projectDirs: string[];
|
|
@@ -114,7 +134,10 @@ export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry
|
|
|
114
134
|
} catch {
|
|
115
135
|
continue;
|
|
116
136
|
}
|
|
117
|
-
const
|
|
137
|
+
const initialLines = readInitialLines(file, 8);
|
|
138
|
+
const meta = headerMeta(initialLines[0]);
|
|
139
|
+
const internal = isInternalSession(initialLines);
|
|
140
|
+
if (internal && !includeInternal) continue;
|
|
118
141
|
entries.push({
|
|
119
142
|
sessionId: sessionIdForFile(name.slice(0, -".jsonl".length), meta.id),
|
|
120
143
|
path: meta.path,
|
|
@@ -123,6 +146,7 @@ export function listRecentSessions(deps: RecentActivityDeps): RecentSessionEntry
|
|
|
123
146
|
sessionStateFile: file,
|
|
124
147
|
mtimeMs,
|
|
125
148
|
currentTerminal: breadcrumbs.has(path.resolve(file)) || undefined,
|
|
149
|
+
internal: internal || undefined,
|
|
126
150
|
});
|
|
127
151
|
}
|
|
128
152
|
}
|
|
@@ -3,12 +3,19 @@ import * as path from "node:path";
|
|
|
3
3
|
import { YAML } from "bun";
|
|
4
4
|
import type { Settings } from "../config/settings";
|
|
5
5
|
import { getNotificationConfig, isGloballyConfigured } from "./config";
|
|
6
|
-
import { daemonPaths
|
|
7
|
-
import {
|
|
6
|
+
import { daemonPaths } from "./daemon-paths";
|
|
7
|
+
import type { TelegramDaemonOptions } from "./telegram-daemon";
|
|
8
|
+
|
|
9
|
+
type TelegramDaemonRunner = {
|
|
10
|
+
run(): Promise<void>;
|
|
11
|
+
requestStop(reason?: "reload" | "signal" | "stop"): void;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type TelegramDaemonConstructor = new (opts: TelegramDaemonOptions) => TelegramDaemonRunner;
|
|
8
15
|
|
|
9
16
|
export interface RunDaemonInternalDeps {
|
|
10
17
|
SettingsImpl?: { init: (options?: { agentDir?: string }) => Promise<Pick<Settings, "get" | "getAgentDir">> };
|
|
11
|
-
DaemonImpl?:
|
|
18
|
+
DaemonImpl?: TelegramDaemonConstructor;
|
|
12
19
|
processPid?: number;
|
|
13
20
|
pidAlive?: (pid: number) => boolean;
|
|
14
21
|
}
|
|
@@ -140,7 +147,9 @@ export async function runDaemonInternal(argv: string[], deps: RunDaemonInternalD
|
|
|
140
147
|
const settings = await resolveDaemonSettings(resolvedAgentDir, deps);
|
|
141
148
|
const cfg = getNotificationConfig(settings as Settings);
|
|
142
149
|
if (!isGloballyConfigured(cfg) || !cfg.botToken || !cfg.chatId) return;
|
|
143
|
-
const
|
|
150
|
+
const { clearTelegramControlRequest, readTelegramControlRequest } = await import("./telegram-daemon-control");
|
|
151
|
+
const Daemon: TelegramDaemonConstructor =
|
|
152
|
+
deps.DaemonImpl ?? (await import("./telegram-daemon")).TelegramNotificationDaemon;
|
|
144
153
|
const daemon = new Daemon({
|
|
145
154
|
settings: settings as Settings,
|
|
146
155
|
ownerId,
|