@selesai/code 0.13.24 → 0.13.25
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 +8 -0
- package/dist/extensions/pi-graft/commands.test.ts +1 -1
- package/dist/extensions/pi-graft/commands.ts +1 -1
- package/dist/extensions/pi-graft/index.ts +135 -36
- package/dist/extensions/pi-graft/lifecycle.test.ts +78 -5
- package/dist/extensions/pi-graft/state.test.ts +6 -0
- package/dist/extensions/pi-graft/state.ts +6 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@selesai/code` will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.13.25] - 2026-09-14
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Graft deep-build model override.** Set `graft.deepModel` to pin a model on the active provider for Graft deep builds.
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- **Graft deep-build fallback.** When the active model returns no usable Graft summaries, the deep build retries up to two other models scoped to that provider before reporting the deep-build failure.
|
|
12
|
+
|
|
5
13
|
## [0.13.24] - 2026-09-13
|
|
6
14
|
|
|
7
15
|
### Added
|
|
@@ -387,7 +387,7 @@ describe("mutation-capable commands", () => {
|
|
|
387
387
|
const [title, message] = ctx.ui.confirm.mock.calls[0] as [string, string];
|
|
388
388
|
expect(title).toContain("provider-backed");
|
|
389
389
|
expect(message).toContain("leaves this machine");
|
|
390
|
-
expect(message).toContain("
|
|
390
|
+
expect(message).toContain("Credentials are passed only to this Graft child process");
|
|
391
391
|
expect(harness.runBuild).toHaveBeenCalledWith(true, ctx);
|
|
392
392
|
});
|
|
393
393
|
|
|
@@ -330,7 +330,7 @@ export function registerGraftCommands(pi: ExtensionAPI, runtime: GraftCommandRun
|
|
|
330
330
|
? "Set up Graft for this repository?"
|
|
331
331
|
: "Build the Graft graph?",
|
|
332
332
|
deep
|
|
333
|
-
? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass summarizes each changed file and extracts per-symbol cruxes using Selesai's active compatible model,
|
|
333
|
+
? `This runs \`graft build --deep\` in ${repo}.\n\nThe structural graph stays local and deterministic. The deep pass summarizes each changed file and extracts per-symbol cruxes using Selesai's active compatible model, falling back to another model you scoped on the same provider when that model cannot produce Graft summaries. Source-derived content leaves this machine either way. Credentials are passed only to this Graft child process.\n\nFiles it can write:\n${buildEffects(true)}`
|
|
334
334
|
: `This runs \`graft build\` in ${repo} and can write:\n\n${buildEffects(false)}`,
|
|
335
335
|
manualCommand,
|
|
336
336
|
);
|
|
@@ -183,32 +183,94 @@ const GRAFT_PROVIDER_FOR_SELESAI_PROVIDER: Record<string, "openai" | "anthropic"
|
|
|
183
183
|
tokenin: "litellm",
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
/**
|
|
187
|
-
|
|
186
|
+
/**
|
|
187
|
+
* The deep tier either has a plan, or a human-readable reason it does not.
|
|
188
|
+
* Automatic builds fall back to the structural tier on `ok: false`; the
|
|
189
|
+
* explicit `/graft deep` command reports the reason instead.
|
|
190
|
+
*/
|
|
191
|
+
type DeepBuildPlan =
|
|
192
|
+
| { ok: true; env: NodeJS.ProcessEnv | undefined; models: readonly string[] }
|
|
193
|
+
| { ok: false; reason: string };
|
|
194
|
+
|
|
195
|
+
/** Graft's per-symbol summary pass is model-sensitive; this is its failure signature. */
|
|
196
|
+
const UNUSABLE_SUMMARIES = /no usable symbol summaries|empty-parsed/i;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Deep-build candidates in order: an explicit `graft.deepModel` pin, the active
|
|
200
|
+
* model, then the models this session scoped on the same provider. No new
|
|
201
|
+
* setting is needed — models the user already chose are tried before giving up
|
|
202
|
+
* on the deep tier.
|
|
203
|
+
*/
|
|
204
|
+
function deepBuildModels(ctx: ExtensionContext, settings: { deepModel?: string }): readonly string[] {
|
|
205
|
+
const active = ctx.model;
|
|
206
|
+
const scoped = (ctx.scopedModels ?? [])
|
|
207
|
+
.map((entry) => entry.model)
|
|
208
|
+
.filter((candidate) => candidate.provider === active?.provider)
|
|
209
|
+
.map((candidate) => candidate.id);
|
|
210
|
+
const ordered = [settings.deepModel, active?.id, ...scoped].filter((id): id is string => Boolean(id));
|
|
211
|
+
return [...new Set(ordered)].slice(0, 3);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Resolve the active Selesai model into the isolated environment Graft needs.
|
|
216
|
+
*
|
|
217
|
+
* Credentials are read from Selesai's own auth storage — TokenIn keys live in
|
|
218
|
+
* `~/.selesai/agent`, not in the shell environment — and passed only to the
|
|
219
|
+
* Graft child process.
|
|
220
|
+
*/
|
|
221
|
+
async function resolveDeepBuildPlan(
|
|
222
|
+
ctx: ExtensionContext,
|
|
223
|
+
settings: { deepModel?: string } = {},
|
|
224
|
+
): Promise<DeepBuildPlan> {
|
|
225
|
+
const models = deepBuildModels(ctx, settings);
|
|
226
|
+
// A user who already configured Graft's own provider environment outranks us.
|
|
227
|
+
if (process.env.GRAFT_PROVIDER && process.env.GRAFT_API_KEY) return { ok: true, env: undefined, models };
|
|
228
|
+
|
|
188
229
|
const model = ctx.model;
|
|
189
|
-
|
|
190
|
-
|
|
230
|
+
if (!model) return { ok: false, reason: "no active model is selected" };
|
|
231
|
+
const provider = GRAFT_PROVIDER_FOR_SELESAI_PROVIDER[model.provider];
|
|
232
|
+
if (!provider) {
|
|
233
|
+
return {
|
|
234
|
+
ok: false,
|
|
235
|
+
reason: `provider "${model.provider}" has no Graft wire format (use TokenIn, LiteLLM, OpenAI, Anthropic, or OrcaRouter)`,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
191
238
|
|
|
192
|
-
let
|
|
239
|
+
let apiKey: string | undefined;
|
|
240
|
+
let authBaseUrl: string | undefined;
|
|
193
241
|
try {
|
|
194
|
-
auth = await ctx.modelRegistry.getProviderAuth(model.provider);
|
|
242
|
+
const auth = await ctx.modelRegistry.getProviderAuth(model.provider);
|
|
243
|
+
apiKey = auth?.auth.apiKey;
|
|
244
|
+
authBaseUrl = auth?.auth.baseUrl;
|
|
195
245
|
} catch {
|
|
196
|
-
|
|
246
|
+
// Fall through to the missing-credential reason below.
|
|
247
|
+
}
|
|
248
|
+
if (!apiKey) return { ok: false, reason: `no credential is stored for "${model.provider}"` };
|
|
249
|
+
|
|
250
|
+
// The composed model carries the endpoint; auth only sometimes does.
|
|
251
|
+
const baseUrl = authBaseUrl || model.baseUrl || ctx.modelRegistry.getProvider(model.provider)?.baseUrl;
|
|
252
|
+
if (provider === "litellm" && !baseUrl) {
|
|
253
|
+
return { ok: false, reason: `"${model.provider}" has no base URL to send Graft to` };
|
|
197
254
|
}
|
|
198
|
-
const apiKey = auth?.auth.apiKey;
|
|
199
|
-
const baseUrl = auth?.auth.baseUrl;
|
|
200
|
-
if (!apiKey || (provider === "litellm" && !baseUrl)) return undefined;
|
|
201
255
|
|
|
202
|
-
const { GRAFT_PROVIDER:
|
|
256
|
+
const { GRAFT_PROVIDER: _p, GRAFT_MODEL: _m, GRAFT_API_KEY: _k, GRAFT_BASE_URL: _u, ...env } = process.env;
|
|
203
257
|
return {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
258
|
+
ok: true,
|
|
259
|
+
models,
|
|
260
|
+
env: {
|
|
261
|
+
...env,
|
|
262
|
+
GRAFT_PROVIDER: provider,
|
|
263
|
+
GRAFT_API_KEY: apiKey,
|
|
264
|
+
...(baseUrl ? { GRAFT_BASE_URL: baseUrl } : {}),
|
|
265
|
+
},
|
|
209
266
|
};
|
|
210
267
|
}
|
|
211
268
|
|
|
269
|
+
/** Graft's environment for one candidate model; undefined keeps the user's own GRAFT_* setup. */
|
|
270
|
+
function deepEnvFor(plan: Extract<DeepBuildPlan, { ok: true }>, modelId: string): NodeJS.ProcessEnv | undefined {
|
|
271
|
+
return plan.env ? { ...plan.env, GRAFT_MODEL: modelId } : undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
212
274
|
export default function graftExtension(pi: ExtensionAPI): void {
|
|
213
275
|
registerBuiltinAgentAugmentation(pi);
|
|
214
276
|
const exec: ExecLike = (command, args, options) => pi.exec(command, args, options);
|
|
@@ -277,6 +339,48 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
277
339
|
}
|
|
278
340
|
};
|
|
279
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Build the graph: deep with a compatible active model, structural otherwise.
|
|
344
|
+
*
|
|
345
|
+
* A model that cannot produce Graft-parsable summaries is not fatal — the
|
|
346
|
+
* next candidate the session already offers is tried before downgrading.
|
|
347
|
+
*/
|
|
348
|
+
const buildGraph = async (
|
|
349
|
+
ctx: ExtensionContext,
|
|
350
|
+
activeSession: GraftSession,
|
|
351
|
+
phase: "build" | "refresh" = "build",
|
|
352
|
+
): Promise<GraftRun> => {
|
|
353
|
+
const repoRoot = activeSession.repoRoot!;
|
|
354
|
+
// A refresh has already announced its own "syncing" phase at the caller.
|
|
355
|
+
const started = (deep: boolean): void => {
|
|
356
|
+
if (phase === "refresh") return;
|
|
357
|
+
if (session === activeSession && !activeSession.disposed) applyEvent(ctx, { type: "build-started", deep });
|
|
358
|
+
};
|
|
359
|
+
const structural = async (): Promise<GraftRun> => {
|
|
360
|
+
started(false);
|
|
361
|
+
return runGraft(exec, repoRoot, { kind: "build", deep: false });
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const plan = await resolveDeepBuildPlan(ctx, activeSession.settings);
|
|
365
|
+
if (!plan.ok) return structural();
|
|
366
|
+
|
|
367
|
+
// The user's own GRAFT_* environment names the model; never second-guess it.
|
|
368
|
+
const candidates = plan.env ? plan.models : plan.models.slice(0, 1);
|
|
369
|
+
let failed: GraftRun | undefined;
|
|
370
|
+
for (const [index, model] of candidates.entries()) {
|
|
371
|
+
started(true);
|
|
372
|
+
const run = await runGraft(exec, repoRoot, { kind: "build", deep: true }, { env: deepEnvFor(plan, model) });
|
|
373
|
+
if (run.code === 0 || !UNUSABLE_SUMMARIES.test(`${run.stderr}\n${run.stdout}`)) return run;
|
|
374
|
+
failed = run;
|
|
375
|
+
if (index === 0 && candidates.length > 1 && ctx.hasUI) {
|
|
376
|
+
ctx.ui.notify(`Graft cannot summarize with ${model}; retrying the deep pass with another scoped model.`, "info");
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
// Nothing left to try: report the last run rather than pretending the
|
|
380
|
+
// structural tier explained the failure.
|
|
381
|
+
return failed ?? (await structural());
|
|
382
|
+
};
|
|
383
|
+
|
|
280
384
|
/**
|
|
281
385
|
* Start the graph once. Compatible active Selesai models add the deep tier;
|
|
282
386
|
* otherwise the free local structural tier remains usable.
|
|
@@ -290,11 +394,7 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
290
394
|
activeSession.state.name === "failed"
|
|
291
395
|
) return undefined;
|
|
292
396
|
|
|
293
|
-
const pending =
|
|
294
|
-
const deep = env !== undefined;
|
|
295
|
-
if (session === activeSession && !activeSession.disposed) applyEvent(ctx, { type: "build-started", deep });
|
|
296
|
-
return runGraft(exec, activeSession.repoRoot!, { kind: "build", deep }, { env });
|
|
297
|
-
});
|
|
397
|
+
const pending = buildGraph(ctx, activeSession);
|
|
298
398
|
structuralBuildPromise = pending;
|
|
299
399
|
structuralBuildSession = activeSession;
|
|
300
400
|
void pending.then(
|
|
@@ -544,11 +644,9 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
544
644
|
|
|
545
645
|
void (async () => {
|
|
546
646
|
try {
|
|
547
|
-
const
|
|
548
|
-
const deep = env !== undefined;
|
|
549
|
-
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
|
|
647
|
+
const run = await buildGraph(ctx, session, "refresh");
|
|
550
648
|
if (session.disposed) return;
|
|
551
|
-
recordBuild(ctx, run, deep);
|
|
649
|
+
recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
|
|
552
650
|
} catch (error) {
|
|
553
651
|
if (session.disposed) return;
|
|
554
652
|
applyEvent(ctx, {
|
|
@@ -584,24 +682,27 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
584
682
|
if (!deep) {
|
|
585
683
|
const pending = autoBuild(ctx, session);
|
|
586
684
|
if (pending) return pending;
|
|
685
|
+
applyEvent(ctx, { type: "build-started", deep: false });
|
|
686
|
+
const structural = await runGraft(exec, session.repoRoot!, { kind: "build", deep: false });
|
|
687
|
+
recordBuild(ctx, structural, false);
|
|
688
|
+
return structural;
|
|
587
689
|
}
|
|
588
|
-
const
|
|
589
|
-
if (
|
|
690
|
+
const plan = await resolveDeepBuildPlan(ctx, session.settings);
|
|
691
|
+
if (!plan.ok) {
|
|
590
692
|
return {
|
|
591
|
-
op: { kind: "build", deep },
|
|
693
|
+
op: { kind: "build", deep: true },
|
|
592
694
|
argv: ["graft", "build", "--deep"],
|
|
593
695
|
cwd: session.repoRoot!,
|
|
594
696
|
code: 1,
|
|
595
697
|
stdout: "",
|
|
596
|
-
stderr:
|
|
698
|
+
stderr: `No deep build is possible for the active model: ${plan.reason}.`,
|
|
597
699
|
killed: false,
|
|
598
700
|
cancelled: false,
|
|
599
701
|
timedOut: false,
|
|
600
702
|
};
|
|
601
703
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
recordBuild(ctx, run, deep);
|
|
704
|
+
const run = await buildGraph(ctx, session);
|
|
705
|
+
recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
|
|
605
706
|
return run;
|
|
606
707
|
},
|
|
607
708
|
runRefresh: async (ctx) => {
|
|
@@ -609,10 +710,8 @@ export default function graftExtension(pi: ExtensionAPI): void {
|
|
|
609
710
|
await ensureReady(ctx, { kind: "build", deep: false });
|
|
610
711
|
session.lastRefreshAt = Date.now();
|
|
611
712
|
applyEvent(ctx, { type: "refresh-started" });
|
|
612
|
-
const
|
|
613
|
-
|
|
614
|
-
const run = await runGraft(exec, session.repoRoot!, { kind: "build", deep }, { env });
|
|
615
|
-
recordBuild(ctx, run, deep);
|
|
713
|
+
const run = await buildGraph(ctx, session, "refresh");
|
|
714
|
+
recordBuild(ctx, run, run.op.kind === "build" && run.op.deep);
|
|
616
715
|
return run;
|
|
617
716
|
},
|
|
618
717
|
providerEnvNames: () => PROVIDER_ENV_NAMES.filter((name) => Boolean(process.env[name])),
|
|
@@ -304,11 +304,11 @@ describe("proactive refresh", () => {
|
|
|
304
304
|
});
|
|
305
305
|
graftExtension(harness.pi as never);
|
|
306
306
|
const ctx = makeCtx({ cwd: bare });
|
|
307
|
+
// Mirrors the real TokenIn shape: the key lives in ~/.selesai auth storage and
|
|
308
|
+
// the endpoint comes from the composed model, not from the auth resolution.
|
|
307
309
|
Object.assign(ctx, {
|
|
308
|
-
model: { provider: "tokenin", id: "deepseek-v4-flash" },
|
|
309
|
-
modelRegistry: {
|
|
310
|
-
getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token", baseUrl: "https://lite.andlet.me/v1" } })),
|
|
311
|
-
},
|
|
310
|
+
model: { provider: "tokenin", id: "deepseek-v4.1-flash", baseUrl: "https://lite.andlet.me/v1" },
|
|
311
|
+
modelRegistry: { getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token" } })) },
|
|
312
312
|
});
|
|
313
313
|
await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
|
|
314
314
|
{ type: "session_start", reason: "startup" },
|
|
@@ -317,7 +317,80 @@ describe("proactive refresh", () => {
|
|
|
317
317
|
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
|
|
318
318
|
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
319
319
|
expect(build[1]).toEqual(["build", "--deep"]);
|
|
320
|
-
expect(build[2]).toMatchObject({
|
|
320
|
+
expect(build[2]).toMatchObject({
|
|
321
|
+
env: {
|
|
322
|
+
GRAFT_PROVIDER: "litellm",
|
|
323
|
+
GRAFT_MODEL: "deepseek-v4.1-flash",
|
|
324
|
+
GRAFT_API_KEY: "test-token",
|
|
325
|
+
GRAFT_BASE_URL: "https://lite.andlet.me/v1",
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it("retries the deep pass with another scoped model when the active one cannot summarize", async () => {
|
|
331
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
332
|
+
const bare = join(root, "retry-repo");
|
|
333
|
+
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
334
|
+
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
335
|
+
const models: string[] = [];
|
|
336
|
+
const harness = makePi(async (command: string, args: string[], options?: unknown) => {
|
|
337
|
+
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
338
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
339
|
+
if (args.includes("build")) {
|
|
340
|
+
const model = (options as { env?: Record<string, string> }).env?.GRAFT_MODEL ?? "";
|
|
341
|
+
models.push(model);
|
|
342
|
+
if (model === "deepseek-v4.1-flash") {
|
|
343
|
+
return { ...OK, code: 1, stderr: "calc.ts: model returned no usable symbol summaries [empty-parsed]" };
|
|
344
|
+
}
|
|
345
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
346
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
347
|
+
writeFileSync(join(bare, "graft", "concept.md"), "# Concept", "utf-8");
|
|
348
|
+
}
|
|
349
|
+
return { ...OK, stdout: ASK_JSON };
|
|
350
|
+
});
|
|
351
|
+
graftExtension(harness.pi as never);
|
|
352
|
+
const ctx = makeCtx({ cwd: bare });
|
|
353
|
+
Object.assign(ctx, {
|
|
354
|
+
model: { provider: "tokenin", id: "deepseek-v4.1-flash", baseUrl: "https://lite.andlet.me/v1" },
|
|
355
|
+
modelRegistry: { getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-token" } })) },
|
|
356
|
+
scopedModels: [{ model: { provider: "tokenin", id: "glm-5.3-flash" } }],
|
|
357
|
+
});
|
|
358
|
+
await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
|
|
359
|
+
{ type: "session_start", reason: "startup" },
|
|
360
|
+
ctx,
|
|
361
|
+
);
|
|
362
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● deep v0.18.0"));
|
|
363
|
+
expect(models).toEqual(["deepseek-v4.1-flash", "glm-5.3-flash"]);
|
|
364
|
+
expect(notified(ctx)).toContain("retrying the deep pass");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("falls back to the structural build when the active model cannot go deep", async () => {
|
|
368
|
+
process.env.PI_CODING_AGENT_DIR = agentDir;
|
|
369
|
+
const bare = join(root, "fallback-repo");
|
|
370
|
+
mkdirSync(join(bare, ".selesai"), { recursive: true });
|
|
371
|
+
writeFileSync(join(bare, ".selesai", "settings.json"), JSON.stringify({ graft: {} }), "utf-8");
|
|
372
|
+
const harness = makePi(async (command: string, args: string[]) => {
|
|
373
|
+
if (command === "git") return { ...OK, stdout: `${bare}\n` };
|
|
374
|
+
if (args.includes("--version")) return { ...OK, stdout: "0.18.0\n" };
|
|
375
|
+
if (args.includes("build")) {
|
|
376
|
+
mkdirSync(join(bare, "graft", ".graph"), { recursive: true });
|
|
377
|
+
writeFileSync(join(bare, "graft", ".graph", "wiring.json"), "{}", "utf-8");
|
|
378
|
+
}
|
|
379
|
+
return { ...OK, stdout: ASK_JSON };
|
|
380
|
+
});
|
|
381
|
+
graftExtension(harness.pi as never);
|
|
382
|
+
const ctx = makeCtx({ cwd: bare });
|
|
383
|
+
Object.assign(ctx, {
|
|
384
|
+
model: { provider: "google", id: "gemini-3-pro" },
|
|
385
|
+
modelRegistry: { getProviderAuth: vi.fn(async () => ({ auth: { apiKey: "test-key" } })) },
|
|
386
|
+
});
|
|
387
|
+
await (handlerFor(harness, "session_start") as (e: unknown, c: unknown) => Promise<void>)(
|
|
388
|
+
{ type: "session_start", reason: "startup" },
|
|
389
|
+
ctx,
|
|
390
|
+
);
|
|
391
|
+
await vi.waitFor(() => expect(ctx.statuses.get("graft")).toBe("graft: ● structural v0.18.0"));
|
|
392
|
+
const build = harness.exec.mock.calls.find((call) => (call[1] as string[]).includes("build"))!;
|
|
393
|
+
expect(build[1]).toEqual(["build"]);
|
|
321
394
|
});
|
|
322
395
|
|
|
323
396
|
it("re-indexes after edits, shows the sync phase, and returns to fresh", async () => {
|
|
@@ -240,12 +240,18 @@ describe("readGraftSettings", () => {
|
|
|
240
240
|
maxInjectionBytes: -1,
|
|
241
241
|
maxResultBytes: 0,
|
|
242
242
|
refreshDebounceSeconds: "60",
|
|
243
|
+
deepModel: " ",
|
|
243
244
|
in: " ",
|
|
244
245
|
},
|
|
245
246
|
});
|
|
246
247
|
expect(read(false)).toEqual({});
|
|
247
248
|
});
|
|
248
249
|
|
|
250
|
+
it("keeps a deep-build model override for the active provider", () => {
|
|
251
|
+
write(join(agentDir, "settings.json"), { graft: { deepModel: " glm-5.3-flash " } });
|
|
252
|
+
expect(read(false)).toEqual({ deepModel: "glm-5.3-flash" });
|
|
253
|
+
});
|
|
254
|
+
|
|
249
255
|
it("keeps a valid enabled flag and telemetry preference", () => {
|
|
250
256
|
write(join(agentDir, "settings.json"), { graft: { enabled: false, telemetry: "inherit" } });
|
|
251
257
|
expect(read(false)).toEqual({ enabled: false, telemetry: "inherit" });
|
|
@@ -57,6 +57,11 @@ export interface GraftSettings {
|
|
|
57
57
|
in?: string;
|
|
58
58
|
/** Seconds a mutation stays "changed" before a proactive refresh is attempted. */
|
|
59
59
|
refreshDebounceSeconds?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Model id for deep builds, on the active provider. Defaults to the active
|
|
62
|
+
* model; set it when that model cannot produce Graft-parsable summaries.
|
|
63
|
+
*/
|
|
64
|
+
deepModel?: string;
|
|
60
65
|
}
|
|
61
66
|
|
|
62
67
|
export const SETTINGS_KEY = "graft";
|
|
@@ -97,6 +102,7 @@ function normalizeSettings(raw: unknown): GraftSettings {
|
|
|
97
102
|
if (typeof candidate === "number" && Number.isFinite(candidate) && candidate > 0) settings[key] = candidate;
|
|
98
103
|
}
|
|
99
104
|
if (typeof value.in === "string" && value.in.trim()) settings.in = value.in.trim();
|
|
105
|
+
if (typeof value.deepModel === "string" && value.deepModel.trim()) settings.deepModel = value.deepModel.trim();
|
|
100
106
|
return settings;
|
|
101
107
|
}
|
|
102
108
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@selesai/code",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.25",
|
|
4
4
|
"description": "Maintained, extension-first Pi coding agent with built-in workflows, subagents, web research, questions, skills, and an enhanced terminal UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|