@nghyane/arcane 0.1.28 → 0.1.30

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/package.json +4 -4
  3. package/src/cli/config-cli.ts +1 -1
  4. package/src/config/settings-schema.ts +19 -27
  5. package/src/config/settings.ts +3 -4
  6. package/src/extensibility/custom-tools/types.ts +0 -12
  7. package/src/extensibility/extensions/index.ts +0 -5
  8. package/src/extensibility/extensions/runner.ts +6 -26
  9. package/src/extensibility/extensions/types.ts +1 -77
  10. package/src/extensibility/hooks/runner.ts +5 -24
  11. package/src/extensibility/hooks/types.ts +1 -77
  12. package/src/index.ts +2 -13
  13. package/src/modes/components/footer.ts +4 -11
  14. package/src/modes/components/index.ts +0 -1
  15. package/src/modes/components/status-line/segments.ts +1 -2
  16. package/src/modes/components/status-line/types.ts +0 -1
  17. package/src/modes/components/status-line.ts +0 -6
  18. package/src/modes/components/tree-selector.ts +0 -8
  19. package/src/modes/controllers/command-controller.ts +2 -98
  20. package/src/modes/controllers/event-controller.ts +46 -52
  21. package/src/modes/controllers/extension-ui-controller.ts +0 -42
  22. package/src/modes/controllers/input-controller.ts +0 -23
  23. package/src/modes/controllers/selector-controller.ts +0 -5
  24. package/src/modes/interactive-mode.ts +3 -24
  25. package/src/modes/print-mode.ts +0 -16
  26. package/src/modes/rpc/rpc-client.ts +0 -16
  27. package/src/modes/rpc/rpc-mode.ts +0 -32
  28. package/src/modes/rpc/rpc-types.ts +0 -9
  29. package/src/modes/types.ts +1 -13
  30. package/src/modes/utils/ui-helpers.ts +2 -118
  31. package/src/prompts/agents/librarian.md +7 -12
  32. package/src/sdk.ts +0 -15
  33. package/src/session/agent-session.ts +89 -650
  34. package/src/session/compaction/branch-summarization.ts +5 -13
  35. package/src/session/compaction/index.ts +0 -1
  36. package/src/session/compaction/utils.ts +94 -2
  37. package/src/session/messages.ts +0 -37
  38. package/src/session/retry-utils.ts +1 -1
  39. package/src/session/session-manager.ts +8 -108
  40. package/src/session/session-types.ts +4 -25
  41. package/src/session/stats.ts +2 -39
  42. package/src/slash-commands/builtin-registry.ts +0 -11
  43. package/src/task/executor.ts +0 -8
  44. package/src/tools/create-tools.ts +3 -0
  45. package/src/tools/github-fs.ts +195 -0
  46. package/src/tools/github-utils.ts +35 -0
  47. package/src/tools/github.ts +35 -123
  48. package/src/tools/index.ts +1 -0
  49. package/examples/hooks/custom-compaction.ts +0 -116
  50. package/src/modes/components/compaction-summary-message.ts +0 -59
  51. package/src/prompts/compaction/compaction-short-summary.md +0 -9
  52. package/src/prompts/compaction/compaction-summary-context.md +0 -5
  53. package/src/prompts/compaction/compaction-summary.md +0 -41
  54. package/src/prompts/compaction/compaction-turn-prefix.md +0 -17
  55. package/src/prompts/compaction/compaction-update-summary.md +0 -45
  56. package/src/session/compaction/compaction.ts +0 -864
  57. package/src/session/compaction/pruning.ts +0 -91
@@ -11,11 +11,9 @@ import type { Rule } from "../../capability/rule";
11
11
  import type { ModelRegistry } from "../../config/model-registry";
12
12
  import type { ExecOptions, ExecResult } from "../../exec/exec";
13
13
  import type { EditToolDetails } from "../../patch";
14
- import type { CompactionPreparation, CompactionResult } from "../../session/compaction";
15
14
  import type { HookMessage } from "../../session/messages";
