@pellux/goodvibes-tui 0.26.0 → 0.28.0

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 (148) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -7
  3. package/docs/foundation-artifacts/operator-contract.json +4 -1
  4. package/package.json +2 -2
  5. package/src/cli/bundle-command.ts +2 -1
  6. package/src/cli/service-posture.ts +2 -1
  7. package/src/core/context-usage.ts +53 -0
  8. package/src/core/conversation-rendering.ts +2 -2
  9. package/src/core/conversation.ts +53 -8
  10. package/src/core/session-recovery.ts +26 -18
  11. package/src/core/system-message-router.ts +42 -26
  12. package/src/core/turn-event-wiring.ts +13 -16
  13. package/src/daemon/handlers/calendar/caldav-client.ts +2 -1
  14. package/src/daemon/handlers/inbox/index.ts +2 -1
  15. package/src/daemon/handlers/inbox/poller.ts +2 -1
  16. package/src/daemon/handlers/inbox/providers/discord.ts +2 -1
  17. package/src/daemon/handlers/inbox/providers/email.ts +2 -1
  18. package/src/daemon/handlers/inbox/providers/route-util.ts +2 -1
  19. package/src/daemon/handlers/inbox/providers/slack.ts +2 -1
  20. package/src/daemon/handlers/triage/integration.ts +2 -1
  21. package/src/daemon/handlers/triage/pipeline.ts +2 -1
  22. package/src/daemon/handlers/triage/tagger/discord.ts +2 -1
  23. package/src/daemon/handlers/triage/tagger/imap.ts +4 -3
  24. package/src/export/gist-uploader.ts +3 -1
  25. package/src/input/command-registry.ts +1 -0
  26. package/src/input/commands/cloudflare-runtime.ts +2 -1
  27. package/src/input/commands/eval.ts +4 -3
  28. package/src/input/commands/guidance-runtime.ts +2 -4
  29. package/src/input/commands/health-runtime.ts +13 -7
  30. package/src/input/commands/knowledge.ts +2 -1
  31. package/src/input/commands/runtime-services.ts +40 -10
  32. package/src/input/handler-command-route.ts +1 -1
  33. package/src/input/handler-feed-routes.ts +61 -4
  34. package/src/input/handler-feed.ts +13 -0
  35. package/src/input/handler-interactions.ts +2 -1
  36. package/src/input/handler-modal-routes.ts +2 -1
  37. package/src/input/handler-onboarding-cloudflare.ts +2 -1
  38. package/src/input/handler-onboarding.ts +8 -8
  39. package/src/input/handler-picker-routes.ts +18 -13
  40. package/src/input/handler-ui-state.ts +5 -1
  41. package/src/input/keybindings.ts +5 -0
  42. package/src/input/model-picker.ts +5 -0
  43. package/src/input/panel-integration-actions.ts +10 -0
  44. package/src/input/tts-settings-actions.ts +2 -1
  45. package/src/main.ts +52 -60
  46. package/src/panels/agent-inspector-panel.ts +90 -15
  47. package/src/panels/agent-inspector-shared.ts +3 -3
  48. package/src/panels/agent-logs-panel.ts +149 -72
  49. package/src/panels/approval-panel.ts +146 -95
  50. package/src/panels/automation-control-panel.ts +24 -1
  51. package/src/panels/base-panel.ts +28 -3
  52. package/src/panels/builtin/agent.ts +3 -1
  53. package/src/panels/builtin/development.ts +2 -1
  54. package/src/panels/builtin/session.ts +1 -1
  55. package/src/panels/cockpit-panel.ts +24 -5
  56. package/src/panels/cockpit-read-model.ts +2 -1
  57. package/src/panels/communication-panel.ts +108 -43
  58. package/src/panels/context-visualizer-panel.ts +49 -15
  59. package/src/panels/control-plane-panel.ts +27 -1
  60. package/src/panels/cost-tracker-panel.ts +87 -65
  61. package/src/panels/debug-panel.ts +61 -37
  62. package/src/panels/diff-panel.ts +59 -30
  63. package/src/panels/docs-panel.ts +30 -24
  64. package/src/panels/eval-panel.ts +130 -81
  65. package/src/panels/expandable-list-panel.ts +189 -0
  66. package/src/panels/file-explorer-panel.ts +42 -42
  67. package/src/panels/file-preview-panel.ts +11 -3
  68. package/src/panels/forensics-panel.ts +27 -13
  69. package/src/panels/git-panel.ts +65 -84
  70. package/src/panels/hooks-panel.ts +36 -2
  71. package/src/panels/incident-review-panel.ts +31 -10
  72. package/src/panels/intelligence-panel.ts +152 -71
  73. package/src/panels/knowledge-graph-panel.ts +89 -27
  74. package/src/panels/local-auth-panel.ts +17 -11
  75. package/src/panels/marketplace-panel.ts +33 -13
  76. package/src/panels/memory-panel.ts +7 -5
  77. package/src/panels/ops-control-panel.ts +69 -7
  78. package/src/panels/ops-strategy-panel.ts +61 -43
  79. package/src/panels/orchestration-panel.ts +34 -4
  80. package/src/panels/panel-list-panel.ts +33 -8
  81. package/src/panels/panel-manager.ts +23 -4
  82. package/src/panels/plan-dashboard-panel.ts +183 -66
  83. package/src/panels/plugins-panel.ts +48 -10
  84. package/src/panels/policy-panel.ts +60 -23
  85. package/src/panels/polish-core.ts +157 -0
  86. package/src/panels/polish-tables.ts +258 -0
  87. package/src/panels/polish.ts +44 -145
  88. package/src/panels/project-planning-panel.ts +27 -4
  89. package/src/panels/provider-accounts-panel.ts +55 -18
  90. package/src/panels/provider-health-panel.ts +118 -87
  91. package/src/panels/provider-stats-panel.ts +47 -22
  92. package/src/panels/qr-panel.ts +23 -11
  93. package/src/panels/remote-panel.ts +12 -5
  94. package/src/panels/routes-panel.ts +27 -5
  95. package/src/panels/sandbox-panel.ts +62 -27
  96. package/src/panels/schedule-panel.ts +44 -24
  97. package/src/panels/scrollable-list-panel.ts +124 -10
  98. package/src/panels/security-panel.ts +72 -20
  99. package/src/panels/services-panel.ts +68 -22
  100. package/src/panels/session-browser-panel.ts +42 -26
  101. package/src/panels/session-maintenance.ts +2 -2
  102. package/src/panels/settings-sync-panel.ts +30 -15
  103. package/src/panels/skills-panel.ts +2 -1
  104. package/src/panels/subscription-panel.ts +21 -3
  105. package/src/panels/symbol-outline-panel.ts +40 -47
  106. package/src/panels/system-messages-panel.ts +60 -27
  107. package/src/panels/tasks-panel.ts +36 -14
  108. package/src/panels/thinking-panel.ts +32 -36
  109. package/src/panels/token-budget-panel.ts +80 -53
  110. package/src/panels/tool-inspector-panel.ts +37 -39
  111. package/src/panels/types.ts +25 -0
  112. package/src/panels/watchers-panel.ts +25 -5
  113. package/src/panels/work-plan-panel.ts +127 -35
  114. package/src/panels/worktree-panel.ts +65 -20
  115. package/src/panels/wrfc-panel-format.ts +133 -0
  116. package/src/panels/wrfc-panel.ts +53 -147
  117. package/src/renderer/agent-detail-modal.ts +2 -2
  118. package/src/renderer/compaction-preview.ts +2 -1
  119. package/src/renderer/compositor.ts +46 -43
  120. package/src/renderer/modal-utils.ts +0 -10
  121. package/src/renderer/model-picker-overlay.ts +9 -4
  122. package/src/renderer/model-workspace.ts +2 -3
  123. package/src/renderer/panel-composite.ts +7 -20
  124. package/src/renderer/panel-workspace-bar.ts +14 -8
  125. package/src/renderer/process-modal.ts +2 -2
  126. package/src/renderer/progress.ts +3 -2
  127. package/src/renderer/tab-strip.ts +148 -34
  128. package/src/renderer/ui-factory.ts +4 -6
  129. package/src/runtime/bootstrap-command-context.ts +3 -0
  130. package/src/runtime/bootstrap-command-parts.ts +3 -1
  131. package/src/runtime/bootstrap-core.ts +31 -31
  132. package/src/runtime/bootstrap-shell.ts +1 -0
  133. package/src/runtime/onboarding/apply.ts +4 -3
  134. package/src/runtime/onboarding/snapshot.ts +4 -3
  135. package/src/runtime/process-lifecycle.ts +195 -0
  136. package/src/runtime/services.ts +4 -1
  137. package/src/runtime/ui/model-picker/health-enrichment.ts +3 -0
  138. package/src/runtime/ui/model-picker/types.ts +4 -0
  139. package/src/runtime/wrfc-persistence.ts +20 -5
  140. package/src/shell/ui-openers.ts +3 -2
  141. package/src/utils/format-duration.ts +55 -0
  142. package/src/utils/format-number.ts +71 -0
  143. package/src/verification/live-verifier.ts +4 -3
  144. package/src/version.ts +1 -1
  145. package/src/core/context-auto-compact.ts +0 -110
  146. package/src/panels/panel-picker.ts +0 -106
  147. package/src/renderer/panel-picker-overlay.ts +0 -202
  148. package/src/renderer/panel-tab-bar.ts +0 -69
