winter-super-cli 2026.6.6 → 2026.6.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.
package/bin/winter.js CHANGED
@@ -23,6 +23,7 @@ const COMMANDS = new Set([
23
23
  'autopilot', 'plan',
24
24
  'provider', 'providers', 'model', 'models', 'ecc', 'page-agent', 'pageagent',
25
25
  'resources', 'htmlfx', 'memory-vault', 'doctor', 'context', 'scorecard',
26
+ 'tui',
26
27
  ]);
27
28
 
28
29
  function isInteractiveRequest(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "winter-super-cli",
3
- "version": "2026.6.6",
3
+ "version": "2026.6.8",
4
4
  "description": "❄️ AI-Powered Development CLI with Interactive REPL",
5
5
  "type": "module",
6
6
  "main": "bin/winter.js",
@@ -1,6 +1,6 @@
1
1
  import { Spinner } from '../cli/spinner.js';
2
2
  import { colors } from '../cli/snowflake-logo.js';
3
- import { renderBox, terminalWidth, wrapText } from '../cli/terminal-ui.js';
3
+ import { renderToolPanel } from '../cli/tui.js';
4
4
  import { getMutatingToolNames, recordToolCallAdapterStats } from '../cli/tool-runtime.js';
5
5
  import { buildSmallModelAmplification } from '../ai/small-model-amplifier.js';
6
6
 
@@ -38,6 +38,9 @@ export class AgentRuntime {
38
38
  depth,
39
39
  });
40
40
  const maxToolTurns = amplifier.maxToolTurns || 8;
41
+ // Keep self-critique as prompt discipline only. A second runtime model turn
42
+ // duplicates the final answer because the first answer is already rendered.
43
+ amplifier.enforceSelfCritique = false;
41
44
  let forceTextToolFallback = false;
42
45
 
43
46
  try {
@@ -116,7 +119,6 @@ export class AgentRuntime {
116
119
  }
117
120
  }
118
121
 
