pi-sage 0.2.6 → 0.2.8

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.
@@ -371,8 +371,14 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
371
371
  let draft = mergeSettings(loaded.settings);
372
372
  let scope: SettingsScope = loaded.source === "global" ? "global" : "project";
373
373
 
374
+ const persistDraft = (): void => {
375
+ const target = getSettingsPathForScope(ctx.cwd, scope);
376
+ saveSettings(target, draft);
377
+ ctx.ui.notify(`Saved Sage settings to ${target}`, "info");
378
+ };
379
+
374
380
  while (true) {
375
- const action = await selectPaged(ctx, "Sage settings", [
381
+ const action = await selectScrollable(ctx, "Sage settings", [
376
382
  `Enabled: ${onOff(draft.enabled)}`,
377
383
  `Autonomous mode: ${onOff(draft.autonomousEnabled)}`,
378
384
  `Explicit requests bypass soft limits: ${onOff(draft.explicitRequestAlwaysAllowed)}`,
@@ -393,20 +399,11 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
393
399
  `Sensitive denylist: ${(draft.toolPolicy.sensitivePathDenylist ?? []).join(",")}`,
394
400
  `Cost cap/session: ${draft.maxEstimatedCostPerSession ?? "(none)"}`,
395
401
  `Save scope: ${scope}`,
396
- "Test Sage call",
397
- "Save and exit",
398
- "Exit without saving"
402
+ "Test Sage call"
399
403
  ]);
400
404
 
401
- if (!action || action === "Exit without saving") {
402
- ctx.ui.notify("Sage settings unchanged", "info");
403
- return;
404
- }
405
-
406
- if (action === "Save and exit") {
407
- const target = getSettingsPathForScope(ctx.cwd, scope);
408
- saveSettings(target, draft);
409
- ctx.ui.notify(`Saved Sage settings to ${target}`, "info");
405
+ if (!action) {
406
+ ctx.ui.notify("Sage settings closed (changes are saved immediately)", "info");
410
407
  return;
411
408
  }
412
409
 
@@ -417,56 +414,88 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
417
414
 
418
415
  if (action.startsWith("Enabled:")) {
419
416
  draft = { ...draft, enabled: !draft.enabled };
417
+ persistDraft();
420
418
  continue;
421
419
  }
422
420
  if (action.startsWith("Autonomous mode:")) {
423
421
  draft = { ...draft, autonomousEnabled: !draft.autonomousEnabled };
422
+ persistDraft();
424
423
  continue;
425
424
  }
426
425
  if (action.startsWith("Explicit requests bypass soft limits:")) {
427
426
  draft = { ...draft, explicitRequestAlwaysAllowed: !draft.explicitRequestAlwaysAllowed };
427
+ persistDraft();
428
428
  continue;
429
429
  }
430
430
  if (action.startsWith("Model:")) {
431
431
  const selected = await pickModel(ctx, draft.model);
432
- if (selected) draft = { ...draft, model: selected };
432
+ if (selected) {
433
+ draft = { ...draft, model: selected };
434
+ persistDraft();
435
+ }
433
436
  continue;
434
437
  }
435
438
  if (action.startsWith("Reasoning level:")) {
436
- const selected = await ctx.ui.select("Reasoning level", [...REASONING_LEVELS]);
439
+ const selected = await selectScrollable(ctx, "Reasoning level", [...REASONING_LEVELS]);
437
440
  if (selected && REASONING_LEVELS.includes(selected as ReasoningLevel)) {
438
441
  draft = { ...draft, reasoningLevel: selected as ReasoningLevel };
442
+ persistDraft();
439
443
  }
440
444
  continue;
441
445
  }
442
446
  if (action.startsWith("Timeout ms:")) {
443
- draft = await setNumberSetting(ctx, draft, "timeoutMs", "Timeout in milliseconds", 1000);
447
+ const updated = await setNumberSetting(ctx, draft, "timeoutMs", "Timeout in milliseconds", 1000);
448
+ if (updated !== draft) {
449
+ draft = updated;
450
+ persistDraft();
451
+ }
444
452
  continue;
445
453
  }
446
454
  if (action.startsWith("Max calls/turn:")) {
447
- draft = await setNumberSetting(ctx, draft, "maxCallsPerTurn", "Max Sage calls per turn", 1);
455
+ const updated = await setNumberSetting(ctx, draft, "maxCallsPerTurn", "Max Sage calls per turn", 1);
456
+ if (updated !== draft) {
457
+ draft = updated;
458
+ persistDraft();
459
+ }
448
460
  continue;
449
461
  }
450
462
  if (action.startsWith("Max calls/session:")) {
451
- draft = await setNumberSetting(ctx, draft, "maxCallsPerSession", "Max Sage calls per session", 1);
463
+ const updated = await setNumberSetting(ctx, draft, "maxCallsPerSession", "Max Sage calls per session", 1);
464
+ if (updated !== draft) {
465
+ draft = updated;
466
+ persistDraft();
467
+ }
452
468
  continue;
453
469
  }
454
470
  if (action.startsWith("Cooldown turns:")) {
455
- draft = await setNumberSetting(ctx, draft, "cooldownTurnsBetweenAutoCalls", "Cooldown turns", 0);
471
+ const updated = await setNumberSetting(ctx, draft, "cooldownTurnsBetweenAutoCalls", "Cooldown turns", 0);
472
+ if (updated !== draft) {
473
+ draft = updated;
474
+ persistDraft();
475
+ }
456
476
  continue;
457
477
  }
458
478
  if (action.startsWith("Max question chars:")) {
459
- draft = await setNumberSetting(ctx, draft, "maxQuestionChars", "Max question chars", 256);
479
+ const updated = await setNumberSetting(ctx, draft, "maxQuestionChars", "Max question chars", 256);
480
+ if (updated !== draft) {
481
+ draft = updated;
482
+ persistDraft();
483
+ }
460
484
  continue;
461
485
  }
462
486
  if (action.startsWith("Max context chars:")) {
463
- draft = await setNumberSetting(ctx, draft, "maxContextChars", "Max context chars", 512);
487
+ const updated = await setNumberSetting(ctx, draft, "maxContextChars", "Max context chars", 512);
488
+ if (updated !== draft) {
489
+ draft = updated;
490
+ persistDraft();
491
+ }
464
492
  continue;
465
493
  }
466
494
  if (action.startsWith("Tool profile:")) {
467
- const selected = await ctx.ui.select("Tool profile", [...TOOL_PROFILES]);
495
+ const selected = await selectScrollable(ctx, "Tool profile", [...TOOL_PROFILES]);
468
496
  if (selected && TOOL_PROFILES.includes(selected as ToolProfile)) {
469
497
  draft = { ...draft, toolPolicy: { ...draft.toolPolicy, profile: selected as ToolProfile } };
498
+ persistDraft();
470
499
  }
471
500
  continue;
472
501
  }
@@ -478,23 +507,40 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
478
507
  .map((item) => item.trim())
479
508
  .filter(Boolean);
480
509
  draft = { ...draft, toolPolicy: { ...draft.toolPolicy, customAllowedTools: tools } };
510
+ persistDraft();
481
511
  }
482
512
  continue;
483
513
  }
484
514
  if (action.startsWith("Max tool calls:")) {
485
- draft = await setToolPolicyNumberSetting(ctx, draft, "maxToolCalls", "Max tool calls", 1);
515
+ const updated = await setToolPolicyNumberSetting(ctx, draft, "maxToolCalls", "Max tool calls", 1);
516
+ if (updated !== draft) {
517
+ draft = updated;
518
+ persistDraft();
519
+ }
486
520
  continue;
487
521
  }
488
522
  if (action.startsWith("Max files read:")) {
489
- draft = await setToolPolicyNumberSetting(ctx, draft, "maxFilesRead", "Max files read", 1);
523
+ const updated = await setToolPolicyNumberSetting(ctx, draft, "maxFilesRead", "Max files read", 1);
524
+ if (updated !== draft) {
525
+ draft = updated;
526
+ persistDraft();
527
+ }
490
528
  continue;
491
529
  }
492
530
  if (action.startsWith("Max bytes/file:")) {
493
- draft = await setToolPolicyNumberSetting(ctx, draft, "maxBytesPerFile", "Max bytes per file", 1024);
531
+ const updated = await setToolPolicyNumberSetting(ctx, draft, "maxBytesPerFile", "Max bytes per file", 1024);
532
+ if (updated !== draft) {
533
+ draft = updated;
534
+ persistDraft();
535
+ }
494
536
  continue;
495
537
  }
496
538
  if (action.startsWith("Max total bytes:")) {
497
- draft = await setToolPolicyNumberSetting(ctx, draft, "maxTotalBytesRead", "Max total bytes", 1024);
539
+ const updated = await setToolPolicyNumberSetting(ctx, draft, "maxTotalBytesRead", "Max total bytes", 1024);
540
+ if (updated !== draft) {
541
+ draft = updated;
542
+ persistDraft();
543
+ }
498
544
  continue;
499
545
  }
500
546
  if (action.startsWith("Sensitive denylist:")) {
@@ -508,6 +554,7 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
508
554
  .map((item) => item.trim())
509
555
  .filter(Boolean);
510
556
  draft = { ...draft, toolPolicy: { ...draft.toolPolicy, sensitivePathDenylist: denylist } };
557
+ persistDraft();
511
558
  }
512
559
  continue;
513
560
  }
@@ -519,10 +566,12 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
519
566
  const copy = { ...draft };
520
567
  delete copy.maxEstimatedCostPerSession;
521
568
  draft = copy;
569
+ persistDraft();
522
570
  } else {
523
571
  const parsed = Number(trimmed);
524
572
  if (Number.isFinite(parsed) && parsed >= 0) {
525
573
  draft = { ...draft, maxEstimatedCostPerSession: parsed };
574
+ persistDraft();
526
575
  } else {
527
576
  ctx.ui.notify("Invalid cost cap value", "warning");
528
577
  }
@@ -530,8 +579,11 @@ async function runSettingsWizard(ctx: ExtensionCommandContext): Promise<void> {
530
579
  continue;
531
580
  }
532
581
  if (action.startsWith("Save scope:")) {
533
- const selected = await ctx.ui.select("Save scope", ["project", "global"]);
534
- if (selected === "project" || selected === "global") scope = selected;
582
+ const selected = await selectScrollable(ctx, "Save scope", ["project", "global"]);
583
+ if (selected === "project" || selected === "global") {
584
+ scope = selected;
585
+ persistDraft();
586
+ }
535
587
  continue;
536
588
  }
537
589
  }