16
15
  import type {
17
16
  BranchSummaryEntry,
18
- CompactionEntry,
19
17
  ReadonlySessionManager,
20
18
  SessionEntry,
21
19
  SessionManager,
@@ -249,34 +247,6 @@ export interface SessionBranchEvent {
249
247
  previousSessionFile: string | undefined;
250
248
  }
251
249
 
252
- /** Fired before context compaction (can be cancelled) */
253
- export interface SessionBeforeCompactEvent {
254
- type: "session_before_compact";
255
- /** Compaction preparation with messages to summarize, file ops, previous summary, etc. */
256
- preparation: CompactionPreparation;
257
- /** Branch entries (root to current leaf). Use to inspect custom state or previous compactions. */
258
- branchEntries: SessionEntry[];
259
- /** Optional user-provided instructions for the summary */
260
- customInstructions?: string;
261
- /** Abort signal - hooks should pass this to LLM calls and check it periodically */
262
- signal: AbortSignal;
263
- }
264
-
265
- /** Fired before compaction summarization to customize prompts/context */
266
- export interface SessionCompactingEvent {
267
- type: "session.compacting";
268
- sessionId: string;
269
- messages: AgentMessage[];
270
- }
271
-
272
- /** Fired after context compaction */
273
- export interface SessionCompactEvent {
274
- type: "session_compact";
275
- compactionEntry: CompactionEntry;
276
- /** Whether the compaction entry was provided by a hook */
277
- fromExtension: boolean;
278
- }
279
-
280
250
  /** Fired on process exit (SIGINT/SIGTERM) */
281
251
  export interface SessionShutdownEvent {
282
252
  type: "session_shutdown";
@@ -290,7 +260,7 @@ export interface TreePreparation {
290
260
  oldLeafId: string | null;
291
261
  /** Common ancestor of target and old leaf, null if no common ancestor */
292
262
  commonAncestorId: string | null;
293
- /** Entries to summarize (old leaf back to common ancestor or compaction) */
263
+ /** Entries to summarize (old leaf back to common ancestor) */
294
264
  entriesToSummarize: SessionEntry[];
295
265
  /** Whether user chose to summarize */
296
266
  userWantsSummary: boolean;
@@ -325,9 +295,6 @@ export type SessionEvent =
325
295
  | SessionSwitchEvent
326
296
  | SessionBeforeBranchEvent
327
297
  | SessionBranchEvent
328
- | SessionBeforeCompactEvent
329
- | SessionCompactingEvent
330
- | SessionCompactEvent
331
298
  | SessionShutdownEvent
332
299
  | SessionBeforeTreeEvent
333
300
  | SessionTreeEvent;
@@ -391,21 +358,6 @@ export interface TurnEndEvent {
391
358
  toolResults: ToolResultMessage[];
392
359
  }
393
360
 
394
- /** Event data for auto_compaction_start event. */
395
- export interface AutoCompactionStartEvent {
396
- type: "auto_compaction_start";
397
- reason: "threshold" | "overflow";
398
- }
399
-
400
- /** Event data for auto_compaction_end event. */
401
- export interface AutoCompactionEndEvent {
402
- type: "auto_compaction_end";
403
- result: CompactionResult | undefined;
404
- aborted: boolean;
405
- willRetry: boolean;
406
- errorMessage?: string;
407
- }
408
-
409
361
  /** Event data for auto_retry_start event. */
410
362
  export interface AutoRetryStartEvent {
411
363
  type: "auto_retry_start";
@@ -533,8 +485,6 @@ export type HookEvent =
533
485
  | AgentEndEvent
534
486
  | TurnStartEvent
535
487
  | TurnEndEvent
536
- | AutoCompactionStartEvent
537
- | AutoCompactionEndEvent
538
488
  | AutoRetryStartEvent
539
489
  | AutoRetryEndEvent
540
490
  | TtsrTriggeredEvent
@@ -616,24 +566,6 @@ export interface SessionBeforeBranchResult {
616
566
  skipConversationRestore?: boolean;
617
567
  }
618
568
 
619
- /** Return type for session_before_compact handlers */
620
- export interface SessionBeforeCompactResult {
621
- /** If true, cancel the compaction */
622
- cancel?: boolean;
623
- /** Custom compaction result - SessionManager adds id/parentId */
624
- compaction?: CompactionResult;
625
- }
626
-
627
- /** Return type for session.compacting handlers */
628
- export interface SessionCompactingResult {
629
- /** Additional context lines to include in summary */
630
- context?: string[];
631
- /** Override the default compaction prompt */
632
- prompt?: string;
633
- /** Custom data to store in compaction entry */
634
- preserveData?: Record<string, unknown>;
635
- }
636
-
637
569
  /** Return type for session_before_tree handlers */
638
570
  export interface SessionBeforeTreeResult {
639
571
  /** If true, cancel the navigation entirely */
@@ -695,12 +627,6 @@ export interface HookAPI {
695
627
  on(event: "session_switch", handler: HookHandler<SessionSwitchEvent>): void;
696
628
  on(event: "session_before_branch", handler: HookHandler<SessionBeforeBranchEvent, SessionBeforeBranchResult>): void;
697
629
  on(event: "session_branch", handler: HookHandler<SessionBranchEvent>): void;
698
- on(
699
- event: "session_before_compact",
700
- handler: HookHandler<SessionBeforeCompactEvent, SessionBeforeCompactResult>,
701
- ): void;
702
- on(event: "session.compacting", handler: HookHandler<SessionCompactingEvent, SessionCompactingResult>): void;
703
- on(event: "session_compact", handler: HookHandler<SessionCompactEvent>): void;
704
630
  on(event: "session_shutdown", handler: HookHandler<SessionShutdownEvent>): void;
705
631
  on(event: "session_before_tree", handler: HookHandler<SessionBeforeTreeEvent, SessionBeforeTreeResult>): void;
706
632
  on(event: "session_tree", handler: HookHandler<SessionTreeEvent>): void;
@@ -712,8 +638,6 @@ export interface HookAPI {
712
638
  on(event: "agent_end", handler: HookHandler<AgentEndEvent>): void;
713
639
  on(event: "turn_start", handler: HookHandler<TurnStartEvent>): void;
714
640
  on(event: "turn_end", handler: HookHandler<TurnEndEvent>): void;
715
- on(event: "auto_compaction_start", handler: HookHandler<AutoCompactionStartEvent>): void;
716
- on(event: "auto_compaction_end", handler: HookHandler<AutoCompactionEndEvent>): void;
717
641
  on(event: "auto_retry_start", handler: HookHandler<AutoRetryStartEvent>): void;
718
642
  on(event: "auto_retry_end", handler: HookHandler<AutoRetryEndEvent>): void;
719
643
  on(event: "ttsr_triggered", handler: HookHandler<TtsrTriggeredEvent>): void;
package/src/index.ts CHANGED
@@ -13,7 +13,7 @@ export { ModelRegistry } from "./config/model-registry";
13
13
  // Prompt templates
14
14
  export type { PromptTemplate } from "./config/prompt-templates";
15
15
  export { renderPromptTemplate } from "./config/prompt-templates";
16
- export type { CompactionSettings, RetrySettings, SkillsSettings } from "./config/settings";
16
+ export type { RetrySettings, SkillsSettings } from "./config/settings";
17
17
  export { Settings, settings } from "./config/settings";
18
18
  // Custom commands
19
19
  export type {
@@ -114,7 +114,6 @@ export {
114
114
  BashExecutionComponent,
115
115
  BorderedLoader,
116
116
  BranchSummaryMessageComponent,
117
- CompactionSummaryMessageComponent,
118
117
  CustomEditor,
119
118
  CustomMessageComponent,
120
119
  DynamicBorder,
@@ -181,39 +180,29 @@ export {
181
180
  } from "./session/agent-session";
182
181
  // Auth and model registry
183
182
  export { type ApiKeyCredential, type AuthCredential, AuthStorage, type OAuthCredential } from "./session/auth-storage";
184
- // Compaction
183
+ // Branch summarization and utilities
185
184
  export {
186
185
  type BranchPreparation,
187
186
  type BranchSummaryResult,
188
187
  type CollectEntriesResult,
189
- type CompactionResult,
190
- type CutPointResult,
191
188
  calculateContextTokens,
192
189
  collectEntriesForBranchSummary,
193
- compact,
194
- DEFAULT_COMPACTION_SETTINGS,
195
190
  estimateTokens,
196
191
  type FileOperations,
197
- findCutPoint,
198
- findTurnStartIndex,
199
192
  type GenerateBranchSummaryOptions,
200
193
  generateBranchSummary,
201
- generateSummary,
202
194
  getLastAssistantUsage,
203
195
  prepareBranchEntries,
204
196
  serializeConversation,
205
- shouldCompact,
206
197
  } from "./session/compaction";
207
198
  export { convertToLlm } from "./session/messages";
208
199
  export {
209
200
  type BranchSummaryEntry,
210
201
  buildSessionContext,
211
- type CompactionEntry,
212
202
  CURRENT_SESSION_VERSION,
213
203
  type CustomEntry,
214
204
  type CustomMessageEntry,
215
205
  type FileEntry,
216
- getLatestCompactionEntry,
217
206
  type ModeChangeEntry,
218
207
  type ModelChangeEntry,
219
208
  migrateSessionEntries,
@@ -45,15 +45,10 @@ export class FooterComponent implements Component {
45
45
  #cachedBranch: string | null | undefined = undefined; // undefined = not checked yet, null = not in git repo, string = branch name
46
46
  #gitWatcher: fs.FSWatcher | null = null;
47
47
  #onBranchChange: (() => void) | null = null;
48
- #autoCompactEnabled: boolean = true;
49
48
  #extensionStatuses: Map<string, string> = new Map();
50
49
 
51
50
  constructor(private readonly session: AgentSession) {}
52
51
 
53
- setAutoCompactEnabled(enabled: boolean): void {
54
- this.#autoCompactEnabled = enabled;
55
- }
56
-
57
52
  /**
58
53
  * Set extension status text to display in the footer.
59
54
  * Text is sanitized (newlines/tabs replaced with spaces) and truncated to terminal width.
@@ -157,7 +152,7 @@ export class FooterComponent implements Component {
157
152
  render(width: number): string[] {
158
153
  const state = this.session.state;
159
154
 
160
- // Calculate cumulative usage from ALL session entries (not just post-compaction messages)
155
+ // Calculate cumulative usage from ALL session entries
161
156
  let totalInput = 0;
162
157
  let totalOutput = 0;
163
158
  let totalCacheRead = 0;
@@ -174,8 +169,7 @@ export class FooterComponent implements Component {
174
169
  }
175
170
  }
176
171
 
177
- // Calculate context usage from session (handles compaction correctly).
178
- // After compaction, tokens are unknown until the next LLM response.
172
+ // Calculate context usage from session.
179
173
  const contextUsage = this.session.getContextUsage();
180
174
  const contextWindow = contextUsage?.contextWindow ?? state.model?.contextWindow ?? 0;
181
175
  const contextPercentValue = contextUsage?.percent ?? 0;
@@ -227,11 +221,10 @@ export class FooterComponent implements Component {
227
221
 
228
222
  // Colorize context percentage based on usage
229
223
  let contextPercentStr: string;
230
- const autoIndicator = this.#autoCompactEnabled ? " (auto)" : "";
231
224
  const contextPercentDisplay =
232
225
  contextPercent === "?"
233
- ? `?/${formatTokens(contextWindow)}${autoIndicator}`
234
- : `${contextPercent}%/${formatTokens(contextWindow)}${autoIndicator}`;
226
+ ? `?/${formatTokens(contextWindow)}`
227
+ : `${contextPercent}%/${formatTokens(contextWindow)}`;
235
228
  if (contextPercentValue > 90) {
236
229
  contextPercentStr = theme.fg("error", contextPercentDisplay);
237
230
  } else if (contextPercentValue > 70) {
@@ -3,7 +3,6 @@ export { AssistantMessageComponent } from "./assistant-message";
3
3
  export { BashExecutionComponent } from "./bash-execution";
4
4
  export { BorderedLoader } from "./bordered-loader";
5
5
  export { BranchSummaryMessageComponent } from "./branch-summary-message";
6
- export { CompactionSummaryMessageComponent } from "./compaction-summary-message";
7
6
  export { CountdownTimer } from "./countdown-timer";
8
7
  export { CustomEditor } from "./custom-editor";
9
8
  export { CustomMessageComponent } from "./custom-message";
@@ -211,8 +211,7 @@ const contextPctSegment: StatusLineSegment = {
211
211
  const pct = ctx.contextPercent;
212
212
  const window = ctx.contextWindow;
213
213
 
214
- const autoIcon = ctx.autoCompactEnabled && theme.icon.auto ? ` ${theme.icon.auto}` : "";
215
- const text = `${pct.toFixed(1)}%/${formatTokens(window)}${autoIcon}`;
214
+ const text = `${pct.toFixed(1)}%/${formatTokens(window)}`;
216
215
 
217
216
  let content: string;
218
217
  if (pct > 90) {
@@ -30,7 +30,6 @@ export interface SegmentContext {
30
30
  };
31
31
  contextPercent: number;
32
32
  contextWindow: number;
33
- autoCompactEnabled: boolean;
34
33
  subagentCount: number;
35
34
  sessionStartTime: number;
36
35
  git: {
@@ -65,7 +65,6 @@ export class StatusLineComponent implements Component {
65
65
  #cachedBranch: string | null | undefined = undefined;
66
66
  #gitWatcher: fs.FSWatcher | null = null;
67
67
  #onBranchChange: (() => void) | null = null;
68
- #autoCompactEnabled: boolean = true;
69
68
  #hookStatuses: Map<string, string> = new Map();
70
69
  #subagentCount: number = 0;
71
70
  #sessionStartTime: number = Date.now();
@@ -89,10 +88,6 @@ export class StatusLineComponent implements Component {
89
88
  this.#settings = settings;
90
89
  }
91
90
 
92
- setAutoCompactEnabled(enabled: boolean): void {
93
- this.#autoCompactEnabled = enabled;
94
- }
95
-
96
91
  setSubagentCount(count: number): void {
97
92
  this.#subagentCount = count;
98
93
  }
@@ -258,7 +253,6 @@ export class StatusLineComponent implements Component {
258
253
  usageStats,
259
254
  contextPercent,
260
255
  contextWindow,
261
- autoCompactEnabled: this.#autoCompactEnabled,
262
256
  subagentCount: this.#subagentCount,
263
257
  sessionStartTime: this.#sessionStartTime,
264
258
  git: {
@@ -372,9 +372,6 @@ class TreeList implements Component {
372
372
  }
373
373
  break;
374
374
  }
375
- case "compaction":
376
- parts.push("compaction");
377
- break;
378
375
  case "branch_summary":
379
376
  parts.push("branch summary", entry.summary);
380
377
  break;
@@ -573,11 +570,6 @@ class TreeList implements Component {
573
570
  result = theme.fg("customMessageLabel", `[${entry.customType}]: `) + normalize(content);
574
571
  break;
575
572
  }
576
- case "compaction": {
577
- const tokens = Math.round(entry.tokensBefore / 1000);
578
- result = theme.fg("borderAccent", `[compaction: ${tokens}k tokens]`);
579
- break;
580
- }
581
573
  case "branch_summary":
582
574
  result = theme.fg("warning", `[branch summary]: `) + normalize(entry.summary);
583
575
  break;
@@ -9,14 +9,13 @@ import {
9
9
  type UsageReport,
10
10
  } from "@nghyane/arcane-ai";
11
11
  import { copyToClipboard } from "@nghyane/arcane-natives";
12
- import { Loader, Markdown, padding, Spacer, Text, visibleWidth } from "@nghyane/arcane-tui";
12
+ import { Markdown, padding, Spacer, Text, visibleWidth } from "@nghyane/arcane-tui";
13
13
  import { Snowflake } from "@nghyane/arcane-utils";
14
14
  import { setProjectDir } from "@nghyane/arcane-utils/dirs";
15
15
  import { $ } from "bun";
16
16
  import { reset as resetCapabilities } from "../../capability";
17
17
  import { formatKeyHint, type KeyId } from "../../config/keybindings";
18
18
  import { loadCustomShare } from "../../export/custom-share";
19
- import type { CompactOptions } from "../../extensibility/extensions/types";
20
19
  import { getGatewayStatus } from "../../ipy/gateway-coordinator";
21
20
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
21
  import { BorderedLoader } from "../../modes/components/bordered-loader";
@@ -24,8 +23,7 @@ import { DynamicBorder } from "../../modes/components/dynamic-border";
24
23
  import { PythonExecutionComponent } from "../../modes/components/python-execution";
25
24
  import type { InteractiveModeContext } from "../../modes/types";
26
25
  import type { AuthStorage } from "../../session/auth-storage";
27
- import { createCompactionSummaryMessage } from "../../session/messages";
28
- import { getMarkdownTheme, getSymbolTheme, theme } from "../../theme/theme";
26
+ import { getMarkdownTheme, theme } from "../../theme/theme";
29
27
  import { outputMeta } from "../../tools/output-meta";
30
28
  import { resolveToCwd } from "../../tools/path-utils";
31
29
  import { getChangelogPath, parseChangelog } from "../../utils/changelog";
@@ -425,12 +423,6 @@ export class CommandController {
425
423
  }
426
424
  this.ctx.statusContainer.clear();
427
425
 
428
- if (this.ctx.session.isCompacting) {
429
- this.ctx.session.abortCompaction();
430
- while (this.ctx.session.isCompacting) {
431
- await Bun.sleep(10);
432
- }
433
- }
434
426
  await this.ctx.session.newSession();
435
427
 
436
428
  this.ctx.statusLine.invalidate();
@@ -438,7 +430,6 @@ export class CommandController {
438
430
 
439
431
  this.ctx.chatContainer.clear();
440
432
  this.ctx.pendingMessagesContainer.clear();
441
- this.ctx.compactionQueuedMessages = [];
442
433
  this.ctx.streamingComponent = undefined;
443
434
  this.ctx.streamingMessage = undefined;
444
435
  this.ctx.pendingTools.clear();
@@ -604,92 +595,6 @@ export class CommandController {
604
595
  this.ctx.ui.requestRender();
605
596
  }
606
597
 
607
- async handleCompactCommand(customInstructions?: string): Promise<void> {
608
- const entries = this.ctx.sessionManager.getEntries();
609
- const messageCount = entries.filter(e => e.type === "message").length;
610
-
611
- if (messageCount < 2) {
612
- this.ctx.showWarning("Nothing to compact (no messages yet)");
613
- return;
614
- }
615
-
616
- await this.executeCompaction(customInstructions, false);
617
- }
618
-
619
- async handleSkillCommand(skillPath: string, args: string): Promise<void> {
620
- try {
621
- const content = await Bun.file(skillPath).text();
622
- const body = content.replace(/^---\n[\s\S]*?\n---\n/, "").trim();
623
- const metaLines = [`Skill: ${skillPath}`];
624
- if (args) {
625
- metaLines.push(`User: ${args}`);
626
- }
627
- const message = `${body}\n\n---\n\n${metaLines.join("\n")}`;
628
- await this.ctx.session.prompt(message);
629
- } catch (err) {
630
- this.ctx.showError(`Failed to load skill: ${err instanceof Error ? err.message : String(err)}`);
631
- }
632
- }
633
-
634
- async executeCompaction(customInstructionsOrOptions?: string | CompactOptions, isAuto = false): Promise<void> {
635
- if (this.ctx.loadingAnimation) {
636
- this.ctx.loadingAnimation.stop();
637
- this.ctx.loadingAnimation = undefined;
638
- }
639
- this.ctx.statusContainer.clear();
640
-
641
- const originalOnEscape = this.ctx.editor.onEscape;
642
- this.ctx.editor.onEscape = () => {
643
- this.ctx.session.abortCompaction();
644
- };
645
-
646
- this.ctx.chatContainer.addChild(new Spacer(1));
647
- const label = isAuto ? "Auto-compacting context... (esc to cancel)" : "Compacting context... (esc to cancel)";
648
- const compactingLoader = new Loader(
649
- this.ctx.ui,
650
- spinner => theme.fg("accent", spinner),
651
- text => theme.fg("muted", text),
652
- label,
653
- getSymbolTheme().spinnerFrames,
654
- );
655
- this.ctx.statusContainer.addChild(compactingLoader);
656
- this.ctx.ui.requestRender();
657
-
658
- try {
659
- const instructions = typeof customInstructionsOrOptions === "string" ? customInstructionsOrOptions : undefined;
660
- const options =
661
- customInstructionsOrOptions && typeof customInstructionsOrOptions === "object"
662
- ? customInstructionsOrOptions
663
- : undefined;
664
- const result = await this.ctx.session.compact(instructions, options);
665
-
666
- this.ctx.rebuildChatFromMessages();
667
-
668
- const msg = createCompactionSummaryMessage(
669
- result.summary,
670
- result.tokensBefore,
671
- new Date().toISOString(),
672
- result.shortSummary,
673
- );
674
- this.ctx.addMessageToChat(msg);
675
-
676
- this.ctx.statusLine.invalidate();
677
- this.ctx.updateEditorTopBorder();
678
- } catch (error) {
679
- const message = error instanceof Error ? error.message : String(error);
680
- if (message === "Compaction cancelled" || (error instanceof Error && error.name === "AbortError")) {
681
- this.ctx.showError("Compaction cancelled");
682
- } else {
683
- this.ctx.showError(`Compaction failed: ${message}`);
684
- }
685
- } finally {
686
- compactingLoader.stop();
687
- this.ctx.statusContainer.clear();
688
- this.ctx.editor.onEscape = originalOnEscape;
689
- }
690
- await this.ctx.flushCompactionQueue({ willRetry: false });
691
- }
692
-
693
598
  async handleHandoffCommand(customInstructions?: string): Promise<void> {
694
599
  const entries = this.ctx.sessionManager.getEntries();
695
600
  const messageCount = entries.filter(e => e.type === "message").length;
@@ -772,7 +677,6 @@ function formatDurationShort(ms: number): string {
772
677
  const hrs = hours % 24;
773
678
  if (days > 0) return `${days}d${hrs > 0 ? ` ${hrs}h` : ""}`;
774
679
  if (hours > 0) return `${hours}h${mins > 0 ? ` ${mins}m` : ""}`;
775
- if (minutes > 0) return `${minutes}m`;
776
680
  return `${totalSeconds}s`;
777
681
  }
778
682
 
@@ -1,5 +1,5 @@
1
1
  import { type AgentTool, toolDetails } from "@nghyane/arcane-agent";
2
- import { Loader, TERMINAL } from "@nghyane/arcane-tui";
2
+ import { Loader, TERMINAL, Text } from "@nghyane/arcane-tui";
3
3
  import { settings } from "../../config/settings";
4
4
  import { AssistantMessageComponent } from "../../modes/components/assistant-message";
5
5
  import { ContextGroupComponent } from "../../modes/components/context-group";
@@ -259,57 +259,6 @@ export class EventController {
259
259
  this.sendCompletionNotification();
260
260
  break;
261
261
 
262
- case "auto_compaction_start": {
263
- this.ctx.autoCompactionEscapeHandler = this.ctx.editor.onEscape;
264
- this.ctx.editor.onEscape = () => {
265
- this.ctx.session.abortCompaction();
266
- };
267
- this.ctx.statusContainer.clear();
268
- const reasonText = event.reason === "overflow" ? "Context overflow detected, " : "";
269
- this.ctx.autoCompactionLoader = new Loader(
270
- this.ctx.ui,
271
- spinner => theme.fg("accent", spinner),
272
- text => theme.fg("muted", text),
273
- `${reasonText}Auto-compacting… (esc to cancel)`,
274
- getSymbolTheme().spinnerFrames,
275
- );
276
- this.ctx.statusContainer.addChild(this.ctx.autoCompactionLoader);
277
- this.ctx.ui.requestRender();
278
- break;
279
- }
280
-
281
- case "auto_compaction_end": {
282
- if (this.ctx.autoCompactionEscapeHandler) {
283
- this.ctx.editor.onEscape = this.ctx.autoCompactionEscapeHandler;
284
- this.ctx.autoCompactionEscapeHandler = undefined;
285
- }
286
- if (this.ctx.autoCompactionLoader) {
287
- this.ctx.autoCompactionLoader.stop();
288
- this.ctx.autoCompactionLoader = undefined;
289
- this.ctx.statusContainer.clear();
290
- }
291
- if (event.aborted) {
292
- this.ctx.showStatus("Auto-compaction cancelled");
293
- } else if (event.result) {
294
- this.ctx.chatContainer.clear();
295
- this.ctx.rebuildChatFromMessages();
296
- this.ctx.addMessageToChat({
297
- role: "compactionSummary",
298
- tokensBefore: event.result.tokensBefore,
299
- summary: event.result.summary,
300
- shortSummary: event.result.shortSummary,
301
- timestamp: Date.now(),
302
- });
303
- this.ctx.statusLine.invalidate();
304
- this.ctx.updateEditorTopBorder();
305
- } else {
306
- this.ctx.showWarning("Auto-compaction failed; continuing without compaction");
307
- }
308
- await this.ctx.flushCompactionQueue({ willRetry: event.willRetry });
309
- this.ctx.ui.requestRender();
310
- break;
311
- }
312
-
313
262
  case "auto_retry_start": {
314
263
  this.ctx.retryEscapeHandler = this.ctx.editor.onEscape;
315
264
  this.ctx.editor.onEscape = () => {
@@ -348,6 +297,51 @@ export class EventController {
348
297
  break;
349
298
  }
350
299
 
300
+ case "context_warning": {
301
+ this.ctx.statusContainer.clear();
302
+ const warningMsg = `Context usage at ${event.percent}% — session will auto-handoff soon`;
303
+ this.ctx.statusContainer.addChild(
304
+ new Text(theme.fg("warning", `${theme.status.warning} ${warningMsg}`), 0, 0),
305
+ );
306
+ this.ctx.ui.requestRender();
307
+ break;
308
+ }
309
+
310
+ case "auto_handoff_start": {
311
+ this.ctx.statusContainer.clear();
312
+ this.ctx.handoffLoader = new Loader(
313
+ this.ctx.ui,
314
+ spinner => theme.fg("warning", spinner),
315
+ text => theme.fg("muted", text),
316
+ "Auto-handoff in progress…",
317
+ getSymbolTheme().spinnerFrames,
318
+ );
319
+ this.ctx.statusContainer.addChild(this.ctx.handoffLoader);
320
+ this.ctx.ui.requestRender();
321
+ break;
322
+ }
323
+
324
+ case "auto_handoff_end": {
325
+ if (this.ctx.handoffLoader) {
326
+ this.ctx.handoffLoader.stop();
327
+ this.ctx.handoffLoader = undefined;
328
+ this.ctx.statusContainer.clear();
329
+ }
330
+ if (event.success) {
331
+ this.ctx.statusContainer.addChild(
332
+ new Text(
333
+ theme.fg("success", `${theme.status.success} Session handed off — continuing in new session`),
334
+ 0,
335
+ 0,
336
+ ),
337
+ );
338
+ } else if (event.error) {
339
+ this.ctx.showError(`Auto-handoff failed: ${event.error}`);
340
+ }
341
+ this.ctx.ui.requestRender();
342
+ break;
343
+ }
344
+
351
345
  case "ttsr_triggered": {
352
346
  this.#finalizeContextGroup();
353
347
  const component = new TtsrNotificationComponent(event.rules);
@@ -133,12 +133,6 @@ export class ExtensionUiController {
133
133
  // Signal shutdown request (will be handled by main loop)
134
134
  },
135
135
  getContextUsage: () => this.ctx.session.getContextUsage(),
136
- compact: async instructionsOrOptions => {
137
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
138
- const options =
139
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
140
- await this.ctx.session.compact(instructions, options);
141
- },
142
136
  getSystemPrompt: () => this.ctx.session.systemPrompt,
143
137
  };
144
138
  const commandActions: ExtensionCommandContextActions = {
@@ -174,7 +168,6 @@ export class ExtensionUiController {
174
168
  // Clear UI state
175
169
  this.ctx.chatContainer.clear();
176
170
  this.ctx.pendingMessagesContainer.clear();
177
- this.ctx.compactionQueuedMessages = [];
178
171
  this.ctx.streamingComponent = undefined;
179
172
  this.ctx.streamingMessage = undefined;
180
173
  this.ctx.pendingTools.clear();
@@ -220,16 +213,6 @@ export class ExtensionUiController {
220
213
 
221
214
  return { cancelled: false };
222
215
  },
223
- compact: async instructionsOrOptions => {
224
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
225
- const options =
226
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
227
- if (this.ctx.isBackgrounded) {
228
- await this.ctx.session.compact(instructions, options);
229
- return;
230
- }
231
- await this.ctx.executeCompaction(instructionsOrOptions, false);
232
- },
233
216
  switchSession: async sessionPath => {
234
217
  const result = await this.ctx.session.switchSession(sessionPath);
235
218
  if (!result) {
@@ -322,12 +305,6 @@ export class ExtensionUiController {
322
305
  // Signal shutdown request (will be handled by main loop)
323
306
  },
324
307
  getContextUsage: () => this.ctx.session.getContextUsage(),
325
- compact: async instructionsOrOptions => {
326
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
327
- const options =
328
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
329
- await this.ctx.session.compact(instructions, options);
330
- },
331
308
  getSystemPrompt: () => this.ctx.session.systemPrompt,
332
309
  };
333
310
  const commandActions: ExtensionCommandContextActions = {
@@ -369,7 +346,6 @@ export class ExtensionUiController {
369
346
  // Clear UI state
370
347
  this.ctx.chatContainer.clear();
371
348
  this.ctx.pendingMessagesContainer.clear();
372
- this.ctx.compactionQueuedMessages = [];
373
349
  this.ctx.streamingComponent = undefined;
374
350
  this.ctx.streamingMessage = undefined;
375
351
  this.ctx.pendingTools.clear();
@@ -421,16 +397,6 @@ export class ExtensionUiController {
421
397
 
422
398
  return { cancelled: false };
423
399
  },
424
- compact: async instructionsOrOptions => {
425
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
426
- const options =
427
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
428
- if (this.ctx.isBackgrounded) {
429
- await this.ctx.session.compact(instructions, options);
430
- return;
431
- }
432
- await this.ctx.executeCompaction(instructionsOrOptions, false);
433
- },
434
400
  switchSession: async sessionPath => {
435
401
  if (this.ctx.isBackgrounded) {
436
402
  return { cancelled: true };
@@ -497,14 +463,6 @@ export class ExtensionUiController {
497
463
  await registeredTool.definition.onSession(event, {
498
464
  ui: uiContext,
499
465
  getContextUsage: () => this.ctx.session.getContextUsage(),
500
- compact: async instructionsOrOptions => {
501
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
502
- const options =
503
- instructionsOrOptions && typeof instructionsOrOptions === "object"
504
- ? instructionsOrOptions
505
- : undefined;
506
- await this.ctx.session.compact(instructions, options);
507
- },
508
466
  hasUI: !this.ctx.isBackgrounded,
509
467
  cwd: this.ctx.sessionManager.getCwd(),
510
468
  sessionManager: this.ctx.session.sessionManager,