@@ -1,6 +1,5 @@
1
1
  import type { UiRuntimeEvents } from '@/runtime/index.ts';
2
2
  import { buildPersistedSessionContext, persistConversation } from '@/runtime/index.ts';
3
- import { maybeAutoCompact } from './context-auto-compact.ts';
4
3
  import { logger } from '@pellux/goodvibes-sdk/platform/utils';
5
4
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
5
  import type { HookDispatcher, HookPhase, HookCategory, HookEventPath } from '@pellux/goodvibes-sdk/platform/hooks';
@@ -20,6 +19,7 @@ interface TurnOrchestrator {
20
19
  /** Minimal provider registry surface required by turn-event wiring. */
21
20
  interface TurnProviderRegistry {
22
21
  getCurrentModel(): { readonly contextWindow: number };
22
+ getContextWindowForModel(model: { readonly contextWindow: number }): number;
23
23
  }
24
24
 
25
25
  /** Minimal config manager surface required by turn-event wiring. */
@@ -80,17 +80,21 @@ export interface WireTurnEventHandlersResult {
80
80
  * Responsibilities:
81
81
  * - Auto-save conversation to persistent store after each LLM turn
82
82
  * - Fire the Lifecycle:session:save hook
83
- * - Trigger auto-compact when context usage exceeds the configured threshold
84
83
  * - Refresh git status after turns and tool results
85
84
  *
85
+ * Note: auto-compaction is intentionally NOT performed here. The SDK
86
+ * Orchestrator already runs post-turn context maintenance every turn
87
+ * (handlePostTurnContextMaintenance) using the resolved context window and
88
+ * its own getAutoCompactDecision; re-triggering it from the TUI duplicated
89
+ * that work and raced the SDK's compaction on the shared message array.
90
+ *
86
91
  * Returns refreshGit (callable externally) and unsubs (push into parent unsubs).
87
92
  */
88
93
  export function wireTurnEventHandlers(
89
94
  options: WireTurnEventHandlersOptions,
90
95
  ): WireTurnEventHandlersResult {
91
96
  const {
92
- events, conversation, runtime, orchestrator, configManager,
93
- providerRegistry, systemMessageRouter, hookDispatcher,
97
+ events, conversation, runtime, configManager, hookDispatcher,
94
98
  workingDir, homeDirectory, sessionManager, gitStatusProvider,
95
99
  lastGitInfoRef, buildSessionContinuityHints, render, webhookNotifier,
96
100
  _clock = Date.now,
@@ -161,18 +165,11 @@ export function wireTurnEventHandlers(
161
165
  } catch { /* best-effort */ }
162
166
  logger.debug('auto-save on turn:complete failed', { error: summarizeError(e) });
163
167
  }
164
- // Auto-compact: check context usage and compact if threshold exceeded
165
- const currentModelForCompact = providerRegistry.getCurrentModel();
166
- maybeAutoCompact({
167
- configManager: configManager as Parameters<typeof maybeAutoCompact>[0]['configManager'],
168
- conversation,
169
- providerRegistry: providerRegistry as Parameters<typeof maybeAutoCompact>[0]['providerRegistry'],
170
- systemMessageRouter: systemMessageRouter as Parameters<typeof maybeAutoCompact>[0]['systemMessageRouter'],
171
- model: runtime.model,
172
- provider: runtime.provider,
173
- lastInputTokens: orchestrator.lastInputTokens,
174
- contextWindow: currentModelForCompact.contextWindow,
175
- }).catch((err: unknown) => logger.debug('maybeAutoCompact error', { error: summarizeError(err) }));
168
+ // Auto-compaction is owned by the SDK Orchestrator's post-turn maintenance
169
+ // (handlePostTurnContextMaintenance), which runs on every turn against the
170
+ // resolved context window (getContextWindowForModel) via the SDK's
171
+ // getAutoCompactDecision (percentage threshold + 15k safety buffer +
172
+ // small-window handling). The TUI must not independently re-trigger it.
176
173
  refreshGit();
177
174
  }));