@@ -556,7 +608,7 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
556
608
  })
557
609
  ];
558
610
 
559
- const providerChoice = await selectPaged(ctx, "Choose Sage model provider", providerOptions);
611
+ const providerChoice = await selectScrollable(ctx, "Choose Sage model provider", providerOptions);
560
612
  if (!providerChoice) return undefined;
561
613
 
562
614
  if (providerChoice.startsWith(MODEL_INHERIT_VALUE)) {
@@ -587,8 +639,8 @@ async function pickModel(ctx: ExtensionContext, currentModel: string): Promise<s
587
639
  modelOptionMap.set(option, composite);
588
640
  }
589
641
 
590
- const modelChoice = await selectPaged(ctx, `Choose Sage model (${provider})`, modelOptions);
591
- if (!modelChoice) return undefined;
642
+ const modelChoice = await selectScrollable(ctx, `Choose Sage model (${provider})`, modelOptions);
643
+ if (!modelChoice) continue;
592
644
  if (modelChoice === "← Back to providers") continue;
593
645
 
594
646
  const resolved = modelOptionMap.get(modelChoice);
@@ -664,44 +716,86 @@ function resolveModelSpec(
664
716
 
665
717
  type HasUIContext = { ui: ExtensionContext["ui"] };
666
718
 
667
- async function selectPaged(
719
+ async function selectScrollable(
668
720
  ctx: HasUIContext,
669
721
  title: string,
670
722
  options: string[],
671
- pageSize = 10
723
+ maxVisible = 10
672
724
  ): Promise<string | undefined> {
673
- if (options.length <= pageSize) {
674
- return await ctx.ui.select(title, options);
675
- }
725
+ if (options.length === 0) return undefined;
676
726
 
677
- let page = 0;
678
- const totalPages = Math.max(1, Math.ceil(options.length / pageSize));
679
- const PREV = "← Previous page";
680
- const NEXT = "→ Next page";
727
+ return await ctx.ui.custom<string | undefined>((tui, theme, _keybindings, done) => {
728
+ let selectedIndex = 0;
681
729
 
682
- while (true) {
683
- const start = page * pageSize;
684
- const end = Math.min(start + pageSize, options.length);
685
- const pageOptions = options.slice(start, end);
730
+ const move = (delta: number): void => {
731
+ const next = selectedIndex + delta;
732
+ selectedIndex = Math.max(0, Math.min(options.length - 1, next));
733
+ tui.requestRender();
734
+ };
686
735
 
687
- if (page > 0) pageOptions.push(PREV);
688
- if (page < totalPages - 1) pageOptions.push(NEXT);
736
+ const render = (width: number): string[] => {
737
+ const lines: string[] = [];
738
+ lines.push(theme.fg("accent", title));
739
+ lines.push("");
689
740
 
690
- const selected = await ctx.ui.select(`${title} (${start + 1}-${end} of ${options.length})`, pageOptions);
691
- if (!selected) return undefined;
741
+ const visible = Math.max(3, Math.min(maxVisible, options.length));
742
+ const start = Math.max(0, Math.min(selectedIndex - Math.floor(visible / 2), options.length - visible));
743
+ const end = Math.min(options.length, start + visible);
692
744
 
693
- if (selected === PREV && page > 0) {
694
- page -= 1;
695
- continue;
696
- }
745
+ for (let i = start; i < end; i += 1) {
746
+ const option = options[i] ?? "";
747
+ const prefix = i === selectedIndex ? "→ " : " ";
748
+ const raw = `${prefix}${option}`;
749
+ lines.push(i === selectedIndex ? theme.fg("accent", raw) : raw);
750
+ }
697
751
 
698
- if (selected === NEXT && page < totalPages - 1) {
699
- page += 1;
700
- continue;
701
- }
752
+ lines.push("");
753
+ lines.push(theme.fg("muted", `(${selectedIndex + 1}/${options.length}) ↑↓ navigate • enter select • esc back`));
702
754
 
703
- return selected;
704
- }
755
+ return lines.map((line) => (line.length > width ? `${line.slice(0, Math.max(0, width - 1))}…` : line));
756
+ };
757
+
758
+ const handleInput = (data: string): void => {
759
+ const isUp = data.includes("\u001b[A") || data.includes("\u001bOA");
760
+ const isDown = data.includes("\u001b[B") || data.includes("\u001bOB");
761
+ const isEnter = data.includes("\r") || data.includes("\n") || data.includes("\u001bOM");
762
+ const isEscLike = data.startsWith("\u001b");
763
+
764
+ if (isUp) {
765
+ move(-1);
766
+ return;
767
+ }
768
+ if (isDown) {
769
+ move(1);
770
+ return;
771
+ }
772
+ if (data === "k" || data === "K") {
773
+ move(-1);
774
+ return;
775
+ }
776
+ if (data === "j" || data === "J") {
777
+ move(1);
778
+ return;
779
+ }
780
+ if (isEnter) {
781
+ done(options[selectedIndex]);
782
+ return;
783
+ }
784
+ if (data.includes("\u0003")) {
785
+ done(undefined);
786
+ return;
787
+ }
788
+ if (isEscLike) {
789
+ done(undefined);
790
+ }
791
+ };
792
+
793
+ return {
794
+ render,
795
+ invalidate: () => {},
796
+ handleInput
797
+ };
798
+ });
705
799
  }
706
800
 
707
801
  function onOff(value: boolean): string {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-sage",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Interactive-only advisory Sage extension for Pi",