119
- const BOX_WIDTH = terminalWidth(76, 116, 92);
120
122
  messages.push({
121
123
  role: 'assistant',
122
124
  content: assistantMsg.content || '',
@@ -189,20 +191,12 @@ export class AgentRuntime {
189
191
  const summary = repl.formatToolResultForConsole(canonicalToolName, result);
190
192
  if (summary) {
191
193
  toolSummaries.push(`${canonicalToolName}: ${summary}`);
192
- const statusIcon = result.success === false
193
- ? `${colors.red}${repl.useUnicodeUi ? '✖' : 'x'}${colors.reset}`
194
- : `${colors.green}${repl.useUnicodeUi ? '✓' : 'ok'}${colors.reset}`;
195
- const toolLine = `${icon} ${colors.cyan}${colors.bright}${toolName}${colors.reset}`;
196
- const summaryLines = summary.split('\n').flatMap(line => wrapText(line, BOX_WIDTH - 8));
197
- console.log(renderBox({
198
- title: 'AGENT TOOLS EXECUTION',
199
- width: BOX_WIDTH,
200
- borderColor: colors.magenta,
201
- titleColor: colors.bright,
202
- body: [
203
- toolLine,
204
- ...summaryLines.map((line, index) => index === 0 ? `${statusIcon} ${colors.dim}${line}${colors.reset}` : `${colors.dim}${line}${colors.reset}`),
205
- ],
194
+ console.log(renderToolPanel({
195
+ toolName: `${icon} ${toolName}`,
196
+ summary,
197
+ success: result.success !== false,
198
+ colors,
199
+ title: 'Agent Tools',
206
200
  }));
207
201
  }
208
202
  }
@@ -171,6 +171,21 @@ export function getReasoningBump(tier) {
171
171
  }
172
172
  }
173
173
 
174
+ /**
175
+ * Get a budget multiplier for prompt/context sizing.
176
+ * Bigger models can safely absorb more context and larger tool outputs.
177
+ */
178
+ export function getModelBudgetMultiplier(tier) {
179
+ switch (tier) {
180
+ case MODEL_TIERS.TINY: return 0.5;
181
+ case MODEL_TIERS.SMALL: return 0.75;
182
+ case MODEL_TIERS.MEDIUM: return 1;
183
+ case MODEL_TIERS.LARGE: return 2;
184
+ case MODEL_TIERS.FLAGSHIP: return 4;
185
+ default: return 1;
186
+ }
187
+ }
188
+
174
189
  /**
175
190
  * Build a short string describing model capability for system prompt injection.
176
191
  */
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from '../../cli/runtime-env.js';
8
+ import { getModelBudgetMultiplier } from '../model-capabilities.js';
8
9
 
9
10
  const BASE_PRINCIPLES = [
10
11
  'Execute, don\'t describe - Do the work, don\'t write plans about doing the work',
@@ -34,11 +35,22 @@ function buildEnvironmentSummary() {
34
35
  ].join('\n');
35
36
  }
36
37
 
38
+ function getPromptBudgets(modelTier = '') {
39
+ const scale = getModelBudgetMultiplier(modelTier);
40
+ const compactSystemPrompt = scale <= 0.75;
41
+
42
+ return {
43
+ compactSystemPrompt,
44
+ projectContextBudget: Math.round(3200 * scale),
45
+ resourceContextBudget: Math.round(1200 * scale),
46
+ };
47
+ }
48
+
37
49
  function formatToolList(tools = []) {
38
50
  return tools.length > 0 ? tools.slice(0, 10).join(', ') : '';
39
51
  }
40
52
 
41
- function appendSharedContext(parts, { environment, session, design, resourceContext, context, includeResources = false } = {}) {
53
+ function appendSharedContext(parts, { environment, session, design, resourceContext, context, includeResources = false, resourceContextBudget = 1200 } = {}) {
42
54
  parts.push('## Runtime Environment', environment || buildEnvironmentSummary(), '');
43
55
 
44
56
  if (session?.memory?.length) {
@@ -65,7 +77,7 @@ function appendSharedContext(parts, { environment, session, design, resourceCont
65
77
  }
66
78
 
67
79
  if (includeResources && resourceContext) {
68
- parts.push(resourceContext.trim().slice(0, 1200), '');
80
+ parts.push(resourceContext.trim().slice(0, resourceContextBudget), '');
69
81
  }
70
82
 
71
83
  if (context && typeof context === 'object') {
@@ -74,7 +86,10 @@ function appendSharedContext(parts, { environment, session, design, resourceCont
74
86
  }
75
87
 
76
88
  function buildStandardSystemPrompt(options = {}) {
77
- const { role = 'coding', tools = [], resourceContext } = options;
89
+ const { role = 'coding', tools = [], resourceContext, modelTier = '' } = options;
90
+ const budgets = getPromptBudgets(modelTier);
91
+ const projectContextBudget = options.projectContextBudget ?? budgets.projectContextBudget;
92
+ const compactSystemPrompt = options.compactSystemPrompt ?? budgets.compactSystemPrompt;
78
93
  const parts = [
79
94
  'You are Winter, an expert AI coding assistant.',
80
95
  '',
@@ -93,7 +108,11 @@ function buildStandardSystemPrompt(options = {}) {
93
108
 
94
109
  const toolList = formatToolList(tools);
95
110
  if (toolList) parts.push('## Tools', toolList, '');
96
- appendSharedContext(parts, { ...options, includeResources: Boolean(resourceContext) && (role === 'design' || role === 'ui') });
111
+ appendSharedContext(parts, {
112
+ ...options,
113
+ includeResources: Boolean(resourceContext) && (role === 'design' || role === 'ui'),
114
+ resourceContextBudget: budgets.resourceContextBudget,
115
+ });
97
116
 
98
117
  parts.push('Always respond in Vietnamese.');
99
118
  return parts.filter(Boolean).join('\n');
@@ -109,7 +128,10 @@ export function buildSystemPrompt({
109
128
  resourceContext,
110
129
  modelTier,
111
130
  } = {}) {
131
+ const budgets = getPromptBudgets(modelTier);
112
132
  const options = { role, context, tools, session, environment, design, resourceContext, modelTier };
133
+ options.projectContextBudget = options.projectContextBudget ?? budgets.projectContextBudget;
134
+ options.compactSystemPrompt = options.compactSystemPrompt ?? budgets.compactSystemPrompt;
113
135
  return buildStandardSystemPrompt(options);
114
136
  }
115
137
 
@@ -25,11 +25,64 @@ const RESERVED_CONFIG_SECTIONS = new Set([
25
25
  'ui',
26
26
  ]);
27
27
 
28
+ const DEFAULT_REQUEST_TIMEOUT_MS = 120000;
29
+
28
30
  function isAuthError(error) {
29
31
  const msg = String(error?.message || error || '');
30
32
  return /\b(401|403)\b/.test(msg) || /authentication_error|invalid_api_key|unauthorized|auth\s*failed/i.test(msg);
31
33
  }
32
34
 
35
+ function isRateLimitError(error) {
36
+ const msg = String(error?.message || error || '');
37
+ return error?.status === 429 || /\b429\b|rate[_ -]?limit|tokens per minute|\bTPM\b/i.test(msg);
38
+ }
39
+
40
+ function getRequestTimeoutMs(options = {}) {
41
+ const raw = options.timeoutMs ?? process.env.WINTER_REQUEST_TIMEOUT_MS;
42
+ const value = Number(raw);
43
+ if (Number.isFinite(value) && value > 0) return value;
44
+ return DEFAULT_REQUEST_TIMEOUT_MS;
45
+ }
46
+
47
+ function createTimeoutSignal(timeoutMs, externalSignal = null) {
48
+ const controller = new AbortController();
49
+ let timedOut = false;
50
+ const onAbort = () => {
51
+ controller.abort(externalSignal?.reason || new DOMException('The operation was aborted.', 'AbortError'));
52
+ };
53
+ if (externalSignal?.aborted) {
54
+ onAbort();
55
+ } else if (externalSignal) {
56
+ externalSignal.addEventListener('abort', onAbort, { once: true });
57
+ }
58
+ const timer = setTimeout(() => {
59
+ timedOut = true;
60
+ controller.abort(new Error(`Winter request timed out after ${timeoutMs}ms`));
61
+ }, timeoutMs);
62
+ if (typeof timer.unref === 'function') timer.unref();
63
+ return {
64
+ signal: controller.signal,
65
+ timedOut: () => timedOut,
66
+ cleanup: () => {
67
+ clearTimeout(timer);
68
+ if (externalSignal) externalSignal.removeEventListener('abort', onAbort);
69
+ },
70
+ };
71
+ }
72
+
73
+ function normalizeFetchError(error, provider, timeoutMs, stream = false, timedOut = false) {
74
+ if (timedOut || /timed out/i.test(String(error?.message || ''))) {
75
+ const label = stream ? 'stream' : 'request';
76
+ return new Error(`${provider?.name || 'Provider'} ${label} timed out after ${Math.ceil(timeoutMs / 1000)}s`);
77
+ }
78
+ if (error?.name === 'AbortError' || /abort/i.test(String(error?.message || ''))) {
79
+ const abortError = new Error('AbortError');
80
+ abortError.name = 'AbortError';
81
+ return abortError;
82
+ }
83
+ return error;
84
+ }
85
+
33
86
  export class AIProviderManager {
34
87
  constructor(config) {
35
88
  this.config = config;
@@ -365,7 +418,7 @@ export class AIProviderManager {
365
418
  model: routingModel,
366
419
  reasoning: routingReasoning,
367
420
  reasoningLevel: options.reasoningLevel || executionProfile.reasoningLevel,
368
- }), { maxAttempts: 3, baseDelayMs: 150 });
421
+ }), { maxAttempts: 3, baseDelayMs: 150, retryable: error => !isRateLimitError(error) && !/\b(400|404)\b/.test(String(error?.message || error || '')) });
369
422
  } catch (error) {
370
423
  if (isAuthError(error) && routedProvider !== defaultProvider && defaultProvider) {
371
424
  if (!this._fallbackWarned) {
@@ -377,7 +430,7 @@ export class AIProviderManager {
377
430
  model: options.model || defaultProvider.model,
378
431
  reasoning: routingReasoning,
379
432
  reasoningLevel: options.reasoningLevel || executionProfile.reasoningLevel,
380
- }), { maxAttempts: 1, baseDelayMs: 0 });
433
+ }), { maxAttempts: 1, baseDelayMs: 0, retryable: error => !isRateLimitError(error) && !/\b(400|404)\b/.test(String(error?.message || error || '')) });
381
434
  }
382
435
  throw error;
383
436
  }
@@ -426,6 +479,7 @@ export class AIProviderManager {
426
479
  if (!provider) {
427
480
  throw new Error('No active provider is configured');
428
481
  }
482
+ const timeoutMs = getRequestTimeoutMs(options);
429
483
 
430
484
  const body = {
431
485
  model: options.model || provider.model,
@@ -459,15 +513,26 @@ export class AIProviderManager {
459
513
  headers['Authorization'] = `Bearer ${provider.apiKey}`;
460
514
  }
461
515
 
462
- const response = await fetch(`${provider.baseURL}/chat/completions`, {
463
- method: 'POST',
464
- headers,
465
- body: JSON.stringify(body),
466
- });
516
+ const timeout = createTimeoutSignal(timeoutMs, options.signal || options.abortSignal);
517
+ let response;
518
+ try {
519
+ response = await fetch(`${provider.baseURL}/chat/completions`, {
520
+ method: 'POST',
521
+ headers,
522
+ body: JSON.stringify(body),
523
+ signal: timeout.signal,
524
+ });
525
+ } catch (error) {
526
+ throw normalizeFetchError(error, provider, timeoutMs, false, timeout.timedOut());
527
+ } finally {
528
+ timeout.cleanup();
529
+ }
467
530
 
468
531
  if (!response.ok) {
469
532
  const error = await response.text();
470
- throw new Error(`${provider.name} error (${response.status}): ${error}`);
533
+ const requestError = new Error(`${provider.name} error (${response.status}): ${error}`);
534
+ requestError.status = response.status;
535
+ throw requestError;
471
536
  }
472
537
 
473
538
  return await response.json();
@@ -477,6 +542,7 @@ export class AIProviderManager {
477
542
  if (!provider) {
478
543
  throw new Error('No active provider is configured');
479
544
  }
545
+ const timeoutMs = getRequestTimeoutMs(options);
480
546
 
481
547
  const body = {
482
548
  model: options.model || provider.model,
@@ -515,67 +581,78 @@ export class AIProviderManager {
515
581
  headers['Authorization'] = `Bearer ${provider.apiKey}`;
516
582
  }
517
583
 
518
- const response = await fetch(`${provider.baseURL}/chat/completions`, {
519
- method: 'POST',
520
- headers,
521
- body: JSON.stringify(body),
522
- });
523
-
524
- if (!response.ok) {
525
- const error = await response.text();
526
- throw new Error(`${provider.name} stream error (${response.status}): ${error}`);
527
- }
584
+ const timeout = createTimeoutSignal(timeoutMs, options.signal || options.abortSignal);
585
+ let response;
586
+ try {
587
+ response = await fetch(`${provider.baseURL}/chat/completions`, {
588
+ method: 'POST',
589
+ headers,
590
+ body: JSON.stringify(body),
591
+ signal: timeout.signal,
592
+ });
528
593
 
529
- if (!response.body) {
530
- throw new Error(`${provider.name} did not return a stream body`);
531
- }
594
+ if (!response.ok) {
595
+ const error = await response.text();
596
+ const streamError = new Error(`${provider.name} stream error (${response.status}): ${error}`);
597
+ streamError.status = response.status;
598
+ throw streamError;
599
+ }
532
600
 
533
- const decoder = new TextDecoder();
534
- let buffer = '';
601
+ if (!response.body) {
602
+ throw new Error(`${provider.name} did not return a stream body`);
603
+ }
535
604
 
536
- for await (const chunk of response.body) {
537
- buffer += decoder.decode(chunk, { stream: true });
538
- const lines = buffer.split(/\r?\n/);
539
- buffer = lines.pop() || '';
605
+ const decoder = new TextDecoder();
606
+ let buffer = '';
540
607
 
541
- for (const line of lines) {
542
- const trimmed = line.trim();
543
- if (!trimmed || !trimmed.startsWith('data:')) continue;
608
+ for await (const chunk of response.body) {
609
+ buffer += decoder.decode(chunk, { stream: true });
610
+ const lines = buffer.split(/\r?\n/);
611
+ buffer = lines.pop() || '';
544
612
 
545
- const payload = trimmed.slice(5).trim();
546
- if (!payload || payload === '[DONE]') continue;
613
+ for (const line of lines) {
614
+ const trimmed = line.trim();
615
+ if (!trimmed || !trimmed.startsWith('data:')) continue;
547
616
 
548
- let data;
549
- try {
550
- data = JSON.parse(payload);
551
- } catch {
552
- continue;
553
- }
617
+ const payload = trimmed.slice(5).trim();
618
+ if (!payload || payload === '[DONE]') continue;
554
619
 
555
- const choice = data.choices?.[0] || {};
556
- const content = choice.delta?.content ?? choice.message?.content ?? choice.text ?? '';
557
- yield {
558
- content,
559
- usage: data.usage,
560
- raw: data,
561
- };
562
- }
563
- }
620
+ let data;
621
+ try {
622
+ data = JSON.parse(payload);
623
+ } catch {
624
+ continue;
625
+ }
564
626
 
565
- const tail = buffer.trim();
566
- if (tail.startsWith('data:')) {
567
- const payload = tail.slice(5).trim();
568
- if (payload && payload !== '[DONE]') {
569
- try {
570
- const data = JSON.parse(payload);
571
627
  const choice = data.choices?.[0] || {};
628
+ const content = choice.delta?.content ?? choice.message?.content ?? choice.text ?? '';
572
629
  yield {
573
- content: choice.delta?.content ?? choice.message?.content ?? choice.text ?? '',
630
+ content,
574
631
  usage: data.usage,
575
632
  raw: data,
576
633
  };
577
- } catch {}
634
+ }
635
+ }
636
+
637
+ const tail = buffer.trim();
638
+ if (tail.startsWith('data:')) {
639
+ const payload = tail.slice(5).trim();
640
+ if (payload && payload !== '[DONE]') {
641
+ try {
642
+ const data = JSON.parse(payload);
643
+ const choice = data.choices?.[0] || {};
644
+ yield {
645
+ content: choice.delta?.content ?? choice.message?.content ?? choice.text ?? '',
646
+ usage: data.usage,
647
+ raw: data,
648
+ };
649
+ } catch {}
650
+ }
578
651
  }
652
+ } catch (error) {
653
+ throw normalizeFetchError(error, provider, timeoutMs, true, timeout.timedOut());
654
+ } finally {
655
+ timeout.cleanup();
579
656
  }
580
657
  }
581
658
 
@@ -9,9 +9,9 @@ export function buildSmallModelAmplification({ modelTier = '', workflowProfile =
9
9
  const hint = [
10
10
  '[Winter Strength Amplifier]',
11
11
  '- Every model, including tiny/local/free models, must run at Winter maximum capability.',
12
- '- Mandatory loop: PLAN (requirements + files + risks) -> TOOL ACTIONS -> VERIFY -> SELF-CHECK -> FINAL.',
12
+ '- Mandatory internal loop: PLAN (requirements + files + risks) -> TOOL ACTIONS -> VERIFY -> PRIVATE SELF-CHECK -> FINAL.',
13
13
  '- Do not skip verification. If verification fails, iterate until max loops.',
14
- '- Before final answer, run a private self-critique: missing edge cases, missing tests, over-claims, and incorrect assumptions.',
14
+ '- Before final answer, run a private self-critique silently; do not print the self-check or an improved-answer preface.',
15
15
  '- Prefer concrete evidence from tool outputs over reasoning guesses.',
16
16
  '- Use CodeGraph/codebase index context before broad file reads when available.',
17
17
  ].join('\n');
@@ -15,6 +15,7 @@ import { redactSecrets } from './secret-env.js';
15
15
  import { formatRuntimeEnvironmentSummary, getRuntimeEnvironment } from './runtime-env.js';
16
16
  import { ContextLoader } from './context-loader.js';
17
17
  import { ECCManager } from './ecc.js';
18
+ import { buildTuiSnapshot, renderLandingTui } from './tui.js';
18
19
  import { HtmlFxManager } from '../integrations/htmlfx-manager.js';
19
20
  import { selectWorkflow } from '../ai/workflow-selector.js';
20
21
  import { getProfileBlueprint } from '../ai/profile-blueprints.js';
@@ -73,6 +74,7 @@ export class CommandParser {
73
74
  resources: this.handleResources.bind(this),
74
75
  htmlfx: this.handleHtmlFx.bind(this),
75
76
  'memory-vault': this.handleMemoryVault.bind(this),
77
+ tui: this.handleTui.bind(this),
76
78
  provider: this.handleProvider.bind(this),
77
79
  providers: this.showProviders.bind(this),
78
80
  model: this.handleModel.bind(this),
@@ -121,6 +123,7 @@ export class CommandParser {
121
123
  '/forget': (args) => this.session.clearMemory(args.length > 0 ? args.join(' ') : null),
122
124
  '/memories': () => this.showMemories(),
123
125
  '/memory-vault': () => this.handleMemoryVault(args),
126
+ '/tui': () => this.handleTui(args),
124
127
  '/plans': () => this.showPlans(),
125
128
  '/plan': () => this.handlePlan(args),
126
129
  '/cache': () => this.handleCache(args),
@@ -220,6 +223,15 @@ export class CommandParser {
220
223
  }
221
224
  }
222
225
 
226
+ async handleTui() {
227
+ await this.ai.init?.();
228
+ const snapshot = buildTuiSnapshot(this);
229
+ console.log(`\n${renderLandingTui(snapshot, {
230
+ colors,
231
+ title: 'Winter Dashboard',
232
+ })}\n`);
233
+ }
234
+
223
235
  async searchResourceFiles(root, query, limit = 30) {
224
236
  const matches = [];
225
237
  const needle = String(query || '').toLowerCase();
@@ -226,7 +226,7 @@ export class ContextLoader {
226
226
  this.readTextIfExists(pageAgentAgentsPath, 2200),
227
227
  ]);
228
228
 
229
- const hasRequired = Boolean(karpathy || agents || designReadme || designBrands.length > 0 || pageAgentWinter);
229
+ const hasRequired = Boolean(karpathy || agents || designReadme || designBrands.length > 0 || pageAgentWinter || pageAgentAgents);
230
230
  if (!hasRequired) return '';
231
231
 
232
232
  const lines = [];
@@ -8,9 +8,16 @@ import { promises as fs } from 'fs';
8
8
  import path from 'path';
9
9
  import readline from 'readline';
10
10
  import { spawn } from 'child_process';
11
+ import { highlight } from 'cli-highlight';
11
12
  import { renderBox, terminalWidth, stripAnsi, wrapText, visibleWidth } from './terminal-ui.js';
12
13
  import { colors } from './snowflake-logo.js';
13
14
 
15
+ // Setup background colors if not defined in snowflake-logo
16
+ const bgRed = '\x1b[41m';
17
+ const bgGreen = '\x1b[42m';
18
+ const bgDarkRed = '\x1b[48;5;52m';
19
+ const bgDarkGreen = '\x1b[48;5;22m';
20
+
14
21
  export class DiffView {
15
22
  constructor(options = {}) {
16
23
  this.projectPath = options.projectPath || process.cwd();
@@ -124,42 +131,91 @@ export class DiffView {
124
131
  // ── Private Methods ─────────────────────────────────
125
132
 
126
133
  _renderDiff(title, diff, width) {
127
- const body = [];
128
- const maxLines = Math.min(30, diff.additionsList.length + diff.removalsList.length);
129
- let added = 0;
130
- let removed = 0;
134
+ const innerWidth = Math.max(40, width - 6);
135
+ const header = `${colors.bright} ${title} ${colors.reset} ${colors.dim}— ${diff.additions} additions, ${diff.removals} deletions${colors.reset}`;
136
+
137
+ console.log(`\n${colors.magenta}┌${'─'.repeat(width - 2)}┐${colors.reset}`);
138
+ console.log(`${colors.magenta}│${colors.reset} ${header}${''.padEnd(Math.max(0, width - 4 - stripAnsi(header).length))}${colors.magenta}│${colors.reset}`);
139
+ console.log(`${colors.magenta}├${'─'.repeat(width - 2)}┤${colors.reset}`);
131
140
 
132
- body.push(` ${colors.dim}${path.basename(title)} ${diff.additions} additions, ${diff.removals} deletions${colors.reset}`);
141
+ const maxLines = 40;
142
+ let printed = 0;
143
+ let lineNum = 0;
144
+ const contextLines = 2; // lines of context around changes
133
145
 
146
+ // Build display entries with context
147
+ const entries = [];
134
148
  for (const part of diff.raw) {
135
- if (added + removed >= maxLines) {
136
- body.push(` ${colors.dim}... and ${diff.changes - maxLines} more changes${colors.reset}`);
137
- break;
149
+ const lines = part.value.replace(/\n$/, '').split('\n');
150
+ for (const line of lines) {
151
+ lineNum++;
152
+ if (part.added) {
153
+ entries.push({ type: 'add', num: lineNum, text: line });
154
+ } else if (part.removed) {
155
+ entries.push({ type: 'del', num: lineNum, text: line });
156
+ } else {
157
+ entries.push({ type: 'ctx', num: lineNum, text: line });
158
+ }
138
159
  }
160
+ }
139
161
 
140
- const lines = part.value.split('\n').filter(Boolean);
141
- const isAdded = part.added;
142
- const isRemoved = part.removed;
162
+ // Find which context lines to show (near changes)
163
+ const changeIndices = new Set();
164
+ entries.forEach((e, i) => {
165
+ if (e.type !== 'ctx') {
166
+ for (let j = Math.max(0, i - contextLines); j <= Math.min(entries.length - 1, i + contextLines); j++) {
167
+ changeIndices.add(j);
168
+ }
169
+ }
170
+ });
143
171
 
144
- for (const line of lines) {
145
- if (added + removed >= maxLines) break;
146
- if (isAdded) {
147
- added++;
148
- body.push(` ${colors.green}+ ${line}${colors.reset}`);
149
- } else if (isRemoved) {
150
- removed++;
151
- body.push(` ${colors.red}- ${line}${colors.reset}`);
172
+ let lastPrinted = -1;
173
+ for (let i = 0; i < entries.length && printed < maxLines; i++) {
174
+ const e = entries[i];
175
+ if (e.type === 'ctx' && !changeIndices.has(i)) continue;
176
+
177
+ // Show separator if there's a gap
178
+ if (lastPrinted >= 0 && i - lastPrinted > 1) {
179
+ console.log(`${colors.magenta}│${colors.reset} ${colors.dim}${'·'.repeat(Math.min(20, innerWidth))}${colors.reset}`);
180
+ }
181
+
182
+ const numStr = String(e.num).padStart(4);
183
+ const maxText = Math.max(10, innerWidth - 8);
184
+ const truncated = e.text.length > maxText ? e.text.slice(0, maxText - 3) + '...' : e.text;
185
+
186
+ // Detect language from file extension for highlight
187
+ const ext = path.extname(title).slice(1) || 'javascript';
188
+ const syntaxHighlight = (text) => {
189
+ try {
190
+ return highlight(text, { language: ext, ignoreIllegals: true });
191
+ } catch (e) {
192
+ return text;
152
193
  }
194
+ };
195
+
196
+ if (e.type === 'add') {
197
+ const lineContent = syntaxHighlight(truncated);
198
+ console.log(`${colors.magenta}│${colors.reset} ${bgDarkGreen}${colors.white}${numStr} + ${lineContent}${' '.repeat(Math.max(0, innerWidth - stripAnsi(truncated).length - 8))}${colors.reset}`);
199
+ } else if (e.type === 'del') {
200
+ const lineContent = syntaxHighlight(truncated);
201
+ console.log(`${colors.magenta}│${colors.reset} ${bgDarkRed}${colors.white}${numStr} - ${lineContent}${' '.repeat(Math.max(0, innerWidth - stripAnsi(truncated).length - 8))}${colors.reset}`);
202
+ } else {
203
+ const lineContent = syntaxHighlight(truncated);
204
+ console.log(`${colors.magenta}│${colors.reset} ${colors.dim}${numStr} ${colors.reset}${lineContent}`);
205
+ }
206
+
207
+ printed++;
208
+ lastPrinted = i;
209
+ }
210
+
211
+ if (printed >= maxLines && entries.length > maxLines) {
212
+ const remaining = entries.filter(e => e.type !== 'ctx').length - printed;
213
+ if (remaining > 0) {
214
+ console.log(`${colors.magenta}│${colors.reset} ${colors.dim} ... and ${remaining} more changes${colors.reset}`);
153
215
  }
154
216
  }
155
217
 
156
- console.log(`\n${renderBox({
157
- title: ` ${title} `,
158
- width,
159
- borderColor: colors.magenta,
160
- titleColor: colors.bright,
161
- body,
162
- })}\n`);
218
+ console.log(`${colors.magenta}└${'─'.repeat(width - 2)}┘${colors.reset}\n`);
163
219
  }
164
220
 
165
221
  async _promptChoice() {
@@ -236,12 +292,12 @@ export class DiffView {
236
292
  };
237
293
 
238
294
  rl.question(
239
- `\n${colors.cyan}Options:${colors.reset}\n` +
240
- ` ${colors.green}[a]${colors.reset} — Accept all (apply full diff)\n` +
241
- ` ${colors.red}[r]${colors.reset} — Reject all\n` +
242
- ` ${colors.yellow}[m]${colors.reset} — Manual edit (opens file in $EDITOR)\n` +
243
- ` ${colors.dim}[s]${colors.reset} — Skip\n` +
244
- `${colors.yellow}Choose [a/r/m/s]: ${colors.reset}`,
295
+ `\n${colors.cyan}Edit Options:${colors.reset}\n` +
296
+ ` ${colors.green}[a]${colors.reset} Accept Apply the complete diff\n` +
297
+ ` ${colors.red}[r]${colors.reset} Reject Discard these changes\n` +
298
+ ` ${colors.yellow}[m]${colors.reset} Manual Open file in $EDITOR to manually resolve\n` +
299
+ ` ${colors.dim}[s]${colors.reset} Skip — Skip for now\n` +
300
+ `${colors.yellow}👉 Choose [a/r/m/s]: ${colors.reset}`,
245
301
  onAnswer
246
302
  );
247
303