178
175
 
@@ -26,6 +26,7 @@ import {
26
26
  type GenerateICalInput,
27
27
  type ParsedICalEvent,
28
28
  } from './ics.ts';
29
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
29
30
 
30
31
  // ---------------------------------------------------------------------------
31
32
  // Configuration
@@ -586,7 +587,7 @@ export function createCalDavClient(options: CreateCalDavClientOptions): CalDavCl
586
587
  eventIds.push(toRelativeHref(joinUrl(collectionPathOrRoot(collectionUrl, config.baseUrl), resourcePath)));
587
588
  imported += 1;
588
589
  } catch (error) {
589
- const message = error instanceof HandlerError ? error.message : error instanceof Error ? error.message : String(error);
590
+ const message = error instanceof HandlerError ? error.message : summarizeError(error);
590
591
  errors.push(`${uid}: ${message}`);
591
592
  }
592
593
  }
@@ -40,6 +40,7 @@ import { InboundPoller } from './poller.ts';
40
40
  import { createSlackAdapter, SLACK_PROVIDER_ID } from './providers/slack.ts';
41
41
  import { createDiscordAdapter, DISCORD_PROVIDER_ID } from './providers/discord.ts';
42
42
  import { createEmailAdapter, EMAIL_PROVIDER_ID } from './providers/email.ts';
43
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
43
44
 
44
45
  export const INBOX_LIST_METHOD_ID = 'channels.inbox.list';
45
46
  const DEFAULT_LIMIT = 50;
@@ -173,7 +174,7 @@ export function registerInboxMethods(
173
174
  poller.start();
174
175
  })().catch((error: unknown) => {
175
176
  ctx.logger.error('inbox surface bootstrap failed', {
176
- error: error instanceof Error ? error.message : String(error),
177
+ error: summarizeError(error),
177
178
  });
178
179
  });
179
180
 
@@ -17,6 +17,7 @@
17
17
  import type { InboundProviderAdapter, ProviderState } from './provider-adapter.ts';
18
18
  import type { InboxCursorStore } from './cursor-store.ts';
19
19
  import type { HandlerLogger } from '../context.ts';
20
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
20
21
 
21
22
  export interface ProviderStatus {
22
23
  id: string;
@@ -115,7 +116,7 @@ export class InboundPoller {
115
116
  lastPolledAt: Date.now(),
116
117
  });
