@gajae-code/coding-agent 0.11.6 → 0.11.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +56 -6
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +25 -4
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +57 -6
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +8 -7
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +86 -27
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/index.ts +4 -5
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -3
- package/src/sdk/bus/telegram-daemon-control.ts +2 -2
- package/src/sdk/bus/telegram-daemon.ts +1663 -375
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +240 -149
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +319 -85
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +1 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -169,6 +169,26 @@ async function snapshotDirectory(directoryPath: string, relativePath: string): P
|
|
|
169
169
|
return { relativePath, dev: stat.dev, ino: stat.ino, size: stat.size };
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
+
interface CanonicalLegacyRoot {
|
|
173
|
+
readonly root: string;
|
|
174
|
+
readonly identity: LegacyEntrySnapshot;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function canonicalLegacyRoot(artifactsDir: string): Promise<CanonicalLegacyRoot> {
|
|
178
|
+
const lexicalRoot = path.resolve(artifactsDir, "local");
|
|
179
|
+
const lexicalIdentity = await snapshotDirectory(lexicalRoot, "");
|
|
180
|
+
const [canonicalArtifactsDir, canonicalRoot] = await Promise.all([
|
|
181
|
+
fs.realpath(path.resolve(artifactsDir)),
|
|
182
|
+
fs.realpath(lexicalRoot),
|
|
183
|
+
]);
|
|
184
|
+
if (canonicalRoot === canonicalArtifactsDir) throw new Error("Unsafe legacy local:// migration source");
|
|
185
|
+
ensureWithinRoot(canonicalRoot, canonicalArtifactsDir);
|
|
186
|
+
const canonicalIdentity = await snapshotDirectory(canonicalRoot, "");
|
|
187
|
+
if (!matchesSnapshot(canonicalIdentity, lexicalIdentity))
|
|
188
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
189
|
+
return { root: canonicalRoot, identity: lexicalIdentity };
|
|
190
|
+
}
|
|
191
|
+
|
|
172
192
|
async function snapshotRegularFile(filePath: string, relativePath: string): Promise<LegacyEntrySnapshot> {
|
|
173
193
|
const before = await fs.lstat(filePath, { bigint: true });
|
|
174
194
|
if (!before.isFile() || before.isSymbolicLink()) throw new Error("Unsafe legacy local:// migration source");
|
|
@@ -202,11 +222,16 @@ function manifestsMatch(left: readonly LegacyEntrySnapshot[], right: readonly Le
|
|
|
202
222
|
);
|
|
203
223
|
}
|
|
204
224
|
|
|
205
|
-
async function captureLegacyManifest(
|
|
225
|
+
async function captureLegacyManifest(
|
|
226
|
+
root: string,
|
|
227
|
+
expectedRootIdentity: LegacyEntrySnapshot,
|
|
228
|
+
): Promise<readonly LegacyEntrySnapshot[]> {
|
|
206
229
|
const manifest: LegacyEntrySnapshot[] = [];
|
|
207
230
|
let copiedBytes = 0n;
|
|
208
231
|
const captureDirectory = async (directoryPath: string, relativePath: string): Promise<void> => {
|
|
209
232
|
const directory = await snapshotDirectory(directoryPath, relativePath);
|
|
233
|
+
if (relativePath === "" && !matchesSnapshot(directory, expectedRootIdentity))
|
|
234
|
+
throw new Error("Legacy local:// migration source changed during capture");
|
|
210
235
|
manifest.push(directory);
|
|
211
236
|
const entries = (await fs.readdir(directoryPath, { withFileTypes: true })).sort((a, b) =>
|
|
212
237
|
a.name.localeCompare(b.name),
|
|
@@ -384,10 +409,11 @@ async function migrateLegacyLocal(
|
|
|
384
409
|
await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
|
|
385
410
|
return;
|
|
386
411
|
}
|
|
387
|
-
|
|
412
|
+
let legacySource: CanonicalLegacyRoot;
|
|
388
413
|
let manifest: readonly LegacyEntrySnapshot[];
|
|
389
414
|
try {
|
|
390
|
-
|
|
415
|
+
legacySource = await canonicalLegacyRoot(artifactsDir);
|
|
416
|
+
manifest = await captureLegacyManifest(legacySource.root, legacySource.identity);
|
|
391
417
|
} catch (error) {
|
|
392
418
|
if (isEnoent(error)) {
|
|
393
419
|
await fs.writeFile(marker, "absent\n", { mode: 0o600, flag: "wx" });
|
|
@@ -397,8 +423,8 @@ async function migrateLegacyLocal(
|
|
|
397
423
|
}
|
|
398
424
|
const staging = path.join(scratchParent, `.gjc-local-migration-${randomUUID()}`);
|
|
399
425
|
try {
|
|
400
|
-
await copyLegacyManifest(
|
|
401
|
-
const verifiedManifest = await captureLegacyManifest(
|
|
426
|
+
await copyLegacyManifest(legacySource.root, staging, manifest);
|
|
427
|
+
const verifiedManifest = await captureLegacyManifest(legacySource.root, legacySource.identity);
|
|
402
428
|
if (!manifestsMatch(verifiedManifest, manifest))
|
|
403
429
|
throw new Error("Legacy local:// migration source changed during capture");
|
|
404
430
|
for (const entry of await fs.readdir(staging)) {
|
|
@@ -410,7 +436,7 @@ async function migrateLegacyLocal(
|
|
|
410
436
|
}
|
|
411
437
|
await fs.rename(path.join(staging, entry), path.join(localRoot, entry));
|
|
412
438
|
}
|
|
413
|
-
await retireLegacyTree(
|
|
439
|
+
await retireLegacyTree(legacySource.root, manifest);
|
|
414
440
|
await fs.writeFile(marker, "verified\n", { mode: 0o600, flag: "wx" });
|
|
415
441
|
} finally {
|
|
416
442
|
await fs.rm(staging, { recursive: true, force: true });
|
package/src/main.ts
CHANGED
|
@@ -12,6 +12,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
12
12
|
import type { ImageContent } from "@gajae-code/ai";
|
|
13
13
|
import {
|
|
14
14
|
$env,
|
|
15
|
+
$pickenv,
|
|
16
|
+
getAgentDir,
|
|
15
17
|
getProjectDir,
|
|
16
18
|
logger,
|
|
17
19
|
normalizePathForComparison,
|
|
@@ -534,7 +536,16 @@ interface InteractiveModeFactoryOptions {
|
|
|
534
536
|
type CreateInteractiveMode = (options: InteractiveModeFactoryOptions) => InteractiveMode;
|
|
535
537
|
|
|
536
538
|
type ResumePickerTerminalCheck = () => boolean;
|
|
537
|
-
type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string) => Promise<SessionInfo[]>;
|
|
539
|
+
type ListForResumePickerReadOnly = (cwd: string, sessionDir?: string, storage?: undefined) => Promise<SessionInfo[]>;
|
|
540
|
+
|
|
541
|
+
type ListManagedForResumePickerReadOnly = (
|
|
542
|
+
cwd: string,
|
|
543
|
+
managedAgentDir?: string,
|
|
544
|
+
storage?: undefined,
|
|
545
|
+
) => Promise<SessionInfo[]>;
|
|
546
|
+
|
|
547
|
+
type ResolveManagedAgentDirForScope = (cwd: string) => string;
|
|
548
|
+
|
|
538
549
|
type SelectResumeSession = (sessions: SessionInfo[]) => Promise<SessionSelectionResult>;
|
|
539
550
|
type OpenExistingSessionStrict = (
|
|
540
551
|
identity: ResumeSessionIdentity,
|
|
@@ -546,6 +557,11 @@ type OpenExistingSessionStrict = (
|
|
|
546
557
|
export const BARE_RESUME_CONFLICT_ERROR =
|
|
547
558
|
"--resume without a session cannot be combined with --continue, --fork, or --no-session.";
|
|
548
559
|
export const BARE_RESUME_INTERACTIVE_ERROR = "--resume requires an interactive terminal; use --resume <id>.";
|
|
560
|
+
|
|
561
|
+
/** Resolves only the managed agent root needed for pre-consent picker inventory. */
|
|
562
|
+
export function resolveManagedAgentDirForScope(_cwd: string): string {
|
|
563
|
+
return getAgentDir();
|
|
564
|
+
}
|
|
549
565
|
export const BARE_RESUME_OPEN_ERROR = "Could not open the selected session. Use --resume <id>.";
|
|
550
566
|
|
|
551
567
|
function isBareResume(parsed: Args): boolean {
|
|
@@ -741,7 +757,13 @@ export async function createSessionManager(
|
|
|
741
757
|
if (forkSource.includes("/") || forkSource.includes("\\") || forkSource.endsWith(".jsonl")) {
|
|
742
758
|
return await SessionManager.forkFrom(forkSource, cwd, sessionDestination(), undefined, migrationPolicy);
|
|
743
759
|
}
|
|
744
|
-
const match = await resolveResumableSession(
|
|
760
|
+
const match = await resolveResumableSession(
|
|
761
|
+
forkSource,
|
|
762
|
+
cwd,
|
|
763
|
+
parsed.sessionDir,
|
|
764
|
+
undefined,
|
|
765
|
+
parsed.sessionDir ? undefined : activeSettings.getAgentDir(),
|
|
766
|
+
);
|
|
745
767
|
if (!match) {
|
|
746
768
|
throw new Error(`Session "${forkSource}" not found.`);
|
|
747
769
|
}
|
|
@@ -754,14 +776,18 @@ export async function createSessionManager(
|
|
|
754
776
|
if (typeof parsed.resume === "string") {
|
|
755
777
|
const sessionArg = parsed.resume;
|
|
756
778
|
if (sessionArg.includes("/") || sessionArg.includes("\\") || sessionArg.endsWith(".jsonl")) {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
SessionManager.explicitDestination(path.dirname(sessionArg))
|
|
760
|
-
|
|
761
|
-
migrationPolicy,
|
|
762
|
-
);
|
|
779
|
+
const destination = parsed.sessionDir
|
|
780
|
+
? SessionManager.explicitDestination(parsed.sessionDir)
|
|
781
|
+
: SessionManager.explicitDestination(path.dirname(sessionArg));
|
|
782
|
+
return await SessionManager.open(sessionArg, destination, undefined, migrationPolicy);
|
|
763
783
|
}
|
|
764
|
-
const match = await resolveResumableSession(
|
|
784
|
+
const match = await resolveResumableSession(
|
|
785
|
+
sessionArg,
|
|
786
|
+
cwd,
|
|
787
|
+
parsed.sessionDir,
|
|
788
|
+
undefined,
|
|
789
|
+
parsed.sessionDir ? undefined : activeSettings.getAgentDir(),
|
|
790
|
+
);
|
|
765
791
|
if (!match) {
|
|
766
792
|
throw new Error(`Session "${sessionArg}" not found.`);
|
|
767
793
|
}
|
|
@@ -1058,12 +1084,41 @@ export interface RunRootCommandDependencies {
|
|
|
1058
1084
|
runPrintMode?: RunPrintMode;
|
|
1059
1085
|
isResumePickerTerminal?: ResumePickerTerminalCheck;
|
|
1060
1086
|
listForResumePickerReadOnly?: ListForResumePickerReadOnly;
|
|
1087
|
+
listManagedForResumePickerReadOnly?: ListManagedForResumePickerReadOnly;
|
|
1088
|
+
resolveManagedAgentDirForScope?: ResolveManagedAgentDirForScope;
|
|
1061
1089
|
selectResumeSession?: SelectResumeSession;
|
|
1062
1090
|
openExistingSessionStrict?: OpenExistingSessionStrict;
|
|
1063
1091
|
initializeSettings?: typeof Settings.init;
|
|
1064
1092
|
loadSettingsForScope?: typeof Settings.loadForScope;
|
|
1065
1093
|
}
|
|
1066
1094
|
|
|
1095
|
+
export interface ModelRoleOverrides {
|
|
1096
|
+
smol?: string;
|
|
1097
|
+
slow?: string;
|
|
1098
|
+
plan?: string;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
/**
|
|
1102
|
+
* Resolve the ephemeral `smol`/`slow`/`plan` model-role overrides.
|
|
1103
|
+
*
|
|
1104
|
+
* Precedence per role is CLI flag > documented `GJC_*_MODEL` > legacy
|
|
1105
|
+
* `PI_*_MODEL`, matching the repo-wide GJC-first/PI-fallback convention.
|
|
1106
|
+
* Resolution reads the process environment via `$pickenv`, which trims values
|
|
1107
|
+
* and treats empty/whitespace as unset; it is deliberately kept separate from
|
|
1108
|
+
* credential env resolution (`$credentialEnv`/`$pickCredentialEnv`). The
|
|
1109
|
+
* function is pure and stateless, so it reads fresh each call and a later
|
|
1110
|
+
* invocation never inherits an earlier one's values.
|
|
1111
|
+
*/
|
|
1112
|
+
export function resolveModelRoleOverrides(parsed: Pick<Args, "smol" | "slow" | "plan">): ModelRoleOverrides {
|
|
1113
|
+
const overrides: ModelRoleOverrides = {};
|
|
1114
|
+
const smol = parsed.smol ?? $pickenv("GJC_SMOL_MODEL", "PI_SMOL_MODEL");
|
|
1115
|
+
const slow = parsed.slow ?? $pickenv("GJC_SLOW_MODEL", "PI_SLOW_MODEL");
|
|
1116
|
+
const plan = parsed.plan ?? $pickenv("GJC_PLAN_MODEL", "PI_PLAN_MODEL");
|
|
1117
|
+
if (smol) overrides.smol = smol;
|
|
1118
|
+
if (slow) overrides.slow = slow;
|
|
1119
|
+
if (plan) overrides.plan = plan;
|
|
1120
|
+
return overrides;
|
|
1121
|
+
}
|
|
1067
1122
|
export async function runRootCommand(
|
|
1068
1123
|
parsed: Args,
|
|
1069
1124
|
rawArgs: string[],
|
|
@@ -1094,10 +1149,18 @@ export async function runRootCommand(
|
|
|
1094
1149
|
await logger.time("maybeAutoChdir", maybeAutoChdir, parsedArgs);
|
|
1095
1150
|
autoChdirApplied = true;
|
|
1096
1151
|
const resumeCwd = getProjectDir();
|
|
1097
|
-
const
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1152
|
+
const managedAgentDir = parsedArgs.sessionDir
|
|
1153
|
+
? undefined
|
|
1154
|
+
: (deps.resolveManagedAgentDirForScope ?? resolveManagedAgentDirForScope)(resumeCwd);
|
|
1155
|
+
const sessions = parsedArgs.sessionDir
|
|
1156
|
+
? await (deps.listForResumePickerReadOnly ?? SessionManager.listForResumePickerReadOnly)(
|
|
1157
|
+
resumeCwd,
|
|
1158
|
+
parsedArgs.sessionDir,
|
|
1159
|
+
)
|
|
1160
|
+
: await (deps.listManagedForResumePickerReadOnly ?? SessionManager.listManagedForResumePickerReadOnly)(
|
|
1161
|
+
resumeCwd,
|
|
1162
|
+
managedAgentDir,
|
|
1163
|
+
);
|
|
1101
1164
|
if (sessions.length === 0) {
|
|
1102
1165
|
process.stdout.write(`${chalk.dim("No sessions found")}\n`);
|
|
1103
1166
|
return;
|
|
@@ -1108,19 +1171,16 @@ export async function runRootCommand(
|
|
|
1108
1171
|
if (selection.kind === "cancelled") {
|
|
1109
1172
|
return;
|
|
1110
1173
|
}
|
|
1174
|
+
const scopedSettings = await (deps.loadSettingsForScope ?? Settings.loadForScope)({ cwd: resumeCwd });
|
|
1111
1175
|
const resumeMigrationPolicy =
|
|
1112
|
-
(
|
|
1113
|
-
"session.directoryMigration",
|
|
1114
|
-
) === "disabled"
|
|
1115
|
-
? "disabled"
|
|
1116
|
-
: "copy-retain";
|
|
1176
|
+
scopedSettings.get("session.directoryMigration") === "disabled" ? "disabled" : "copy-retain";
|
|
1117
1177
|
let opened: StrictSessionOpenResult;
|
|
1118
1178
|
try {
|
|
1119
1179
|
opened = await (deps.openExistingSessionStrict ?? SessionManager.openExistingStrict)(
|
|
1120
1180
|
selection.identity,
|
|
1121
1181
|
parsedArgs.sessionDir
|
|
1122
1182
|
? SessionManager.explicitDestination(parsedArgs.sessionDir)
|
|
1123
|
-
: SessionManager.managedDestination(resumeCwd,
|
|
1183
|
+
: SessionManager.managedDestination(resumeCwd, scopedSettings.getAgentDir()),
|
|
1124
1184
|
undefined,
|
|
1125
1185
|
resumeMigrationPolicy,
|
|
1126
1186
|
);
|
|
@@ -1252,15 +1312,14 @@ export async function runRootCommand(
|
|
|
1252
1312
|
// Initialize discovery system with settings for provider persistence
|
|
1253
1313
|
logger.time("initializeWithSettings", initializeWithSettings, settingsInstance);
|
|
1254
1314
|
|
|
1255
|
-
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted)
|
|
1256
|
-
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
if (smolModel || slowModel || planModel) {
|
|
1315
|
+
// Apply model role overrides from CLI args or env vars (ephemeral, not persisted).
|
|
1316
|
+
// Precedence per role: CLI flag > documented GJC_*_MODEL > legacy PI_*_MODEL.
|
|
1317
|
+
const roleOverrides = resolveModelRoleOverrides(parsedArgs);
|
|
1318
|
+
if (roleOverrides.smol || roleOverrides.slow || roleOverrides.plan) {
|
|
1260
1319
|
settingsInstance.overrideModelRoles({
|
|
1261
|
-
smol:
|
|
1262
|
-
slow:
|
|
1263
|
-
plan:
|
|
1320
|
+
...(roleOverrides.smol ? { smol: roleOverrides.smol } : {}),
|
|
1321
|
+
...(roleOverrides.slow ? { slow: roleOverrides.slow } : {}),
|
|
1322
|
+
...(roleOverrides.plan ? { plan: roleOverrides.plan } : {}),
|
|
1264
1323
|
});
|
|
1265
1324
|
}
|
|
1266
1325
|
|
|
@@ -49,7 +49,7 @@ export const APP_ACTION_METADATA: readonly ActionMetadata[] = [
|
|
|
49
49
|
action("app.clipboard.copyPrompt", "Copy prompt", "Clipboard", ["composer"]),
|
|
50
50
|
action("app.session.new", "New session", "Session", ["composer"]),
|
|
51
51
|
action("app.session.tree", "Session tree", "Session", ["composer"]),
|
|
52
|
-
action("app.session.fork", "
|
|
52
|
+
action("app.session.fork", "Branch from message", "Session", ["composer"]),
|
|
53
53
|
action("app.session.resume", "Resume session", "Session", ["composer"]),
|
|
54
54
|
action("app.session.observe", "Observe sessions", "Session", ["composer"]),
|
|
55
55
|
action("app.session.dashboard", "Show sessions dashboard", "Session", ["composer"]),
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { type Component, Container, Markdown, Spacer, Text, type TUI } from "@gajae-code/tui";
|
|
2
|
+
import {
|
|
3
|
+
BTW_MAX_ANSWER_UTF8_BYTES,
|
|
4
|
+
BTW_MAX_CONTEXT_TURNS,
|
|
5
|
+
sanitizeBtwError,
|
|
6
|
+
truncateUtf8,
|
|
7
|
+
} from "../../session/btw-contract";
|
|
2
8
|
import { replaceTabs } from "../../tools/render-utils";
|
|
3
9
|
import { getMarkdownTheme, theme } from "../theme/theme";
|
|
4
10
|
import { DynamicBorder } from "./dynamic-border";
|
|
@@ -10,12 +16,21 @@ interface BtwPanelComponentOptions {
|
|
|
10
16
|
tui: TUI;
|
|
11
17
|
}
|
|
12
18
|
|
|
19
|
+
interface BtwTurn {
|
|
20
|
+
question: string;
|
|
21
|
+
answer: string;
|
|
22
|
+
state: BtwPanelState;
|
|
23
|
+
errorMessage?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
13
26
|
export class BtwPanelComponent extends Container {
|
|
14
27
|
#question: string;
|
|
15
28
|
#tui: TUI;
|
|
16
29
|
#state: BtwPanelState = "running";
|
|
17
30
|
#answer = "";
|
|
18
31
|
#errorMessage: string | undefined;
|
|
32
|
+
#completedTurns: BtwTurn[] = [];
|
|
33
|
+
#streamingContent = new Container();
|
|
19
34
|
#closed = false;
|
|
20
35
|
|
|
21
36
|
constructor(options: BtwPanelComponentOptions) {
|
|
@@ -25,16 +40,29 @@ export class BtwPanelComponent extends Container {
|
|
|
25
40
|
this.#rebuild();
|
|
26
41
|
}
|
|
27
42
|
|
|
43
|
+
beginTurn(question: string): void {
|
|
44
|
+
if (this.#closed) return;
|
|
45
|
+
if (this.#state === "complete") {
|
|
46
|
+
this.#completedTurns.push(this.#currentTurn());
|
|
47
|
+
if (this.#completedTurns.length > BTW_MAX_CONTEXT_TURNS) this.#completedTurns.shift();
|
|
48
|
+
}
|
|
49
|
+
this.#question = question;
|
|
50
|
+
this.#answer = "";
|
|
51
|
+
this.#errorMessage = undefined;
|
|
52
|
+
this.#state = "running";
|
|
53
|
+
this.#rebuild();
|
|
54
|
+
}
|
|
55
|
+
|
|
28
56
|
appendText(delta: string): void {
|
|
29
57
|
if (!delta || this.#closed) return;
|
|
30
|
-
this.#answer
|
|
31
|
-
this.#
|
|
58
|
+
this.#answer = truncateUtf8(this.#answer + delta, BTW_MAX_ANSWER_UTF8_BYTES);
|
|
59
|
+
this.#updateStreamingContent();
|
|
32
60
|
}
|
|
33
61
|
|
|
34
62
|
setAnswer(text: string): void {
|
|
35
63
|
if (this.#closed) return;
|
|
36
|
-
this.#answer = text;
|
|
37
|
-
this.#
|
|
64
|
+
this.#answer = truncateUtf8(text, BTW_MAX_ANSWER_UTF8_BYTES);
|
|
65
|
+
this.#updateStreamingContent();
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
markComplete(): void {
|
|
@@ -54,21 +82,33 @@ export class BtwPanelComponent extends Container {
|
|
|
54
82
|
markError(message: string): void {
|
|
55
83
|
if (this.#closed) return;
|
|
56
84
|
this.#state = "error";
|
|
57
|
-
this.#
|
|
85
|
+
this.#answer = "";
|
|
86
|
+
this.#errorMessage = sanitizeBtwError(message);
|
|
58
87
|
this.#rebuild();
|
|
59
88
|
}
|
|
60
89
|
|
|
61
90
|
close(): void {
|
|
62
91
|
this.#closed = true;
|
|
92
|
+
this.#question = "";
|
|
93
|
+
this.#completedTurns = [];
|
|
94
|
+
this.#answer = "";
|
|
95
|
+
this.#errorMessage = undefined;
|
|
96
|
+
this.#streamingContent.clear();
|
|
97
|
+
this.clear();
|
|
63
98
|
}
|
|
64
99
|
|
|
65
100
|
#rebuild(): void {
|
|
66
101
|
this.clear();
|
|
67
102
|
this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
|
|
68
103
|
this.addChild(new Spacer(1));
|
|
104
|
+
for (const turn of this.#completedTurns) {
|
|
105
|
+
this.addChild(this.#turnComponent(turn));
|
|
106
|
+
this.addChild(new Spacer(1));
|
|
107
|
+
}
|
|
69
108
|
this.addChild(new Text(theme.fg("accent", replaceTabs(this.#question)), 1, 0));
|
|
70
109
|
this.addChild(new Spacer(1));
|
|
71
|
-
this
|
|
110
|
+
this.#updateStreamingContent(false);
|
|
111
|
+
this.addChild(this.#streamingContent);
|
|
72
112
|
this.addChild(new Spacer(1));
|
|
73
113
|
this.addChild(new Text(this.#footerLine(), 1, 0));
|
|
74
114
|
this.addChild(new Spacer(1));
|
|
@@ -76,29 +116,55 @@ export class BtwPanelComponent extends Container {
|
|
|
76
116
|
this.#tui.requestRender();
|
|
77
117
|
}
|
|
78
118
|
|
|
119
|
+
#updateStreamingContent(requestRender = true): void {
|
|
120
|
+
this.#streamingContent.clear();
|
|
121
|
+
this.#streamingContent.addChild(this.#contentComponent(this.#state, this.#answer, this.#errorMessage));
|
|
122
|
+
if (requestRender) this.#tui.requestRender();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#currentTurn(): BtwTurn {
|
|
126
|
+
return {
|
|
127
|
+
question: this.#question,
|
|
128
|
+
answer: this.#answer,
|
|
129
|
+
state: this.#state,
|
|
130
|
+
errorMessage: this.#errorMessage,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#turnComponent(turn: BtwTurn): Component {
|
|
135
|
+
const container = new Container();
|
|
136
|
+
container.addChild(new Text(theme.fg("accent", replaceTabs(turn.question)), 1, 0));
|
|
137
|
+
container.addChild(new Spacer(1));
|
|
138
|
+
container.addChild(this.#contentComponent(turn.state, turn.answer, turn.errorMessage));
|
|
139
|
+
return container;
|
|
140
|
+
}
|
|
141
|
+
|
|
79
142
|
#footerLine(): string {
|
|
80
143
|
switch (this.#state) {
|
|
81
144
|
case "running":
|
|
82
|
-
return theme.fg("muted", "Esc cancel /btw");
|
|
145
|
+
return theme.fg("muted", "Esc cancel /btw and return to main chat");
|
|
83
146
|
case "complete":
|
|
84
|
-
return theme.fg("muted", "Esc
|
|
147
|
+
return theme.fg("muted", "Type a follow-up · Esc return to main chat");
|
|
85
148
|
case "aborted":
|
|
86
|
-
return theme.fg(
|
|
149
|
+
return theme.fg(
|
|
150
|
+
"warning",
|
|
151
|
+
`${theme.status.warning} Cancelled · Type a follow-up · Esc return to main chat`,
|
|
152
|
+
);
|
|
87
153
|
case "error":
|
|
88
|
-
return theme.fg("error", `${theme.status.error} Error · Esc
|
|
154
|
+
return theme.fg("error", `${theme.status.error} Error · Type a follow-up · Esc return to main chat`);
|
|
89
155
|
}
|
|
90
156
|
}
|
|
91
157
|
|
|
92
|
-
#contentComponent(): Component {
|
|
93
|
-
if (
|
|
94
|
-
return new Text(theme.fg("error", replaceTabs(
|
|
158
|
+
#contentComponent(state: BtwPanelState, answer: string, errorMessage: string | undefined): Component {
|
|
159
|
+
if (state === "error") {
|
|
160
|
+
return new Text(theme.fg("error", replaceTabs(errorMessage ?? "Unknown error")), 1, 0);
|
|
95
161
|
}
|
|
96
|
-
const text = replaceTabs(
|
|
162
|
+
const text = replaceTabs(answer).trim();
|
|
97
163
|
if (!text) {
|
|
98
|
-
const waiting =
|
|
99
|
-
this.#state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
|
|
164
|
+
const waiting = state === "running" ? `${theme.status.pending} Waiting for response…` : "No text returned.";
|
|
100
165
|
return new Text(theme.fg("dim", waiting), 1, 0);
|
|
101
166
|
}
|
|
167
|
+
if (state === "running") return new Text(text, 1, 0);
|
|
102
168
|
return new Markdown(text, 1, 0, getMarkdownTheme());
|
|
103
169
|
}
|
|
104
170
|
}
|
|
@@ -35,6 +35,7 @@ import type { Settings } from "../../config/settings";
|
|
|
35
35
|
import { type ThemeColor, theme } from "../../modes/theme/theme";
|
|
36
36
|
import { formatModelOnboardingInlineHint } from "../../setup/model-onboarding-guidance";
|
|
37
37
|
import { formatClampedModelSelector, getThinkingLevelMetadata, parseThinkingLevel } from "../../thinking";
|
|
38
|
+
import { getConfiguredImageModel } from "../../tools/image-gen";
|
|
38
39
|
import { getTabBarTheme } from "../shared";
|
|
39
40
|
import { DynamicBorder } from "./dynamic-border";
|
|
40
41
|
|
|
@@ -118,6 +119,9 @@ export type ModelSelectorSelection =
|
|
|
118
119
|
| {
|
|
119
120
|
kind: "deleteProfile";
|
|
120
121
|
profileName: string;
|
|
122
|
+
}
|
|
123
|
+
| {
|
|
124
|
+
kind: "imageGeneration";
|
|
121
125
|
};
|
|
122
126
|
|
|
123
127
|
interface PendingThinkingChoice {
|
|
@@ -183,13 +187,18 @@ interface PresetBrowseRow {
|
|
|
183
187
|
kind: "browse";
|
|
184
188
|
}
|
|
185
189
|
|
|
190
|
+
interface PresetImageGenerationRow {
|
|
191
|
+
kind: "imageGeneration";
|
|
192
|
+
}
|
|
193
|
+
|
|
186
194
|
type PresetLandingRow =
|
|
187
195
|
| PresetGroupRow
|
|
188
196
|
| PresetProfileRow
|
|
189
197
|
| PresetCreateRow
|
|
190
198
|
| PresetCreateUnavailableRow
|
|
191
199
|
| PresetAlreadySavedRow
|
|
192
|
-
| PresetBrowseRow
|
|
200
|
+
| PresetBrowseRow
|
|
201
|
+
| PresetImageGenerationRow;
|
|
193
202
|
|
|
194
203
|
// Stable logical identity for a preset landing row, independent of its current
|
|
195
204
|
// list position. Used to relocate the cursor after the expanded group changes so
|
|
@@ -208,6 +217,8 @@ function presetRowIdentity(row: PresetLandingRow): string {
|
|
|
208
217
|
return "createUnavailable";
|
|
209
218
|
case "alreadySaved":
|
|
210
219
|
return `alreadySaved:${row.profile.name}`;
|
|
220
|
+
case "imageGeneration":
|
|
221
|
+
return "imageGeneration";
|
|
211
222
|
}
|
|
212
223
|
}
|
|
213
224
|
|
|
@@ -962,6 +973,7 @@ export class ModelSelectorComponent extends Container {
|
|
|
962
973
|
} else {
|
|
963
974
|
rows.push({ kind: "createUnavailable", label: "Select a model before creating a custom preset" });
|
|
964
975
|
}
|
|
976
|
+
rows.push({ kind: "imageGeneration" });
|
|
965
977
|
rows.push({ kind: "browse" });
|
|
966
978
|
return rows;
|
|
967
979
|
}
|
|
@@ -1047,7 +1059,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1047
1059
|
selected.kind === "browse" ||
|
|
1048
1060
|
selected.kind === "create" ||
|
|
1049
1061
|
selected.kind === "createUnavailable" ||
|
|
1050
|
-
selected.kind === "alreadySaved"
|
|
1062
|
+
selected.kind === "alreadySaved" ||
|
|
1063
|
+
selected.kind === "imageGeneration"
|
|
1051
1064
|
)
|
|
1052
1065
|
return;
|
|
1053
1066
|
if (this.#expandedPresetProviderId === selected.groupId) return;
|
|
@@ -1063,7 +1076,8 @@ export class ModelSelectorComponent extends Container {
|
|
|
1063
1076
|
selected.kind === "browse" ||
|
|
1064
1077
|
selected.kind === "create" ||
|
|
1065
1078
|
selected.kind === "createUnavailable" ||
|
|
1066
|
-
selected.kind === "alreadySaved"
|
|
1079
|
+
selected.kind === "alreadySaved" ||
|
|
1080
|
+
selected.kind === "imageGeneration"
|
|
1067
1081
|
)
|
|
1068
1082
|
return;
|
|
1069
1083
|
if (this.#expandedPresetProviderId !== selected.groupId) return;
|
|
@@ -1162,6 +1176,16 @@ export class ModelSelectorComponent extends Container {
|
|
|
1162
1176
|
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
1163
1177
|
continue;
|
|
1164
1178
|
}
|
|
1179
|
+
if (row.kind === "imageGeneration") {
|
|
1180
|
+
const config = getConfiguredImageModel();
|
|
1181
|
+
const current =
|
|
1182
|
+
config && config.provider !== "auto"
|
|
1183
|
+
? `${config.provider}${config.model ? ` (${config.model})` : ""}`
|
|
1184
|
+
: "Auto";
|
|
1185
|
+
const label = `Image Generation: ${current}`;
|
|
1186
|
+
this.#listContainer.addChild(new Text(`${prefix}${selected ? theme.fg("accent", label) : label}`, 0, 0));
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1165
1189
|
if (row.kind === "group") {
|
|
1166
1190
|
const authenticated = this.#isPresetGroupUsable(row.profiles);
|
|
1167
1191
|
const mark = this.#providerAuthPending ? "…" : authenticated ? "✓" : "✗";
|
|
@@ -1646,6 +1670,10 @@ export class ModelSelectorComponent extends Container {
|
|
|
1646
1670
|
this.#switchToModelMode();
|
|
1647
1671
|
return;
|
|
1648
1672
|
}
|
|
1673
|
+
if (row.kind === "imageGeneration") {
|
|
1674
|
+
this.#onSelectCallback({ kind: "imageGeneration" });
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1649
1677
|
if (row.kind === "group") {
|
|
1650
1678
|
// A group is a list of alternative presets; only surface a login hint
|
|
1651
1679
|
// when none of its members are usable. A partially-usable group stays
|
|
@@ -46,6 +46,7 @@ export interface StatusLineSettings {
|
|
|
46
46
|
previewHighlightSegment?: StatusLineSegmentId;
|
|
47
47
|
showHookStatus?: boolean;
|
|
48
48
|
showSkillHud?: boolean;
|
|
49
|
+
showActionHints?: boolean;
|
|
49
50
|
sessionAccent?: boolean;
|
|
50
51
|
maxRows?: number;
|
|
51
52
|
}
|
|
@@ -199,6 +200,7 @@ export class StatusLineComponent implements Component {
|
|
|
199
200
|
separator: settings.get("statusLine.separator"),
|
|
200
201
|
showHookStatus: settings.get("statusLine.showHookStatus"),
|
|
201
202
|
showSkillHud: settings.get("statusLine.showSkillHud"),
|
|
203
|
+
showActionHints: settings.get("statusLine.showActionHints"),
|
|
202
204
|
segmentOptions: settings.getGroup("statusLine").segmentOptions,
|
|
203
205
|
sessionAccent: settings.get("statusLine.sessionAccent"),
|
|
204
206
|
maxRows: settings.get("statusLine.maxRows"),
|
|
@@ -791,7 +793,10 @@ export class StatusLineComponent implements Component {
|
|
|
791
793
|
}
|
|
792
794
|
|
|
793
795
|
const right: string[] = [];
|
|
794
|
-
const actionHints =
|
|
796
|
+
const actionHints =
|
|
797
|
+
effectiveSettings.showActionHints === false
|
|
798
|
+
? []
|
|
799
|
+
: getAvailableActionHints(this.#actionRegistry, this.#getKeybindings, width, this.#focusDomain);
|
|
795
800
|
for (const segId of effectiveSettings.rightSegments) {
|
|
796
801
|
const rendered = renderSegment(segId, ctx);
|
|
797
802
|
if (rendered.visible && rendered.content) {
|