lonny-agent 0.1.9 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.kilo/plans/1780293725888-quick-wizard.md +62 -0
  2. package/.lonny/plan-web-cwd-status.md +38 -0
  3. package/dist/agent/compaction.d.ts +5 -0
  4. package/dist/agent/compaction.d.ts.map +1 -1
  5. package/dist/agent/compaction.js +5 -0
  6. package/dist/agent/compaction.js.map +1 -1
  7. package/dist/agent/prompt-builder.d.ts.map +1 -1
  8. package/dist/agent/prompt-builder.js +39 -4
  9. package/dist/agent/prompt-builder.js.map +1 -1
  10. package/dist/agent/providers/google.js +1 -1
  11. package/dist/agent/providers/google.js.map +1 -1
  12. package/dist/agent/providers/openai.js +1 -1
  13. package/dist/agent/providers/openai.js.map +1 -1
  14. package/dist/agent/session.d.ts.map +1 -1
  15. package/dist/agent/session.js +18 -2
  16. package/dist/agent/session.js.map +1 -1
  17. package/dist/config/__tests__/context-window.test.d.ts +2 -0
  18. package/dist/config/__tests__/context-window.test.d.ts.map +1 -0
  19. package/dist/config/__tests__/context-window.test.js +80 -0
  20. package/dist/config/__tests__/context-window.test.js.map +1 -0
  21. package/dist/config/index.d.ts +7 -0
  22. package/dist/config/index.d.ts.map +1 -1
  23. package/dist/config/index.js +211 -0
  24. package/dist/config/index.js.map +1 -1
  25. package/dist/pi-tui/tui.d.ts.map +1 -1
  26. package/dist/pi-tui/tui.js +7 -16
  27. package/dist/pi-tui/tui.js.map +1 -1
  28. package/dist/tools/__tests__/edit.test.js +139 -0
  29. package/dist/tools/__tests__/edit.test.js.map +1 -1
  30. package/dist/tools/__tests__/fetch.test.js +11 -9
  31. package/dist/tools/__tests__/fetch.test.js.map +1 -1
  32. package/dist/tools/edit.d.ts.map +1 -1
  33. package/dist/tools/edit.js +185 -32
  34. package/dist/tools/edit.js.map +1 -1
  35. package/dist/tools/install_superpowers.d.ts +3 -0
  36. package/dist/tools/install_superpowers.d.ts.map +1 -0
  37. package/dist/tools/install_superpowers.js +207 -0
  38. package/dist/tools/install_superpowers.js.map +1 -0
  39. package/dist/tui/index.d.ts.map +1 -1
  40. package/dist/tui/index.js +25 -1
  41. package/dist/tui/index.js.map +1 -1
  42. package/dist/web/index.d.ts.map +1 -1
  43. package/dist/web/index.js +56 -0
  44. package/dist/web/index.js.map +1 -1
  45. package/dist/web/public/app.js +8 -0
  46. package/dist/web/public/index.html +6 -0
  47. package/dist/web/public/style.css +11 -0
  48. package/m.role+' +0 -0
  49. package/package.json +1 -1
  50. package/src/agent/compaction.ts +6 -0
  51. package/src/agent/prompt-builder.ts +40 -4
  52. package/src/agent/providers/google.ts +1 -1
  53. package/src/agent/providers/openai.ts +1 -1
  54. package/src/agent/session.ts +19 -2
  55. package/src/config/__tests__/context-window.test.ts +94 -0
  56. package/src/config/index.ts +242 -0
  57. package/src/pi-tui/tui.ts +8 -14
  58. package/src/tools/__tests__/edit.test.ts +155 -0
  59. package/src/tools/__tests__/fetch.test.ts +12 -10
  60. package/src/tools/edit.ts +202 -33
  61. package/src/tools/registry.ts +265 -265
  62. package/src/tui/index.ts +23 -1
  63. package/src/web/index.ts +71 -0
  64. package/src/web/public/app.js +9 -0
  65. package/src/web/public/index.html +6 -0
  66. package/src/web/public/style.css +11 -0
  67. package/.lonny/fix-new-command-session-cleanup.md +0 -65
  68. package/.lonny/tui-autocomplete-above.md +0 -62