117
118
  } catch (error) {
118
- const message = error instanceof Error ? error.message : String(error);
119
+ const message = summarizeError(error);
119
120
  this.logger.warn('inbound poll failed', { provider: id, error: message });
120
121
  this.setStatus(id, {
121
122
  id,
@@ -31,6 +31,7 @@ import type {
31
31
  import { POLL_CADENCE_MS } from '../provider-adapter.ts';
32
32
  import { digestSender, toBodyPreview, toSubjectPreview } from '../mapping.ts';
33
33
  import { resolveRouteId } from './route-util.ts';
34
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
34
35
 
35
36
  const DISCORD_API = 'https://discord.com/api/v10';
36
37
  // Discord epoch (2015-01-01T00:00:00Z) in ms, used to decode snowflakes.
@@ -247,5 +248,5 @@ function unavailable(error: string): ProviderPollResult {
247
248
  }
248
249
 
249
250
  function errMsg(error: unknown): string {
250
- return error instanceof Error ? error.message : String(error);
251
+ return summarizeError(error);
251
252
  }
@@ -25,6 +25,7 @@ import { digestSender, toBodyPreview, toSubjectPreview } from '../mapping.ts';
25
25
  import { resolveRouteId } from './route-util.ts';
26
26
  import { ImapClient } from './imap-client.ts';
27
27
  import type { ImapConfig, ImapEnvelope } from './imap-client.ts';
28
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
28
29
 
29
30
  export const EMAIL_PROVIDER_ID = 'email';
30
31
  export const EMAIL_HOST_KEY = 'surfaces.email.imapHost';
@@ -147,5 +148,5 @@ function unavailable(error: string): ProviderPollResult {
147
148
  }
148
149
 
149
150
  function errMsg(error: unknown): string {
150
- return error instanceof Error ? error.message : String(error);
151
+ return summarizeError(error);
151
152
  }
@@ -3,6 +3,7 @@
3
3
  // surface) so a route lookup can never crash a provider poll.
4
4
 
5
5
  import type { AdapterContext, InboundChannelItem } from '../provider-adapter.ts';
6
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
6
7
 
7
8
  export async function resolveRouteId(
8
9
  ctx: AdapterContext,
@@ -16,7 +17,7 @@ export async function resolveRouteId(
16
17
  } catch (error) {
17
18
  ctx.logger.warn('route resolution failed', {
18
19
  provider,
19
- error: error instanceof Error ? error.message : String(error),
20
+ error: summarizeError(error),
20
21
  });
21
22
  return undefined;
22
23
  }
@@ -30,6 +30,7 @@ import type {
30
30
  import { POLL_CADENCE_MS } from '../provider-adapter.ts';
31
31
  import { digestSender, toBodyPreview, toSubjectPreview } from '../mapping.ts';
32
32
  import { resolveRouteId } from './route-util.ts';
33
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
33
34
 
34
35
  const SLACK_API = 'https://slack.com/api';
35
36
  export const SLACK_PROVIDER_ID = 'slack';
@@ -258,5 +259,5 @@ function unavailable(error: string): ProviderPollResult {
258
259
  }
259
260
 
260
261
  function errMsg(error: unknown): string {
261
- return error instanceof Error ? error.message : String(error);
262
+ return summarizeError(error);
262
263
  }
@@ -47,6 +47,7 @@ import {
47
47
  type TriageTagger,
48
48
  type TriageTaggerOptions,
49
49
  } from './tagger/index.ts';
50
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
50
51
 
51
52
  /**
52
53
  * Canonical id of the inbox list method whose handler we decorate. This is the
@@ -151,7 +152,7 @@ function withInboxEnrichment(ctx: HandlerContext): EnrichmentProxy {
151
152
  // Triage is best-effort: a missing/locked store must never break the
152
153
  // read-only inbox feed. Log and return the un-enriched result.
153
154
  ctx.logger.warn('triage: inbox enrichment skipped', {
154
- message: error instanceof Error ? error.message : String(error),
155
+ message: summarizeError(error),
155
156
  });
156
157
  return result;
157
158
  }
@@ -23,6 +23,7 @@ import {
23
23
  type TriageScore,
24
24
  type TriageScorerOptions,
25
25
  } from './scorer.ts';
26
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
26
27
 
27
28
  export const TRIAGE_STORE_FILE = 'inbox-triage.sqlite';
28
29
 
@@ -158,7 +159,7 @@ export async function runInboxTriage(
158
159
  if (ownsStore) await store.save();
159
160
  } catch (error) {
160
161
  ctx.logger.error('triage: failed to persist scores', {
161
- message: error instanceof Error ? error.message : String(error),
162
+ message: summarizeError(error),
162
163
  });
163
164
  throw error;
164
165
  } finally {
@@ -19,6 +19,7 @@ import { HandlerError } from '../../errors.ts';
19
19
  import type { InboundChannelItem } from '../types.ts';
20
20
  import type { ApplyTagsResult, TaggerProviderConfig } from './shared.ts';
21
21
  import { discordEmojiForTag, stringMeta } from './shared.ts';
22
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
22
23
 
23
24
  export async function applyDiscord(
24
25
  item: InboundChannelItem,
@@ -179,7 +180,7 @@ async function fetchDiscordAppliedTags(
179
180
  return current.filter((t): t is string => typeof t === 'string');
180
181
  } catch (error) {
181
182
  ctx.logger.warn('triage: discord thread tag read errored', {
182
- message: error instanceof Error ? error.message : String(error),
183
+ message: summarizeError(error),
183
184
  });
184
185
  return null;
185
186
  }
@@ -14,6 +14,7 @@ import type { DaemonCredentialStore } from '../../credentials.ts';
14
14
  import type { InboundChannelItem } from '../types.ts';
15
15
  import type { ApplyTagsResult, TaggerProviderConfig } from './shared.ts';
16
16
  import { imapKeywordForTag } from './shared.ts';
17
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
17
18
 
18
19
  export interface ImapStoreArgs {
19
20
  host: string;
@@ -227,7 +228,7 @@ export function imapStoreFlagOverTls(
227
228
  reject(
228
229
  err instanceof ImapStoreError
229
230
  ? err
230
- : new ImapStoreError(err instanceof Error ? err.message : String(err), false),
231
+ : new ImapStoreError(summarizeError(err), false),
231
232
  );
232
233
  return;
233
234
  }
@@ -288,7 +289,7 @@ export function imapStoreFlagOverTls(
288
289
  finish(
289
290
  err instanceof ImapStoreError
290
291
  ? err
291
- : new ImapStoreError(err instanceof Error ? err.message : String(err), false),
292
+ : new ImapStoreError(summarizeError(err), false),
292
293
  ),
293
294
  );
294
295
  return;
@@ -318,7 +319,7 @@ export function imapStoreFlagOverTls(
318
319
  };
319
320
 
320
321
  socket.on('error', (err) =>
321
- finish(new ImapStoreError(err instanceof Error ? err.message : String(err), true)),
322
+ finish(new ImapStoreError(summarizeError(err), true)),
322
323
  );
323
324
  socket.on('close', () => {
324
325
  if (completed) finish();
@@ -16,6 +16,8 @@
16
16
  // the URL can view it).
17
17
  // ---------------------------------------------------------------------------
18
18
 
19
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
20
+
19
21
  export type UploadResult =
20
22
  | { ok: true; url: string }
21
23
  | { ok: false; error: string };
@@ -96,7 +98,7 @@ export class GistUploadTarget implements UploadTarget {
96
98
  body,
97
99
  });
98
100
  } catch (fetchErr: unknown) {
99
- const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
101
+ const msg = summarizeError(fetchErr);
100
102
  return { ok: false, error: `Network error: ${msg}` };
101
103
  }
102
104
 
@@ -139,6 +139,7 @@ export interface CommandSessionServices {
139
139
  readonly sessionManager?: import('@pellux/goodvibes-sdk/platform/sessions').SessionManager;
140
140
  readonly sessionMemoryStore?: import('@pellux/goodvibes-sdk/platform/core').SessionMemoryStore;
141
141
  readonly sessionLineageTracker?: import('@pellux/goodvibes-sdk/platform/core').SessionLineageTracker;
142
+ readonly wrfcController?: import('@pellux/goodvibes-sdk/platform/agents').WrfcController;
142
143
  readonly changeTracker?: import('@pellux/goodvibes-sdk/platform/sessions').SessionChangeTracker;
143
144
  }
144
145
 
@@ -11,6 +11,7 @@ import {
11
11
  } from '../../runtime/cloudflare-control-plane.ts';
12
12
  import type { CommandContext, CommandRegistry } from '../command-registry.ts';
13
13
  import { requireShellPaths } from './runtime-services.ts';
14
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
14
15
 
15
16
  interface ParsedCloudflareArgs {
16
17
  readonly positional: readonly string[];
@@ -366,5 +367,5 @@ function formatCloudflareError(error: unknown): string {
366
367
  if (error instanceof CloudflareDaemonRouteError) {
367
368
  return `${error.message} (HTTP ${error.status}, ${error.code})`;
368
369
  }
369
- return error instanceof Error ? error.message : String(error);
370
+ return summarizeError(error);
370
371
  }
@@ -109,8 +109,9 @@ async function handleCompare(args: string[], context: CommandContext): Promise<v
109
109
  // ── /eval gate ────────────────────────────────────────────────────────────────
110
110
 
111
111
  async function handleGate(args: string[], context: CommandContext): Promise<void> {
112
- const suiteName = args[0];
113
- const baselineFile = args[1] ?? '.goodvibes/eval/baseline.json';
112
+ const positionals = args.filter(a => !a.startsWith('--'));
113
+ const suiteName = positionals[0];
114
+ const baselineFile = positionals[1] ?? '.goodvibes/eval/baseline.json';
114
115
  const saveFlag = args.includes('--save-baseline');
115
116
  const projectRoot = requireShellPaths(context).workingDirectory;
116
117
 
@@ -141,7 +142,7 @@ async function handleGate(args: string[], context: CommandContext): Promise<void
141
142
  context.print(formatGateResult(gate));
142
143
 
143
144
  if (saveFlag || !baseline) {
144
- const label = args[0] ?? 'latest';
145
+ const label = positionals[0] ?? 'latest';
145
146
  const newBaseline = captureBaseline(label, [fresh]);
146
147
  try {
147
148
  await writeBaseline(baselineFile, newBaseline, projectRoot);
@@ -2,7 +2,7 @@ import { estimateConversationTokens } from '@pellux/goodvibes-sdk/platform/core'
2
2
  import { evaluateSessionMaintenance, formatSessionMaintenanceLines, getGuidanceMode } from '@/runtime/index.ts';
3
3
  import { dismissGuidance, evaluateContextualGuidance, formatGuidanceItems, resetGuidance } from '@/runtime/index.ts';
4
4
  import type { CommandRegistry } from '../command-registry.ts';
5
- import { requireProviderApi, requireReadModels, requireSessionMemoryStore, requireShellPaths } from './runtime-services.ts';
5
+ import { requireReadModels, requireSessionMemoryStore, requireShellPaths } from './runtime-services.ts';
6
6
 
7
7
  export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void {
8
8
  registry.register({
@@ -68,8 +68,6 @@ export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void
68
68
  return;
69
69
  }
70
70
 
71
- const providerApi = requireProviderApi(ctx);
72
- const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
73
71
  const llmMessages = ctx.session.conversationManager.getMessagesForLLM();
74
72
  const readModels = requireReadModels(ctx);
75
73
  const session = readModels.session.getSnapshot();
@@ -80,7 +78,7 @@ export function registerGuidanceRuntimeCommands(registry: CommandRegistry): void
80
78
  const maintenance = evaluateSessionMaintenance({
81
79
  configManager: ctx.platform.configManager,
82
80
  currentTokens: estimateConversationTokens(llmMessages),
83
- contextWindow: currentModel?.contextWindow ?? 0,
81
+ contextWindow: ctx.provider.providerRegistry.getContextWindowForModel(ctx.provider.providerRegistry.getCurrentModel()),
84
82
  messageCount: llmMessages.length,
85
83
  sessionMemoryCount: requireSessionMemoryStore(ctx).list().length,
86
84
  session: session.session,
@@ -13,7 +13,6 @@ import {
13
13
  openCommandPanel,
14
14
  requireLocalUserAuthManager,
15
15
  requireOperatorClient,
16
- requireProviderApi,
17
16
  requireReadModels,
18
17
  requireSecretsManager,
19
18
  requireServiceRegistry,
@@ -240,15 +239,19 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
240
239
 
241
240
  if (sub === 'maintenance') {
242
241
  const session = readModels.session.getSnapshot();
243
- const providerApi = requireProviderApi(ctx);
244
- const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
242
+ const providerRegistry = ctx.provider.providerRegistry;
243
+ // Resolve the context window the same way the Tokens panel does so the
244
+ // maintenance usage %/remaining agree across every diagnostics surface.
245
+ const contextWindow = providerRegistry.getContextWindowForModel(
246
+ providerRegistry.getCurrentModel(),
247
+ );
245
248
  const llmMessages = typeof ctx.session.conversationManager.getMessagesForLLM === 'function'
246
249
  ? ctx.session.conversationManager.getMessagesForLLM()
247
250
  : [];
248
251
  const maintenance = evaluateSessionMaintenance({
249
252
  configManager: ctx.platform.configManager,
250
253
  currentTokens: estimateConversationTokens(llmMessages),
251
- contextWindow: currentModel?.contextWindow ?? 0,
254
+ contextWindow,
252
255
  messageCount: llmMessages.length,
253
256
  sessionMemoryCount: requireSessionMemoryStore(ctx).list().length,
254
257
  session: session.session,
@@ -386,12 +389,15 @@ export function registerHealthRuntimeCommands(registry: CommandRegistry): void {
386
389
  }
387
390
 
388
391
  const session = readModels.session.getSnapshot();
389
- const providerApi = requireProviderApi(ctx);
390
- const currentModel = await providerApi.getCurrentModel().catch(() => null); // best-effort: null handled as unknown context window
392
+ const providerRegistry = ctx.provider.providerRegistry;
391
393
  const llmMessages = typeof ctx.session.conversationManager.getMessagesForLLM === 'function'
392
394
  ? ctx.session.conversationManager.getMessagesForLLM()
393
395
  : [];
394
- const contextWindow = currentModel?.contextWindow ?? 0;
396
+ // Resolve the context window the same way the Tokens panel does so the
397
+ // maintenance usage %/remaining agree across every diagnostics surface.
398
+ const contextWindow = providerRegistry.getContextWindowForModel(
399
+ providerRegistry.getCurrentModel(),
400
+ );
395
401
  const maintenance = evaluateSessionMaintenance({
396
402
  configManager: ctx.platform.configManager,
397
403
  currentTokens: estimateConversationTokens(llmMessages),
@@ -1,5 +1,6 @@
1
1
  import type { KnowledgeService } from '@pellux/goodvibes-sdk/platform/knowledge';
2
2
  import type { CommandContext, SlashCommand } from '../command-registry.ts';
3
+ import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
3
4
 
4
5
  const KNOWLEDGE_REVIEW_ACTIONS = ['accept', 'reject', 'resolve', 'reopen', 'edit', 'forget'] as const;
5
6
 
@@ -397,7 +398,7 @@ export const knowledgeCommand: SlashCommand = {
397
398
  ...(result.appliedFacts ? [` applied facts: ${Object.keys(result.appliedFacts).join(', ') || 'none'}`] : []),
398
399
  ].join('\n'));
399
400
  } catch (error) {
400
- context.print(`[knowledge] ${error instanceof Error ? error.message : String(error)}`);
401
+ context.print(`[knowledge] ${summarizeError(error)}`);
401
402
  }
402
403
  break;
403
404
  }
@@ -7,7 +7,7 @@ import type {
7
7
  CommandSessionServices,
8
8
  CommandWorkspaceServices,
9
9
  } from '../command-registry.ts';
10
- import { getLastCompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
10
+ import { compactSmallWindow, getLastCompactionEvent, SMALL_WINDOW_THRESHOLD } from '@pellux/goodvibes-sdk/platform/core';
11
11
  import type { CompactionContext, CompactionEvent } from '@pellux/goodvibes-sdk/platform/core';
12
12
  import type { UiReadModels } from '../../runtime/ui-read-models.ts';
13
13
  import type { ShellPathService } from '@/runtime/index.ts';
@@ -244,23 +244,53 @@ export function requireProviderApi(context: CommandContext): ProviderApi {
244
244
  * change).
245
245
  */
246
246
  export async function compactConversation(context: CommandContext): Promise<CompactionEvent | null> {
247
+ const conversationManager = context.session.conversationManager;
248
+ const providerRegistry = context.provider.providerRegistry;
249
+ // Resolve the live context window for the current model so manual /compact
250
+ // matches the auto-compaction path (which scales behaviour by window size)
251
+ // instead of passing a meaningless 0.
252
+ const contextWindow = providerRegistry.getContextWindowForModel(
253
+ providerRegistry.getCurrentModel(),
254
+ );
255
+
256
+ // Small-window models lack room for the structured (multi-LLM) extraction
257
+ // pipeline, so mirror the SDK auto-compaction path and degrade to the
258
+ // simplified "keep the most recent messages" compaction.
259
+ if (contextWindow > 0 && contextWindow < SMALL_WINDOW_THRESHOLD) {
260
+ const compacted = compactSmallWindow(conversationManager.getMessagesForLLM(), 10);
261
+ conversationManager.replaceMessagesForLLM(compacted);
262
+ return null;
263
+ }
264
+
247
265
  const eventBefore = getLastCompactionEvent();
248
266
  const sessionMemories = context.session.sessionMemoryStore?.list() ?? [];
267
+ const lineageTracker = context.session.sessionLineageTracker;
268
+ const agentManager = context.ops.agentManager;
269
+ const planManager = context.ops.planManager;
249
270
  const compactionCtx: CompactionContext = {
250
- messages: context.session.conversationManager.getMessagesForLLM(),
271
+ messages: conversationManager.getMessagesForLLM(),
251
272
  sessionMemories,
252
- agents: [],
253
- wrfcChains: [],
254
- activePlan: null,
255
- lineageEntries: [],
256
- compactionCount: 0,
257
- contextWindow: 0,
273
+ // Mirror buildAutoCompactionContext: pass the running/pending agents, the
274
+ // active execution plan, and the session-lineage log so the handoff summary
275
+ // and lineage section are populated correctly (not "compaction #0").
276
+ agents:
277
+ agentManager
278
+ ?.exportState()
279
+ .filter((agent) => agent.status === 'running' || agent.status === 'pending') ?? [],
280
+ // Mirror buildAutoCompactionContext: include the live WRFC chains (wired
281
+ // through context.session.wrfcController) so the handoff summary's
282
+ // orchestration section matches the SDK auto-compaction path.
283
+ wrfcChains: context.session.wrfcController?.listChains() ?? [],
284
+ activePlan: planManager?.getActive(context.session.runtime.sessionId) ?? null,
285
+ lineageEntries: lineageTracker?.getEntries() ?? [],
286
+ compactionCount: lineageTracker?.getCompactionCount() ?? 0,
287
+ contextWindow,
258
288
  trigger: 'manual',
259
289
  extractionModelId: context.session.runtime.model,
260
290
  extractionProvider: context.session.runtime.provider,
261
291
  };
262
- await context.session.conversationManager.compact(
263
- context.provider.providerRegistry,
292
+ await conversationManager.compact(
293
+ providerRegistry,
264
294
  context.session.runtime.model,
265
295
  'manual',
266
296
  context.session.runtime.provider,
@@ -117,7 +117,7 @@ export function handleCommandModeToken(state: CommandModeRouteState, token: Inpu
117
117
  return true;
118
118
  }
119
119
 
120
- return token.logicalName !== 'left' && token.logicalName !== 'right';
120
+ return token.logicalName !== 'left' && token.logicalName !== 'right' && token.logicalName !== 'home' && token.logicalName !== 'end';
121
121
  }
122
122
 
123
123
  function withPanelFocusSync(context: CommandContext, state: CommandModeRouteState): CommandContext {
@@ -13,6 +13,8 @@ import {
13
13
  } from './handler-prompt-buffer.ts';
14
14
  import { cleanupMarkerRegistry, expandPrompt, findMarkerAtPos, registerPaste } from './handler-content-actions.ts';
15
15
  import type { PanelManager } from '../panels/panel-manager.ts';
16
+ import { renderPanelWorkspaceBar } from '../renderer/panel-workspace-bar.ts';
17
+ import type { TabHitRegion } from '../renderer/tab-strip.ts';
16
18
  import type { KeybindingsManager } from './keybindings.ts';
17
19
  import type { KillRing } from './kill-ring.ts';
18
20
  import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
@@ -84,6 +86,23 @@ export function handlePanelFocusToken(state: PanelFocusRouteState, token: InputT
84
86
  state.cyclePanelTab('prev');
85
87
  return { handled: true, panelFocused };
86
88
  }
89
+ if (kb.matches('panel-focus-toggle', token)) {
90
+ // Switch keyboard focus between the top and bottom panes (no-op when
91
+ // there is no visible, non-empty bottom pane).
92
+ state.panelManager.togglePaneFocus();
93
+ state.requestRender();
94
+ return { handled: true, panelFocused };
95
+ }
96
+ // Alt+1..9 — jump directly to the Nth workspace tab (across both panes).
97
+ // The tokenizer maps the Alt modifier onto `meta`, so an Alt-held digit
98
+ // arrives as { logicalName: '1'..'9', meta: true }. Gating on the modifier
99
+ // keeps plain digits reaching the focused panel.
100
+ if (token.meta && !token.ctrl && /^[1-9]$/.test(token.logicalName ?? '')) {
101
+ const index = Number(token.logicalName) - 1;
102
+ state.panelManager.activateWorkspaceIndex(index);
103
+ state.requestRender();
104
+ return { handled: true, panelFocused };
105
+ }
87
106
  if (kb.matches('panel-close-all', token)) {
88
107
  const pm = state.panelManager;
89
108
  for (const p of pm.getAllOpen()) pm.close(p.id);
@@ -577,18 +596,48 @@ function getPanelUnderMouse(
577
596
  return getActivePanelInPane(panelManager, 'top');
578
597
  }
579
598
 
599
+ // Single consolidated workspace bar (row 0) + h-separator; the rest splits
600
+ // between the two panes' content.
580
601
  const panelAreaRows = Math.max(0, layout.height - 1);
581
- const contentRows = Math.max(0, panelAreaRows - 3);
602
+ const contentRows = Math.max(0, panelAreaRows - 1);
582
603
  const topContentRows = contentRows <= 1
583
604
  ? contentRows
584
605
  : Math.max(1, Math.floor(contentRows * clampRatio(layout.verticalSplitRatio)));
585
- const topLastRow = 2 + topContentRows;
586
-
587
- return panelRow <= topLastRow
606
+ // panelRow 0 = workspace bar; rows 1..topContentRows = top pane; rest = bottom.
607
+ return panelRow <= topContentRows
588
608
  ? getActivePanelInPane(panelManager, 'top')
589
609
  : getActivePanelInPane(panelManager, 'bottom');
590
610
  }
591
611
 
612
+ /**
613
+ * If the mouse is over the consolidated workspace tab bar (the first panel
614
+ * row), return the index of the tab under the cursor, else null. Recomputes the
615
+ * tab hit regions by rendering the bar with a layout callback — cheap and keeps
616
+ * the click geometry in lockstep with what was drawn.
617
+ */
618
+ function workspaceTabAtMouse(
619
+ panelManager: PanelManager,
620
+ layout: PanelMouseLayout | null,
621
+ row: number,
622
+ col: number,
623
+ ): number | null {
624
+ if (
625
+ layout === null
626
+ || !panelManager.isVisible()
627
+ || panelManager.getAllOpen().length === 0
628
+ || row !== layout.y // workspace bar is the first panel row
629
+ || col < layout.x
630
+ || col >= layout.x + layout.width
631
+ ) {
632
+ return null;
633
+ }
634
+ let regions: readonly TabHitRegion[] = [];
635
+ renderPanelWorkspaceBar(panelManager.getWorkspaceTabs(), layout.width, true, (r) => { regions = r; });
636
+ const relCol = col - layout.x;
637
+ const hit = regions.find((rg) => relCol >= rg.startCol && relCol < rg.endCol);
638
+ return hit ? hit.index : null;
639
+ }
640
+
592
641
  function scrollPanelUnderMouse(
593
642
  state: MouseRouteState,
594
643
  token: Extract<InputToken, { type: 'mouse' }>,
@@ -634,6 +683,14 @@ export function handleMouseToken(state: MouseRouteState, token: InputToken): {
634
683
  return { handled: true, mouseDownRow, mouseDownCol };
635
684
  }
636
685
  if (token.button === 0 && token.action === 'press') {
686
+ // Click on a workspace tab activates it (checked before starting a text
687
+ // selection so a tab click doesn't begin a drag-select).
688
+ const tabIndex = workspaceTabAtMouse(state.panelManager, state.panelMouseLayout, token.row, token.col);
689
+ if (tabIndex !== null) {
690
+ state.panelManager.activateWorkspaceIndex(tabIndex);
691
+ state.requestRender();
692
+ return { handled: true, mouseDownRow, mouseDownCol };
693
+ }
637
694
  mouseDownRow = token.row;
638
695
  mouseDownCol = token.col;
639
696
  state.selection.startSelection(token.col, viewportRow, state.scrollTop, state.viewportHeight, state.lineCount);
@@ -296,6 +296,19 @@ export function feedInputTokens(context: InputFeedContext, tokens: readonly Inpu
296
296
  context.cursorPos = shortcutState.cursorPos;
297
297
  context.commandMode = shortcutState.commandMode;
298
298
  context.panelFocused = shortcutState.panelFocused;
299
+ if (context.commandMode) {
300
+ if (!context.prompt.startsWith('/')) {
301
+ context.commandMode = false;
302
+ context.autocomplete?.reset();
303
+ } else {
304
+ const q = context.prompt.slice(1);
305
+ if (q.indexOf(' ') === -1) {
306
+ context.autocomplete?.update(q);
307
+ } else {
308
+ context.autocomplete?.reset();
309
+ }
310
+ }
311
+ }
299
312
  continue;
300
313
  }
301
314
  }