@mariozechner/pi-coding-agent 0.8.5 → 0.9.1

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.
@@ -35,7 +35,6 @@ export class TuiRenderer {
35
35
  isInitialized = false;
36
36
  onInputCallback;
37
37
  loadingAnimation = null;
38
- onInterruptCallback;
39
38
  lastSigintTime = 0;
40
39
  changelogMarkdown = null;
41
40
  newVersion = null;
@@ -63,6 +62,8 @@ export class TuiRenderer {
63
62
  scopedModels = [];
64
63
  // Tool output expansion state
65
64
  toolOutputExpanded = false;
65
+ // Agent subscription unsubscribe function
66
+ unsubscribe;
66
67
  constructor(agent, sessionManager, settingsManager, version, changelogMarkdown = null, newVersion = null, scopedModels = []) {
67
68
  this.agent = agent;
68
69
  this.sessionManager = sessionManager;
@@ -120,6 +121,10 @@ export class TuiRenderer {
120
121
  name: "theme",
121
122
  description: "Select color theme (opens selector UI)",
122
123
  };
124
+ const clearCommand = {
125
+ name: "clear",
126
+ description: "Clear context and start a fresh session",
127
+ };
123
128
  // Setup autocomplete for file paths and slash commands
124
129
  const autocompleteProvider = new CombinedAutocompleteProvider([
125
130
  thinkingCommand,
@@ -132,6 +137,7 @@ export class TuiRenderer {
132
137
  loginCommand,
133
138
  logoutCommand,
134
139
  queueCommand,
140
+ clearCommand,
135
141
  ], process.cwd());
136
142
  this.editor.setAutocompleteProvider(autocompleteProvider);
137
143
  }
@@ -199,7 +205,7 @@ export class TuiRenderer {
199
205
  // Set up custom key handlers on the editor
200
206
  this.editor.onEscape = () => {
201
207
  // Intercept Escape key when processing
202
- if (this.loadingAnimation && this.onInterruptCallback) {
208
+ if (this.loadingAnimation) {
203
209
  // Get all queued messages
204
210
  const queuedText = this.queuedMessages.join("\n\n");
205
211
  // Get current editor text
@@ -214,7 +220,7 @@ export class TuiRenderer {
214
220
  // Clear agent's queue too
215
221
  this.agent.clearMessageQueue();
216
222
  // Abort
217
- this.onInterruptCallback();
223
+ this.agent.abort();
218
224
  }
219
225
  };
220
226
  this.editor.onCtrlC = () => {
@@ -296,6 +302,12 @@ export class TuiRenderer {
296
302
  this.editor.setText("");
297
303
  return;
298
304
  }
305
+ // Check for /clear command
306
+ if (text === "/clear") {
307
+ this.handleClearCommand();
308
+ this.editor.setText("");
309
+ return;
310
+ }
299
311
  // Normal message submission - validate model and API key first
300
312
  const currentModel = this.agent.state.model;
301
313
  if (!currentModel) {
@@ -337,6 +349,8 @@ export class TuiRenderer {
337
349
  // Start the UI
338
350
  this.ui.start();
339
351
  this.isInitialized = true;
352
+ // Subscribe to agent events for UI updates and session saving
353
+ this.subscribeToAgent();
340
354
  // Set up theme file watcher for live reload
341
355
  onThemeChange(() => {
342
356
  this.ui.invalidate();
@@ -344,6 +358,20 @@ export class TuiRenderer {
344
358
  this.ui.requestRender();
345
359
  });
346
360
  }
361
+ subscribeToAgent() {
362
+ this.unsubscribe = this.agent.subscribe(async (event) => {
363
+ // Handle UI updates
364
+ await this.handleEvent(event, this.agent.state);
365
+ // Save messages to session
366
+ if (event.type === "message_end") {
367
+ this.sessionManager.saveMessage(event.message);
368
+ // Check if we should initialize session now (after first user+assistant exchange)
369
+ if (this.sessionManager.shouldInitializeSession(this.agent.state.messages)) {
370
+ this.sessionManager.startSession(this.agent.state);
371
+ }
372
+ }
373
+ });
374
+ }
347
375
  async handleEvent(event, state) {
348
376
  if (!this.isInitialized) {
349
377
  await this.init();
@@ -582,9 +610,6 @@ export class TuiRenderer {
582
610
  };
583
611
  });
584
612
  }
585
- setInterruptCallback(callback) {
586
- this.onInterruptCallback = callback;
587
- }
588
613
  handleCtrlC() {
589
614
  // Handle Ctrl+C double-press logic
590
615
  const now = Date.now();
@@ -620,8 +645,9 @@ export class TuiRenderer {
620
645
  const nextLevel = levels[nextIndex];
621
646
  // Apply the new thinking level
622
647
  this.agent.setThinkingLevel(nextLevel);
623
- // Save thinking level change to session
648
+ // Save thinking level change to session and settings
624
649
  this.sessionManager.saveThinkingLevelChange(nextLevel);
650
+ this.settingsManager.setDefaultThinkingLevel(nextLevel);
625
651
  // Update border color
626
652
  this.updateEditorBorderColor();
627
653
  // Show brief notification
@@ -631,48 +657,88 @@ export class TuiRenderer {
631
657
  }
632
658
  async cycleModel() {
633
659
  // Use scoped models if available, otherwise all available models
634
- let modelsToUse;
635
660
  if (this.scopedModels.length > 0) {
636
- modelsToUse = this.scopedModels;
661
+ // Use scoped models with thinking levels
662
+ if (this.scopedModels.length === 1) {
663
+ this.chatContainer.addChild(new Spacer(1));
664
+ this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model in scope"), 1, 0));
665
+ this.ui.requestRender();
666
+ return;
667
+ }
668
+ const currentModel = this.agent.state.model;
669
+ let currentIndex = this.scopedModels.findIndex((sm) => sm.model.id === currentModel?.id && sm.model.provider === currentModel?.provider);
670
+ // If current model not in scope, start from first
671
+ if (currentIndex === -1) {
672
+ currentIndex = 0;
673
+ }
674
+ const nextIndex = (currentIndex + 1) % this.scopedModels.length;
675
+ const nextEntry = this.scopedModels[nextIndex];
676
+ const nextModel = nextEntry.model;
677
+ const nextThinking = nextEntry.thinkingLevel;
678
+ // Validate API key
679
+ const apiKey = await getApiKeyForModel(nextModel);
680
+ if (!apiKey) {
681
+ this.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);
682
+ return;
683
+ }
684
+ // Switch model
685
+ this.agent.setModel(nextModel);
686
+ // Save model change to session and settings
687
+ this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
688
+ this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
689
+ // Apply thinking level (silently use "off" if model doesn't support thinking)
690
+ const effectiveThinking = nextModel.reasoning ? nextThinking : "off";
691
+ this.agent.setThinkingLevel(effectiveThinking);
692
+ this.sessionManager.saveThinkingLevelChange(effectiveThinking);
693
+ this.settingsManager.setDefaultThinkingLevel(effectiveThinking);
694
+ this.updateEditorBorderColor();
695
+ // Show notification
696
+ this.chatContainer.addChild(new Spacer(1));
697
+ const thinkingStr = nextModel.reasoning && nextThinking !== "off" ? ` (thinking: ${nextThinking})` : "";
698
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}${thinkingStr}`), 1, 0));
699
+ this.ui.requestRender();
637
700
  }
638
701
  else {
702
+ // Fallback to all available models (no thinking level changes)
639
703
  const { models: availableModels, error } = await getAvailableModels();
640
704
  if (error) {
641
705
  this.showError(`Failed to load models: ${error}`);
642
706
  return;
643
707
  }
644
- modelsToUse = availableModels;
645
- }
646
- if (modelsToUse.length === 0) {
647
- this.showError("No models available to cycle");
648
- return;
649
- }
650
- if (modelsToUse.length === 1) {
708
+ if (availableModels.length === 0) {
709
+ this.showError("No models available to cycle");
710
+ return;
711
+ }
712
+ if (availableModels.length === 1) {
713
+ this.chatContainer.addChild(new Spacer(1));
714
+ this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model available"), 1, 0));
715
+ this.ui.requestRender();
716
+ return;
717
+ }
718
+ const currentModel = this.agent.state.model;
719
+ let currentIndex = availableModels.findIndex((m) => m.id === currentModel?.id && m.provider === currentModel?.provider);
720
+ // If current model not in scope, start from first
721
+ if (currentIndex === -1) {
722
+ currentIndex = 0;
723
+ }
724
+ const nextIndex = (currentIndex + 1) % availableModels.length;
725
+ const nextModel = availableModels[nextIndex];
726
+ // Validate API key
727
+ const apiKey = await getApiKeyForModel(nextModel);
728
+ if (!apiKey) {
729
+ this.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);
730
+ return;
731
+ }
732
+ // Switch model
733
+ this.agent.setModel(nextModel);
734
+ // Save model change to session and settings
735
+ this.sessionManager.saveModelChange(nextModel.provider, nextModel.id);
736
+ this.settingsManager.setDefaultModelAndProvider(nextModel.provider, nextModel.id);
737
+ // Show notification
651
738
  this.chatContainer.addChild(new Spacer(1));
652
- this.chatContainer.addChild(new Text(theme.fg("dim", "Only one model in scope"), 1, 0));
739
+ this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));
653
740
  this.ui.requestRender();
654
- return;
655
- }
656
- const currentModel = this.agent.state.model;
657
- let currentIndex = modelsToUse.findIndex((m) => m.id === currentModel?.id && m.provider === currentModel?.provider);
658
- // If current model not in scope, start from first
659
- if (currentIndex === -1) {
660
- currentIndex = 0;
661
- }
662
- const nextIndex = (currentIndex + 1) % modelsToUse.length;
663
- const nextModel = modelsToUse[nextIndex];
664
- // Validate API key
665
- const apiKey = await getApiKeyForModel(nextModel);
666
- if (!apiKey) {
667
- this.showError(`No API key for ${nextModel.provider}/${nextModel.id}`);
668
- return;
669
741
  }
670
- // Switch model
671
- this.agent.setModel(nextModel);
672
- // Show notification
673
- this.chatContainer.addChild(new Spacer(1));
674
- this.chatContainer.addChild(new Text(theme.fg("dim", `Switched to ${nextModel.name || nextModel.id}`), 1, 0));
675
- this.ui.requestRender();
676
742
  }
677
743
  toggleToolOutputExpansion() {
678
744
  this.toolOutputExpanded = !this.toolOutputExpanded;
@@ -705,8 +771,9 @@ export class TuiRenderer {
705
771
  this.thinkingSelector = new ThinkingSelectorComponent(this.agent.state.thinkingLevel, (level) => {
706
772
  // Apply the selected thinking level
707
773
  this.agent.setThinkingLevel(level);
708
- // Save thinking level change to session
774
+ // Save thinking level change to session and settings
709
775
  this.sessionManager.saveThinkingLevelChange(level);
776
+ this.settingsManager.setDefaultThinkingLevel(level);
710
777
  // Update border color
711
778
  this.updateEditorBorderColor();
712
779
  // Show confirmation message with proper spacing
@@ -1107,6 +1174,35 @@ export class TuiRenderer {
1107
1174
  this.chatContainer.addChild(new DynamicBorder());
1108
1175
  this.ui.requestRender();
1109
1176
  }
1177
+ async handleClearCommand() {
1178
+ // Unsubscribe first to prevent processing abort events
1179
+ this.unsubscribe?.();
1180
+ // Abort and wait for completion
1181
+ this.agent.abort();
1182
+ await this.agent.waitForIdle();
1183
+ // Stop loading animation
1184
+ if (this.loadingAnimation) {
1185
+ this.loadingAnimation.stop();
1186
+ this.loadingAnimation = null;
1187
+ }
1188
+ this.statusContainer.clear();
1189
+ // Reset agent and session
1190
+ this.agent.reset();
1191
+ this.sessionManager.reset();
1192
+ // Resubscribe to agent
1193
+ this.subscribeToAgent();
1194
+ // Clear UI state
1195
+ this.chatContainer.clear();
1196
+ this.pendingMessagesContainer.clear();
1197
+ this.queuedMessages = [];
1198
+ this.streamingComponent = null;
1199
+ this.pendingTools.clear();
1200
+ this.isFirstUserMessage = true;
1201
+ // Show confirmation
1202
+ this.chatContainer.addChild(new Spacer(1));
1203
+ this.chatContainer.addChild(new Text(theme.fg("accent", "✓ Context cleared") + "\n" + theme.fg("muted", "Started fresh session"), 1, 1));
1204
+ this.ui.requestRender();
1205
+ }
1110
1206
  updatePendingMessagesDisplay() {
1111
1207
  this.pendingMessagesContainer.clear();
1112
1208
  if (this.queuedMessages.length > 0) {