@@ -6,6 +6,12 @@ import type { LLMMessage } from './llm.js'
6
6
  */
7
7
 
8
8
  const DEFAULT_MAX_TOKENS = 256_000
9
+
10
+ /**
11
+ * 获取默认的上下文窗口大小(保持向后兼容)
12
+ * @deprecated 请使用 config.contextWindow 代替
13
+ */
14
+ export const DEFAULT_CONTEXT_WINDOW = DEFAULT_MAX_TOKENS
9
15
  const COMPACTION_THRESHOLD = 0.75 // compact when usage exceeds 75% of budget
10
16
 
11
17
  /** Rough token estimation (4 chars ~= 1 token).
@@ -67,11 +67,14 @@ ${getToolListForMode(config.mode)}`
67
67
  // ── Mode-specific instructions ───────────────────────────────────────────
68
68
  const modeInstructions =
69
69
  config.mode === 'plan'
70
- ? `You are a planning agent. Your sole job is to investigate the codebase and produce an actionable implementation plan plus a todo list. You NEVER edit source files.
70
+ ? `You are a planning agent. Your ONLY job is to investigate the codebase and produce an actionable implementation plan with a todo list.
71
+
72
+ You CANNOT edit files. You do NOT have access to edit, bash (write mode), exec, or install_skill.
73
+ Any attempt to call these tools will FAIL — they are simply unavailable in this mode.
71
74
 
72
75
  RULES (plan-specific):
73
76
  1. Read first: Use read/grep/glob tools to gather all context you need before planning.
74
- 2. You NEVER edit source files. You ONLY use read-only tools (read/glob/grep/ls/find/git/search) and bash (read-only commands only). You do NOT have access to edit, install_skill, or exec.
77
+ 2. You NEVER edit source files. You ONLY use read-only tools (read/glob/grep/ls/find/git/search) and bash (read-only commands only).
75
78
  3. Use \`bash\` for investigation only — NEVER to modify files, install packages, or run write operations.
76
79
  4. Your ONLY output is a plan file saved via \`write_plan\`. You CANNOT modify the codebase directly.
77
80
  5. You MUST persist the final plan AND todo list to a file in \`.lonny/\` using \`write_plan\`. The \`write_plan\` content MUST include both ## Plan and ## Todo List sections.
@@ -117,7 +120,40 @@ RULES (code-specific):
117
120
  7. COST OPTIMIZATION (CRITICAL): Each API call costs money. You have a hard limit of ~5 API calls per task.
118
121
  8. TODO LIST MAINTENANCE: After completing a task item, update the corresponding plan file in \`.lonny/\` by checking off the TODO item (change \`- [ ]\` to \`- [x]\`). Use \`read\` to find the plan file, then \`edit\` to update the checkbox.`
119
122
 
120
- // ── Environment section (dynamic content — put LAST for prefix caching) ──
123
+ // ── Built-in development methodologies ─────────────────────────────────
124
+ // Embedded directly from Superpowers — no skill files needed.
125
+ const methodologySection =
126
+ config.mode === 'code'
127
+ ? `
128
+
129
+ ## Development Methodology
130
+
131
+ ### Systematic Debugging
132
+ When investigating a bug or test failure, follow this process:
133
+ 1. **Root Cause** — Read errors carefully, reproduce consistently, check recent changes
134
+ 2. **Pattern Analysis** — Find working examples, compare with broken code
135
+ 3. **Hypothesis** — Form single hypothesis, test minimally (one change at a time)
136
+ 4. **Implementation** — Create failing test first, implement minimal fix, verify
137
+
138
+ NO FIXES WITHOUT ROOT CAUSE INVESTIGATION FIRST.
139
+ If you've tried 3+ fixes without success, question the architecture — don't keep guessing.
140
+
141
+ ### Verification Before Completion
142
+ After ANY change, you MUST verify it actually works before claiming success:
143
+ - Run the relevant tests and check the exit code
144
+ - Only then report the result
145
+
146
+ You CANNOT claim "tests pass" or "fix works" without having run the verification command in this same turn. Evidence before assertions, always.`
147
+ : config.mode === 'plan'
148
+ ? `
149
+
150
+ ## Design-First Planning
151
+ Before writing a plan, explore the user's request thoroughly:
152
+ 1. Ask clarifying questions — understand purpose, constraints, success criteria
153
+ 2. Explore alternatives — consider 2-3 different approaches with trade-offs
154
+ 3. Break into bite-sized tasks (2-5 minutes each) with exact file paths and verification steps
155
+ 4. Save the plan via \`write_plan\``
156
+ : ''
121
157
  const envSection = `Environment:
122
158
  - Platform: ${platform} ${release} (${arch})
123
159
  - Shell: ${shell}
@@ -135,5 +171,5 @@ ${isWindows ? ' - Use `type` instead of `cat`, `dir` instead of `ls`, `where` i
135
171
  ${sharedRules}`
136
172
  return `${modeInstructions}
137
173
 
138
- ${envSection}${rulesSection}${skillsSection}`
174
+ ${envSection}${rulesSection}${methodologySection}${skillsSection}`
139
175
  }
@@ -68,7 +68,7 @@ export class GoogleProvider implements LLMProvider {
68
68
  if (m.content) {
69
69
  parts.push({ text: m.content })
70
70
  }
71
- if (m.tool_calls) {
71
+ if (m.tool_calls && m.tool_calls.length > 0) {
72
72
  for (const tc of m.tool_calls) {
73
73
  parts.push({
74
74
  functionCall: {
@@ -91,7 +91,7 @@ export class OpenAIProvider implements LLMProvider {
91
91
  content: m.content || '',
92
92
  }
93
93
  }
94
- if (m.role === 'assistant' && m.tool_calls) {
94
+ if (m.role === 'assistant' && m.tool_calls && m.tool_calls.length > 0) {
95
95
  return {
96
96
  role: 'assistant',
97
97
  content: m.content,
@@ -388,6 +388,8 @@ export class Session {
388
388
  const out = this.output
389
389
  printUserMessage(userPrompt, out)
390
390
  this.messages.push({ role: 'user', content: userPrompt })
391
+ // Save immediately after user message so page refresh doesn't lose it
392
+ this.save()
391
393
 
392
394
  // Reset per-turn counters
393
395
  this.turnInputTokens = 0
@@ -568,6 +570,12 @@ export class Session {
568
570
  }
569
571
  if (chunk.finish_reason === 'stop' || chunk.finish_reason === 'end_turn') {
570
572
  if (toolCalls.length === 0) {
573
+ const finalAssistantMsg: LLMMessage = {
574
+ role: 'assistant',
575
+ content: fullResponse || null,
576
+ reasoning_content: reasoningContent,
577
+ }
578
+ this.messages.push(finalAssistantMsg)
571
579
  printTokenStats(
572
580
  this.turnInputTokens,
573
581
  this.turnOutputTokens,
@@ -610,6 +618,12 @@ export class Session {
610
618
 
611
619
  if (toolCalls.length === 0) {
612
620
  if (fullResponse) {
621
+ const finalAssistantMsg: LLMMessage = {
622
+ role: 'assistant',
623
+ content: fullResponse,
624
+ reasoning_content: reasoningContent,
625
+ }
626
+ this.messages.push(finalAssistantMsg)
613
627
  printTokenStats(
614
628
  this.turnInputTokens,
615
629
  this.turnOutputTokens,
@@ -639,6 +653,7 @@ export class Session {
639
653
  reasoning_content: reasoningContent,
640
654
  }
641
655
  this.messages.push(assistantMsg)
656
+ this.save()
642
657
 
643
658
  // ── User confirmation for write-type tool calls ──
644
659
  if (!this.config.autoApprove && this.output?.confirmTool && toolCalls.length > 0) {
@@ -691,13 +706,15 @@ export class Session {
691
706
  this.messages.push(resultMsg)
692
707
  }
693
708
 
709
+ this.save()
710
+
694
711
  // End the current turn before next iteration — frontend creates a new message
695
712
  bus.emit(EventChannels.TURN_END, { iterations, toolCallCount: toolCalls.length })
696
713
 
697
714
  // Check if compaction is needed
698
- if (shouldCompact(this.messages)) {
715
+ if (shouldCompact(this.messages, this.config.contextWindow)) {
699
716
  const before = this.messages.length
700
- const result = compact(this.messages)
717
+ const result = compact(this.messages, this.config.contextWindow)
701
718
  if (result.compressed) {
702
719
  this.messages = result.messages
703
720
  bus.emit(EventChannels.COMPACTION_TRIGGERED, { before, after: result.newCount })
@@ -0,0 +1,94 @@
1
+ import { describe, expect, test } from 'vitest'
2
+ import { getContextWindowForModel } from '../index.js'
3
+
4
+ describe('getContextWindowForModel', () => {
5
+ // 精确匹配测试
6
+ test('exact match with known model names', () => {
7
+ expect(getContextWindowForModel('doubao-seed-2.0-code')).toBe(256_000)
8
+ expect(getContextWindowForModel('deepseek-v4-pro')).toBe(1_000_000)
9
+ expect(getContextWindowForModel('qwen3.6-plus')).toBe(1_000_000)
10
+ expect(getContextWindowForModel('claude-sonnet-4.6')).toBe(1_000_000)
11
+ expect(getContextWindowForModel('gpt-5.5')).toBe(1_000_000)
12
+ })
13
+
14
+ // 大小写不敏感测试
15
+ test('case insensitive matching', () => {
16
+ expect(getContextWindowForModel('DOUBAO-SEED-2.0-CODE')).toBe(256_000)
17
+ expect(getContextWindowForModel('DeepSeek-V4-Pro')).toBe(1_000_000)
18
+ expect(getContextWindowForModel('QWEN3.6-PLUS')).toBe(1_000_000)
19
+ })
20
+
21
+ // 模糊匹配 - 含空格的变体
22
+ test('fuzzy match with space variants', () => {
23
+ expect(getContextWindowForModel('doubao seed 2.0 code')).toBe(256_000)
24
+ expect(getContextWindowForModel('deepseek v4 pro')).toBe(1_000_000)
25
+ expect(getContextWindowForModel('qwen 3.6 plus')).toBe(1_000_000)
26
+ })
27
+
28
+ // 模糊匹配 - 下划线与连字符变体
29
+ test('fuzzy match with underscore and hyphen variants', () => {
30
+ expect(getContextWindowForModel('doubao_seed_2.0_code')).toBe(256_000)
31
+ expect(getContextWindowForModel('deepseek_v4_pro')).toBe(1_000_000)
32
+ })
33
+
34
+ // 模糊匹配 - 含前缀和后缀的模型名
35
+ test('fuzzy match with prefixed and suffixed model names', () => {
36
+ expect(getContextWindowForModel('my-doubao-seed-2.0-code')).toBe(256_000)
37
+ expect(getContextWindowForModel('deepseek-v4-pro-variant')).toBe(1_000_000)
38
+ expect(getContextWindowForModel('qwen3.6-plus-prod')).toBe(1_000_000)
39
+ })
40
+
41
+ // 模糊匹配 - 关键词匹配(不完整的模型名)
42
+ test('fuzzy match with keyword matching (partial model names)', () => {
43
+ // 百万上下文模型
44
+ expect(getContextWindowForModel('deepseek-v4')).toBe(1_000_000)
45
+ expect(getContextWindowForModel('qwen3.6')).toBe(1_000_000)
46
+ expect(getContextWindowForModel('qwen3.7')).toBe(1_000_000)
47
+ expect(getContextWindowForModel('claude opus 4.7')).toBe(1_000_000)
48
+ expect(getContextWindowForModel('gemini 2.5')).toBe(1_000_000)
49
+
50
+ // 256K 上下文模型
51
+ expect(getContextWindowForModel('doubao-seed-2.0')).toBe(256_000)
52
+ expect(getContextWindowForModel('kimi-k2.5')).toBe(262_144) // 实际值
53
+ expect(getContextWindowForModel('kimi-k2.6')).toBe(262_144) // 实际值
54
+
55
+ // 200K 上下文模型
56
+ expect(getContextWindowForModel('minimax')).toBe(200_000)
57
+ expect(getContextWindowForModel('glm-5')).toBe(200_000)
58
+ expect(getContextWindowForModel('glm-5.1')).toBe(200_000)
59
+ expect(getContextWindowForModel('claude-haiku')).toBe(200_000)
60
+
61
+ // 160K 上下文模型
62
+ expect(getContextWindowForModel('deepseek-v3')).toBe(160_000)
63
+ expect(getContextWindowForModel('deepseek-v3.2')).toBe(160_000)
64
+
65
+ // 128K 上下文模型
66
+ expect(getContextWindowForModel('gpt-4o')).toBe(128_000)
67
+ expect(getContextWindowForModel('gpt-4')).toBe(128_000)
68
+ expect(getContextWindowForModel('llama-3')).toBe(128_000)
69
+ })
70
+
71
+ // 未知模型名 - 兜底默认值
72
+ test('unknown model names return default 128K', () => {
73
+ expect(getContextWindowForModel('unknown-model')).toBe(128_000)
74
+ expect(getContextWindowForModel('random-model-name')).toBe(128_000)
75
+ })
76
+
77
+ // 用户配置中常用的模型名
78
+ test('common model names from user configs', () => {
79
+ // 豆包模型
80
+ expect(getContextWindowForModel('Doubao-Seed-2.0-Code')).toBe(256_000)
81
+
82
+ // DeepSeek 模型
83
+ expect(getContextWindowForModel('deepseek-chat')).toBe(128_000)
84
+ expect(getContextWindowForModel('deepseek-reasoner')).toBe(128_000)
85
+
86
+ // Kimi 模型
87
+ expect(getContextWindowForModel('kimi-k2.5')).toBe(262_144) // 实际值
88
+ expect(getContextWindowForModel('kimi-k2.6')).toBe(262_144) // 实际值
89
+
90
+ // 通义千问
91
+ expect(getContextWindowForModel('qwen3.6-plus')).toBe(1_000_000)
92
+ expect(getContextWindowForModel('qwen3.7-max')).toBe(1_000_000)
93
+ })
94
+ })
@@ -8,6 +8,7 @@ export interface Config {
8
8
  mode: 'code' | 'plan' | 'ask'
9
9
  provider: 'openai' | 'anthropic' | 'google' | 'ollama'
10
10
  model: string
11
+ contextWindow: number
11
12
  cwd: string
12
13
  autoApprove: boolean
13
14
  thinking?: boolean
@@ -19,6 +20,243 @@ export interface Config {
19
20
  tavilyApiKey?: string
20
21
  }
21
22
 
23
+ /**
24
+ * 模型上下文窗口大小映射表(基于 2026 年最新数据)
25
+ * 单位:tokens
26
+ */
27
+ const MODEL_CONTEXT_WINDOWS: Record<string, number> = {
28
+ // Doubao 系列
29
+ 'doubao-seed-2.0-code': 256_000,
30
+ 'doubao-seed-2.0-pro': 256_000,
31
+ 'doubao-seed-2.0-lite': 256_000,
32
+ 'doubao-seed-2.0-mini': 256_000,
33
+ 'doubao-seed-2.0-code-preview-260215': 256_000,
34
+ 'doubao-seed-1.8': 256_000,
35
+ 'doubao-seed-1.6': 256_000,
36
+
37
+ // DeepSeek 系列
38
+ 'deepseek-v4-pro': 1_000_000,
39
+ 'deepseek-v4-flash': 256_000,
40
+ 'deepseek-v3.2': 160_000,
41
+ 'deepseek-r1': 128_000,
42
+ 'deepseek-chat': 128_000,
43
+ 'deepseek-reasoner': 128_000,
44
+
45
+ // Kimi 系列
46
+ 'kimi-k2.5': 262_144,
47
+ 'kimi-k2.6': 262_144,
48
+ 'kimi-k2-thinking': 256_000,
49
+
50
+ // MiniMax 系列
51
+ 'minimax-m2.5': 200_000,
52
+ 'minimax-m2.7': 200_000,
53
+ 'minimax-m2': 205_000,
54
+
55
+ // GLM 系列
56
+ 'glm-5': 200_000,
57
+ 'glm-5.1': 200_000,
58
+ 'glm-4.7-flashx': 200_000,
59
+ 'glm-4-flash': 128_000,
60
+ 'glm-4': 128_000,
61
+
62
+ // Qwen 系列
63
+ 'qwen3.6-plus': 1_000_000,
64
+ 'qwen3.7-max': 1_000_000,
65
+ 'qwen3-max': 256_000,
66
+ 'qwen3.5-plus': 1_000_000,
67
+ 'qwen-flash': 1_000_000,
68
+
69
+ // Claude 系列
70
+ 'claude-opus-4.7': 1_000_000,
71
+ 'claude-opus-4.6': 1_000_000,
72
+ 'claude-sonnet-4.6': 1_000_000,
73
+ 'claude-haiku-4.5': 200_000,
74
+
75
+ // GPT 系列
76
+ 'gpt-5.5': 1_000_000,
77
+ 'gpt-5.4': 1_000_000,
78
+ 'gpt-5.1': 1_000_000,
79
+ 'gpt-5.2-codex': 512_000,
80
+ 'gpt-4.1': 1_000_000,
81
+ 'gpt-4.1-mini': 1_000_000,
82
+ 'gpt-4o': 128_000,
83
+ 'gpt-4': 128_000,
84
+
85
+ // Gemini 系列
86
+ 'gemini-2.5-pro': 1_000_000,
87
+ 'gemini-2.5-flash': 1_000_000,
88
+ 'gemini-3.1-pro': 1_000_000,
89
+ 'gemini-3.1-flash': 1_000_000,
90
+
91
+ // Llama 系列
92
+ 'llama-4-maverick': 1_000_000,
93
+ 'llama-4-scout': 10_000_000,
94
+ 'llama-3.1': 128_000,
95
+ 'llama-3': 128_000,
96
+
97
+ // Mistral 系列
98
+ 'mistral-small-3.1': 128_000,
99
+ 'mistral-small-3.2': 128_000,
100
+ }
101
+
102
+ /**
103
+ * 模糊匹配规则
104
+ */
105
+ interface FuzzyMatchRule {
106
+ keywords: string[]
107
+ windowSize: number
108
+ }
109
+
110
+ /**
111
+ * 模糊匹配规则列表(从高优先级到低优先级)
112
+ */
113
+ const fuzzyMatchRules: FuzzyMatchRule[] = [
114
+ // 首先匹配 128K 模型(防止 gpt-4 匹配到 gpt-5 规则)
115
+ {
116
+ keywords: [
117
+ 'gpt-4o',
118
+ 'gpt 4o',
119
+ 'gpt-4',
120
+ 'gpt 4',
121
+ 'llama-3',
122
+ 'llama 3',
123
+ 'mistral-small',
124
+ 'mistral small',
125
+ 'glm-4-flash',
126
+ 'glm 4 flash',
127
+ 'glm-4',
128
+ ],
129
+ windowSize: 128_000,
130
+ },
131
+ // 百万上下文模型 - 更高优先级(更精确的关键词)
132
+ {
133
+ keywords: [
134
+ 'deepseek-v4',
135
+ 'deepseek v4',
136
+ 'qwen3.6',
137
+ 'qwen 3.6',
138
+ 'qwen3.7',
139
+ 'qwen 3.7',
140
+ 'qwen3.5',
141
+ 'qwen 3.5',
142
+ 'qwen-flash',
143
+ 'qwen flash',
144
+ ],
145
+ windowSize: 1_000_000,
146
+ },
147
+ {
148
+ keywords: [
149
+ 'claude-opus',
150
+ 'claude sonnet',
151
+ 'claude-sonnet',
152
+ 'gpt-5.',
153
+ 'gpt 5',
154
+ 'gpt-4.1',
155
+ 'gpt 4.1',
156
+ 'gemini-2.5',
157
+ 'gemini 2.5',
158
+ 'gemini-3.1',
159
+ 'gemini 3.1',
160
+ 'llama-4',
161
+ 'llama 4',
162
+ ],
163
+ windowSize: 1_000_000,
164
+ },
165
+ // 256K 上下文模型
166
+ {
167
+ keywords: [
168
+ 'doubao-seed-2.0',
169
+ 'doubao seed 2.0',
170
+ 'doubao-seed-1',
171
+ 'doubao seed 1',
172
+ 'kimi-k2',
173
+ 'kimi k2',
174
+ 'kimi-k2.5',
175
+ 'kimi k2.5',
176
+ 'kimi-k2.6',
177
+ 'kimi k2.6',
178
+ 'qwen3',
179
+ 'qwen 3',
180
+ 'qwen3-max',
181
+ 'qwen3 max',
182
+ ],
183
+ windowSize: 256_000,
184
+ },
185
+ // 200K 上下文模型
186
+ {
187
+ keywords: [
188
+ 'minimax',
189
+ 'glm-5',
190
+ 'glm 5',
191
+ 'glm-5.1',
192
+ 'glm 5.1',
193
+ 'glm-4.7',
194
+ 'glm 4.7',
195
+ 'claude-haiku',
196
+ 'claude haiku',
197
+ ],
198
+ windowSize: 200_000,
199
+ },
200
+ // 160K 上下文模型
201
+ {
202
+ keywords: ['deepseek-v3', 'deepseek v3', 'deepseek-v3.2', 'deepseek v3.2'],
203
+ windowSize: 160_000,
204
+ },
205
+ // 最后匹配通用关键词(deepseek)
206
+ {
207
+ keywords: ['deepseek'],
208
+ windowSize: 128_000,
209
+ },
210
+ ]
211
+
212
+ /**
213
+ * 根据模型名称获取上下文窗口大小(改进的模糊匹配)
214
+ * @param modelName 模型名称(不区分大小写)
215
+ * @returns 上下文窗口大小(tokens)
216
+ */
217
+ export function getContextWindowForModel(modelName: string): number {
218
+ // 处理空字符串
219
+ if (!modelName) {
220
+ return 128_000
221
+ }
222
+
223
+ const normalizedModelName = modelName.toLowerCase().replace(/[-_]/g, ' ')
224
+
225
+ // 1. 精确匹配(原始键名)
226
+ if (MODEL_CONTEXT_WINDOWS[modelName]) {
227
+ return MODEL_CONTEXT_WINDOWS[modelName]
228
+ }
229
+
230
+ // 2. 精确匹配(小写键名)
231
+ if (MODEL_CONTEXT_WINDOWS[modelName.toLowerCase()]) {
232
+ return MODEL_CONTEXT_WINDOWS[modelName.toLowerCase()]
233
+ }
234
+
235
+ // 3. 前缀匹配(支持部分匹配)
236
+ for (const [pattern, size] of Object.entries(MODEL_CONTEXT_WINDOWS)) {
237
+ const normalizedPattern = pattern.toLowerCase().replace(/[-_]/g, ' ')
238
+ if (
239
+ normalizedModelName.includes(normalizedPattern) ||
240
+ normalizedPattern.includes(normalizedModelName)
241
+ ) {
242
+ return size
243
+ }
244
+ }
245
+
246
+ // 4. 模糊匹配(关键词匹配)- 按照规则顺序(优先级)处理
247
+ for (const rule of fuzzyMatchRules) {
248
+ for (const keyword of rule.keywords) {
249
+ const normalizedKeyword = keyword.toLowerCase().replace(/[-_]/g, ' ')
250
+ if (normalizedModelName.includes(normalizedKeyword)) {
251
+ return rule.windowSize
252
+ }
253
+ }
254
+ }
255
+
256
+ // 5. 兜底默认值
257
+ return 128_000
258
+ }
259
+
22
260
  interface JsonConfig {
23
261
  apiKey?: string
24
262
  baseUrl?: string
@@ -69,6 +307,9 @@ export function loadConfig(options?: {
69
307
  // Auto-enable cache for DeepSeek models unless explicitly disabled
70
308
  const enableCache = jsonConfig.enableCache ?? (isDeepSeekModel(model, baseUrl) || undefined)
71
309
 
310
+ // 根据模型名称获取上下文窗口大小
311
+ const contextWindow = getContextWindowForModel(model)
312
+
72
313
  return {
73
314
  apiKey: jsonConfig.apiKey || '',
74
315
  baseUrl,
@@ -79,6 +320,7 @@ export function loadConfig(options?: {
79
320
  | 'ollama',
80
321
  mode: options?.mode || 'code',
81
322
  model,
323
+ contextWindow,
82
324
  cwd: options?.cwd || process.cwd(),
83
325
  autoApprove: options?.autoApprove ?? jsonConfig.autoApprove ?? false,
84
326
  thinking: jsonConfig.thinking,
package/src/pi-tui/tui.ts CHANGED
@@ -1351,26 +1351,20 @@ export class TUI extends Container {
1351
1351
  const targetRow = Math.max(0, Math.min(cursorPos.row, totalLines - 1))
1352
1352
  const targetCol = Math.max(0, cursorPos.col)
1353
1353
 
1354
- // Move cursor from current position to target
1355
- const rowDelta = targetRow - this.hardwareCursorRow
1356
- let buffer = ''
1357
- if (rowDelta > 0) {
1358
- buffer += `\x1b[${rowDelta}B` // Move down
1359
- } else if (rowDelta < 0) {
1360
- buffer += `\x1b[${-rowDelta}A` // Move up
1354
+ // Use absolute cursor positioning (CUP) to avoid scroll artifacts.
1355
+ // Relative movement (\x1b[NB/\x1b[NA) with a stale baseline can
1356
+ // cause the terminal to scroll, inserting blank lines, especially
1357
+ // when the editor overlay cursor is far from the rendered content.
1358
+ let buffer = `\x1b[${targetRow + 1};${targetCol + 1}H`
1359
+
1360
+ if (this.showHardwareCursor) {
1361
+ buffer += '\x1b[?25h'
1361
1362
  }
1362
- // Move to absolute column (1-indexed)
1363
- buffer += `\x1b[${targetCol + 1}G`
1364
1363
 
1365
1364
  if (buffer) {
1366
1365
  this.terminal.write(buffer)
1367
1366
  }
1368
1367
 
1369
1368
  this.hardwareCursorRow = targetRow
1370
- if (this.showHardwareCursor) {
1371
- this.terminal.showCursor()
1372
- } else {
1373
- this.terminal.hideCursor()
1374
- }
1375
1369
  }
1376
1370
  }