bingocode 1.1.131 → 1.1.133

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.
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "permissions": {
3
3
  "allow": [
4
+ "Read(//c/Users/qi.lin/.claude/**)",
4
5
  "Bash(dir \"F:\\\\Leanchy\\\\VirtuosAgent\\\\BingoCode\\\\src\\\\server\\\\proxy\")",
5
- "Bash(dir \"F:\\\\Leanchy\\\\VirtuosAgent\\\\BingoCode\\\\src\\\\server\")",
6
- "Bash(grep -rn \"stream\\\\|proxy\\\\|SSE\\\\|duplicate\\\\|repeat\\\\|render\" F:LeanchyVirtuosAgentBingoCodesrc --include=*.ts -l)",
7
- "Bash(xargs grep:*)"
6
+ "Bash(ls src/**/)",
7
+ "Bash(cat config/*.json)"
8
8
  ]
9
9
  }
10
10
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.1.131",
3
+ "version": "1.1.133",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -78,34 +78,46 @@ const TOP_H_COMPACT = Number(process.env.CLI_TOP_H_COMPACT || 3);
78
78
  // Bottom bar height
79
79
  const BOTTOM_H = Number(process.env.CLI_BOTTOM_H || 3);
80
80
 
81
+ const LANG_OPTIONS = [
82
+ { label: 'English', value: 'en' as const },
83
+ { label: '中文', value: 'zh' as const },
84
+ { label: '日本語', value: 'ja' as const },
85
+ ];
86
+
81
87
  const i18nMap = {
82
- zh: {
83
- menu: {
84
- newSession: 'New Session',
85
- history: 'History',
86
- provider: 'API Config',
87
- settings: 'Settings',
88
- about: 'About',
89
- exit: 'Exit',
90
- },
91
- about: 'Bingo CLI - Version Info & About',
92
- aboutContent: [
93
- 'Bingo is an AI assistant terminal client.',
94
- '1. API Config: Press "P" or select "API Config" to set up your keys.',
95
- '2. Model Slots: Configure specific models in the Provider panel.',
96
- '3. Background Service: Bingo runs a local server to manage sessions.',
97
- '4. Start Chat: Run `bingocode` or `claude` in any terminal to start.',
98
- ].join('\n'),
99
- aboutFooter: 'Author: leanchy (leanchy07@outlook.com) · github.com/leanchy/bingo-claude-code-offline-installer',
100
- mark: '→ Mark Session',
101
- unmark: '→ Unmark Session',
102
- tipsSimple: 'L Lang | ESC Back | ←→ Menu | ↩ Enter | ? Help',
103
- noData: 'No data',
104
- emptyHistory: 'Nothing here yet. Start a new session?',
105
- deleting: 'Delete this session? (Irreversible)',
106
- historyHint: 'Enter to open · j next · k first · q back',
107
- helpTitle: 'Shortcuts',
88
+ zh: {
89
+ menu: {
90
+ newSession: '新建会话',
91
+ history: '会话历史',
92
+ provider: 'API 配置',
93
+ settings: '设置',
94
+ about: '关于',
95
+ exit: '退出',
108
96
  },
97
+ about: 'Bingo CLI 终端 - 版本信息与关于',
98
+ aboutContent: [
99
+ 'Bingo 是一款 AI 助手终端客户端。',
100
+ '1. API 配置:按 "P" 或选择「API 配置」来设置你的密钥。',
101
+ '2. 模型槽:在 Provider 面板中配置各模型。',
102
+ '3. 后台服务:Bingo 会运行一个本地服务器来管理会话。',
103
+ '4. 开始聊天:在任意终端中运行 `bingocode` 或 `claude`。',
104
+ ].join('\n'),
105
+ aboutFooter: '作者: leanchy (leanchy07@outlook.com) · github.com/leanchy/bingo-claude-code-offline-installer',
106
+ mark: '→ 标记会话',
107
+ unmark: '→ 取消标记',
108
+ tipsSimple: 'L 语言 | ESC 返回 | ←→ 菜单 | ↩ 确认 | ? 帮助',
109
+ noData: '暂无数据',
110
+ emptyHistory: '还没有会话,要新建一个吗?',
111
+ deleting: '确定删除此会话?(不可恢复)',
112
+ historyHint: '↩ 打开 · j 下一页 · k 首页 · q 返回',
113
+ helpTitle: '快捷键',
114
+ // Settings page
115
+ settingsTitle: '设置',
116
+ langLabel: '语言',
117
+ langPickerTitle: '选择语言',
118
+ settingsHint: '↑/k ↓/j 滚动 · ↩ 编辑 · ESC 返回',
119
+ langOptions: LANG_OPTIONS,
120
+ },
109
121
  en: {
110
122
  menu: {
111
123
  newSession: 'New Session',
@@ -132,7 +144,46 @@ const i18nMap = {
132
144
  deleting: 'Delete this session? (Irreversible)',
133
145
  historyHint: 'Enter to open · j next · k first · q back',
134
146
  helpTitle: 'Shortcuts',
135
- }
147
+ // Settings page
148
+ settingsTitle: 'Settings',
149
+ langLabel: 'Language',
150
+ langPickerTitle: 'Select Language',
151
+ settingsHint: '↑/k ↓/j scroll · ↩ edit · ESC back',
152
+ langOptions: LANG_OPTIONS,
153
+ },
154
+ ja: {
155
+ menu: {
156
+ newSession: '新規セッション',
157
+ history: 'セッション履歴',
158
+ provider: 'API設定',
159
+ settings: '設定',
160
+ about: 'について',
161
+ exit: '終了',
162
+ },
163
+ about: 'Bingo CLI ターミナル - バージョン情報',
164
+ aboutContent: [
165
+ 'BingoはAIアシスタントのターミナルクライアントです。',
166
+ '1. API設定: "P"キーまたは「API設定」を選択してキーを設定。',
167
+ '2. モデルスロット: Providerパネルで各モデルを設定。',
168
+ '3. バックグラウンドサービス: セッション管理用ローカルサーバーを起動。',
169
+ '4. チャット開始: 任意のターミナルで `bingocode` または `claude` を実行。',
170
+ ].join('\n'),
171
+ aboutFooter: '作者: leanchy (leanchy07@outlook.com) · github.com/leanchy/bingo-claude-code-offline-installer',
172
+ mark: '→ セッションをマーク',
173
+ unmark: '→ マークを解除',
174
+ tipsSimple: 'L 言語 | ESC 戻る | ←→ メニュー | ↩ 決定 | ? ヘルプ',
175
+ noData: 'データなし',
176
+ emptyHistory: 'まだセッションがありません。新規作成しますか?',
177
+ deleting: 'このセッションを削除しますか?(元に戻せません)',
178
+ historyHint: '↩ 開く · j 次へ · k 最初へ · q 戻る',
179
+ helpTitle: 'ショートカット',
180
+ // Settings page
181
+ settingsTitle: '設定',
182
+ langLabel: '言語',
183
+ langPickerTitle: '言語を選択',
184
+ settingsHint: '↑/k ↓/j スクロール · ↩ 編集 · ESC 戻る',
185
+ langOptions: LANG_OPTIONS,
186
+ },
136
187
  };
137
188
 
138
189
  const menuKeys = [
@@ -243,7 +294,7 @@ export const CliMenuManager: React.FC = () => {
243
294
  if (configReady) {
244
295
  try {
245
296
  const cfg = getGlobalConfig();
246
- if (cfg.language && (cfg.language === 'en' || cfg.language === 'zh')) {
297
+ if (cfg.language && (cfg.language === 'en' || cfg.language === 'zh' || cfg.language === 'ja')) {
247
298
  setLang(cfg.language as Lang);
248
299
  }
249
300
  } catch (e) {
@@ -294,6 +345,8 @@ export const CliMenuManager: React.FC = () => {
294
345
  const [settingData, setSettingData] = useState<any>(null);
295
346
  const [loadingSetting, setLoadingSetting] = useState(false);
296
347
  const [setErr, setSetErr] = useState<string | null>(null);
348
+ const [settingsStage, setSettingsStage] = useState<'list' | 'langPicker'>('list');
349
+ const [settingsCursor, setSettingsCursor] = useState(0);
297
350
 
298
351
  // Top toolbar state
299
352
  const [animEnabled, setAnimEnabled] = useState(true);
@@ -373,6 +426,8 @@ export const CliMenuManager: React.FC = () => {
373
426
  }
374
427
  if (page !== 'settings') {
375
428
  setSettingsOffset(0);
429
+ setSettingsStage('list');
430
+ setSettingsCursor(0);
376
431
  }
377
432
  // Close help overlay
378
433
  setShowHelp(false);
@@ -515,9 +570,10 @@ export const CliMenuManager: React.FC = () => {
515
570
 
516
571
  // Keyboard interactions
517
572
  useInput((input, key) => {
518
- // Language toggle
573
+ // Language toggle (en → zh → ja → en)
519
574
  if (input === 'l' || input === 'L') {
520
- const nextLang = lang === 'zh' ? 'en' : 'zh';
575
+ const langOrder: Lang[] = ['en', 'zh', 'ja'];
576
+ const nextLang = langOrder[(langOrder.indexOf(lang) + 1) % langOrder.length];
521
577
  setLang(nextLang);
522
578
  try {
523
579
  const cfg = getGlobalConfig();
@@ -563,6 +619,10 @@ export const CliMenuManager: React.FC = () => {
563
619
  if (key.escape) {
564
620
  if (showHelp) { setShowHelp(false); return; }
565
621
  if (page === 'provider') return; // Handled internally
622
+ // Settings: langPicker → back to list; list → back to main menu
623
+ if (page === 'settings') {
624
+ if (settingsStage === 'langPicker') { setSettingsStage('list'); return; }
625
+ }
566
626
  setPage(null);
567
627
  setHistoryMenuStage('list');
568
628
  setSelectedHistory(null);
@@ -670,18 +730,30 @@ export const CliMenuManager: React.FC = () => {
670
730
  }
671
731
  }
672
732
 
673
- // Settings scrolling
674
- if (!showHelp && page === 'settings' && settingData && typeof settingData === 'object') {
675
- const total = Object.keys(settingData).length;
676
- const visible = Math.max(1, MID_H - 1);
677
- if (key.downArrow || input === 'j') {
678
- setSettingsOffset(o => Math.min(Math.max(0, total - visible), o + 1));
679
- }
680
- if (key.upArrow || input === 'k') {
681
- setSettingsOffset(o => Math.max(0, o - 1));
733
+ // Settings interactions
734
+ if (!showHelp && page === 'settings') {
735
+ if (settingsStage === 'list') {
736
+ // +1 for the fixed Language row prepended before settingData entries
737
+ const totalRows = 1 + (settingData && typeof settingData === 'object' ? Object.keys(settingData).length : 0);
738
+ const visible = Math.max(1, MID_H - 2);
739
+ if (key.downArrow || input === 'j') {
740
+ setSettingsCursor(c => Math.min(totalRows - 1, c + 1));
741
+ setSettingsOffset(o => Math.min(Math.max(0, totalRows - visible), o + 1));
742
+ }
743
+ if (key.upArrow || input === 'k') {
744
+ setSettingsCursor(c => Math.max(0, c - 1));
745
+ setSettingsOffset(o => Math.max(0, o - 1));
746
+ }
747
+ if (key.return) {
748
+ // Row 0 is the interactive Language row
749
+ if (settingsCursor === 0) {
750
+ setSettingsStage('langPicker');
751
+ }
752
+ }
682
753
  }
754
+ // langPicker stage: ESC handled above; selection via SelectInput onSelect
683
755
  }
684
- }, [menuItems, page, historyMenuStage, historyList, historyHasMore, navIndex, sessionMessages, settingData, MID_H, MSGS_PAGE_SIZE, showHelp, theme]);
756
+ }, [menuItems, page, historyMenuStage, historyList, historyHasMore, navIndex, sessionMessages, settingData, MID_H, MSGS_PAGE_SIZE, showHelp, theme, settingsStage, settingsCursor]);
685
757
 
686
758
  function cleanText(text: string): string {
687
759
  return String(text ?? '').replace(/[\n\r]+/g, ' ').replace(/\u001b\[[0-9;]*m/g, '').trim();
@@ -915,7 +987,7 @@ export const CliMenuManager: React.FC = () => {
915
987
  <Text color="cyan">R</Text><Text> Quick Resume</Text>
916
988
  <Text color="cyan">P</Text><Text> Open Provider Config</Text>
917
989
  <Text color="cyan">G</Text><Text> Toggle Theme (light/dark/highContrast)</Text>
918
- <Text color="cyan">L</Text><Text> Toggle Language (en/zh)</Text>
990
+ <Text color="cyan">L</Text><Text> Toggle Language (enzh → ja)</Text>
919
991
  <Text color="cyan">O</Text><Text> Toggle Top Animation</Text>
920
992
  <Text color="cyan">T</Text><Text> Toggle Top Tips</Text>
921
993
  <Text color="cyan">?</Text><Text> Toggle Help</Text>
@@ -1130,18 +1202,84 @@ export const CliMenuManager: React.FC = () => {
1130
1202
  if (page === 'settings') {
1131
1203
  if (loadingSetting) return <StateDisplay type="loading" message="Loading settings..." />;
1132
1204
  if (setErr) return <StateDisplay type="error" message={setErr} />;
1133
- if (!settingData || typeof settingData !== 'object') return <StateDisplay type="empty" message="No settings found" />;
1134
- const entries = Object.entries(settingData);
1135
- const visible = Math.max(1, MID_H - 1);
1136
- const start = Math.min(settingsOffset, Math.max(0, entries.length - visible));
1137
- const sliced = entries.slice(start, start + visible);
1205
+
1206
+ const tS = i18nMap[lang];
1207
+ const currentLangLabel = LANG_OPTIONS.find(o => o.value === lang)?.label ?? lang;
1208
+
1209
+ // --- langPicker sub-menu ---
1210
+ if (settingsStage === 'langPicker') {
1211
+ return (
1212
+ <Box width={VIEW_W} height={MID_H} flexDirection="column">
1213
+ <Box paddingX={1} marginBottom={1}>
1214
+ <Text color="magenta" bold>{tS.langPickerTitle}</Text>
1215
+ </Box>
1216
+ <Box paddingX={2} flexGrow={1} flexDirection="column">
1217
+ <SelectInput
1218
+ items={tS.langOptions}
1219
+ initialIndex={tS.langOptions.findIndex(o => o.value === lang)}
1220
+ onSelect={(item: { label: string; value: Lang }) => {
1221
+ setLang(item.value);
1222
+ try {
1223
+ const cfg = getGlobalConfig();
1224
+ cfg.language = item.value;
1225
+ saveGlobalConfig(cfg);
1226
+ } catch {}
1227
+ setSettingsStage('list');
1228
+ }}
1229
+ />
1230
+ </Box>
1231
+ <Box paddingX={1}>
1232
+ <Hint>↩ confirm · ESC back</Hint>
1233
+ </Box>
1234
+ </Box>
1235
+ );
1236
+ }
1237
+
1238
+ // --- settings list ---
1239
+ type SettingRow = { key: string; label: string; value: string; interactive: boolean };
1240
+ const fixedRows: SettingRow[] = [
1241
+ { key: '__lang', label: tS.langLabel, value: currentLangLabel, interactive: true },
1242
+ ];
1243
+ const dataEntries = settingData && typeof settingData === 'object' ? Object.entries(settingData) : [];
1244
+ const dataRows: SettingRow[] = dataEntries.map(([k, v]) => ({
1245
+ key: k,
1246
+ label: k,
1247
+ value: typeof v === 'object' ? JSON.stringify(v) : String(v),
1248
+ interactive: false,
1249
+ }));
1250
+ const allRows: SettingRow[] = [...fixedRows, ...dataRows];
1251
+ const visible = Math.max(1, MID_H - 2);
1252
+ const start = Math.min(settingsOffset, Math.max(0, allRows.length - visible));
1253
+ const sliced = allRows.slice(start, start + visible);
1254
+
1138
1255
  return (
1139
1256
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
1140
- <ScrollBar total={entries.length} offset={start} height={visible - 1} />
1141
- {sliced.map(([k, v]) => <Text key={k}>{k}: {typeof v === 'object' ? JSON.stringify(v) : String(v)}</Text>)}
1142
- <Hint>
1143
- ↑/k and ↓/j scroll · {start+1}-{Math.min(start+visible, entries.length)}/{entries.length}
1144
- </Hint>
1257
+ <Box flexDirection="row" position="relative" flexGrow={1}>
1258
+ <Box flexDirection="column" flexGrow={1} paddingX={1} overflow="hidden">
1259
+ {sliced.map((row, idx) => {
1260
+ const absIdx = start + idx;
1261
+ const isCursor = absIdx === settingsCursor;
1262
+ const prefix = isCursor ? '>' : ' ';
1263
+ const labelColor = isCursor ? 'cyan' : (row.interactive ? 'white' : 'gray');
1264
+ const valueColor = row.interactive ? 'green' : undefined;
1265
+ return (
1266
+ <Box key={row.key} height={1}>
1267
+ <Text color={labelColor}>
1268
+ {prefix} {row.label}:{' '}
1269
+ <Text color={valueColor ?? (isCursor ? 'white' : 'gray')}>
1270
+ {row.value}
1271
+ {row.interactive ? ' ↩' : ''}
1272
+ </Text>
1273
+ </Text>
1274
+ </Box>
1275
+ );
1276
+ })}
1277
+ </Box>
1278
+ <ScrollBar total={allRows.length} offset={start} height={visible - 1} />
1279
+ </Box>
1280
+ <Box paddingX={1}>
1281
+ <Hint>{tS.settingsHint} · {start + 1}-{Math.min(start + visible, allRows.length)}/{allRows.length}</Hint>
1282
+ </Box>
1145
1283
  </Box>
1146
1284
  );
1147
1285
  }
@@ -298,20 +298,40 @@ function extractReasoning(delta: DeltaEx): { thinking: string; signature: string
298
298
  return null
299
299
  }
300
300
 
301
- // ─── Main chunk processing ─────────────────────────────────
302
-
303
301
  /**
304
- * Process a single SSE chunk using dual-pass logic:
305
- * Pass 1 reasoning/thinking (if present)
306
- * Pass 2 — text content (if present)
307
- * Pass 3 — tool calls (if present; mutually exclusive with text/thinking)
308
- *
309
- * This avoids the single-return priority chain that caused spurious
310
- * close/open cycles when providers (Gemini via OpenRouter, DeepSeek, Qwen3, …)
311
- * send reasoning_content and content in the same chunk or in alternating chunks,
312
- * which previously produced multiple text content_block_start events and
313
- * duplicate rendering in Claude Code's Ink terminal UI.
302
+ * Determine what block type this chunk carries and whether it's a new block.
303
+ * Priority (matches LiteLLM): tool_calls > text > reasoning > ignore
314
304
  */
305
+ function detectBlockTransition(
306
+ delta: DeltaEx,
307
+ state: StreamState,
308
+ ): { type: ContentBlockType; isNew: boolean } | null {
309
+ // Priority 1: Tool calls
310
+ if (delta.tool_calls && delta.tool_calls.length > 0) {
311
+ const tc = delta.tool_calls[0]
312
+ // A tool call with function.name signals a NEW tool block
313
+ const isNew = state.currentBlockType !== 'tool_use' || !!(tc.function?.name)
314
+ return { type: 'tool_use', isNew }
315
+ }
316
+
317
+ // Priority 2: Text content
318
+ if (delta.content != null && delta.content !== '') {
319
+ const isNew = state.currentBlockType !== 'text' || !state.blockStartSent
320
+ return { type: 'text', isNew }
321
+ }
322
+
323
+ // Priority 3: Reasoning/thinking
324
+ const reasoning = extractReasoning(delta)
325
+ if (reasoning) {
326
+ const isNew = state.currentBlockType !== 'thinking' || !state.blockStartSent
327
+ return { type: 'thinking', isNew }
328
+ }
329
+
330
+ return null
331
+ }
332
+
333
+ // ─── Main chunk processing ─────────────────────────────────
334
+
315
335
  function processChunk(chunk: OpenAIChatStreamChunk, state: StreamState): void {
316
336
  const choice = chunk.choices?.[0]
317
337
 
@@ -330,33 +350,31 @@ function processChunk(chunk: OpenAIChatStreamChunk, state: StreamState): void {
330
350
 
331
351
  const delta = choice.delta as DeltaEx
332
352
 
333
- // Tool calls are mutually exclusive with text/thinking — handle separately
334
- if (delta.tool_calls && delta.tool_calls.length > 0) {
335
- // Close any open text/thinking block before entering tool_use
336
- if (state.currentBlockType !== 'tool_use' && state.blockStartSent && !state.blockStopSent) {
337
- closeCurrentBlock(state)
338
- }
339
- handleToolCalls(delta, state)
340
- } else {
341
- // Pass 1: reasoning/thinking
342
- const reasoning = extractReasoning(delta)
343
- if (reasoning) {
344
- // If currently in a text block, close it before opening thinking
345
- if (state.currentBlockType === 'text' && state.blockStartSent && !state.blockStopSent) {
353
+ // Detect what this chunk carries
354
+ const transition = detectBlockTransition(delta, state)
355
+
356
+ if (transition) {
357
+ // Handle block transition: close previous block if type changed
358
+ if (transition.isNew && state.blockStartSent && !state.blockStopSent) {
359
+ if (transition.type !== 'tool_use') {
360
+ // For text/thinking, close the current block
361
+ closeCurrentBlock(state)
362
+ } else if (state.currentBlockType !== 'tool_use') {
363
+ // Switching TO tool_use from text/thinking: close current
346
364
  closeCurrentBlock(state)
347
365
  }
348
- handleThinking(delta, state)
349
366
  }
350
367
 
351
- // Pass 2: text content
352
- // After thinking is handled, resume/open text block independently.
353
- // This is the key fix: text is NOT skipped when reasoning was also present.
354
- if (delta.content != null && delta.content !== '') {
355
- // If currently in a thinking block, close it before opening text
356
- if (state.currentBlockType === 'thinking' && state.blockStartSent && !state.blockStopSent) {
357
- closeCurrentBlock(state)
358
- }
359
- handleText(delta, state)
368
+ switch (transition.type) {
369
+ case 'thinking':
370
+ handleThinking(delta, state)
371
+ break
372
+ case 'text':
373
+ handleText(delta, state)
374
+ break
375
+ case 'tool_use':
376
+ handleToolCalls(delta, state)
377
+ break
360
378
  }
361
379
  }
362
380
 
@@ -45,9 +45,13 @@ export function anthropicToOpenaiChat(body: AnthropicRequest): OpenAIChatRequest
45
45
 
46
46
  // max_tokens — cap to avoid upstream 400 errors from Claude's high defaults (e.g. 64k).
47
47
  // DeepSeek: tools/thinking fail above 8192. Other providers: 32768 covers most upstreams.
48
+ // GPT models (gpt-*): use max_completion_tokens instead of max_tokens (required by newer GPT models).
48
49
  if (body.max_tokens !== undefined) {
49
- if (body.model.toLowerCase().includes('deepseek')) {
50
+ const modelLower = body.model.toLowerCase()
51
+ if (modelLower.includes('deepseek')) {
50
52
  result.max_tokens = Math.min(body.max_tokens, 8192)
53
+ } else if (modelLower.startsWith('gpt-') || modelLower.startsWith('o1') || modelLower.startsWith('o3') || modelLower.startsWith('o4')) {
54
+ result.max_completion_tokens = body.max_tokens
51
55
  } else {
52
56
  result.max_tokens = Math.min(body.max_tokens, 32768)
53
57
  }
@@ -242,7 +242,7 @@ export type GlobalConfig = {
242
242
  hasUsedBackgroundTask?: boolean // Whether the user has backgrounded a task (Ctrl+B)
243
243
  queuedCommandUpHintCount?: number // Counter for how many times the user has seen the queued command up hint
244
244
  diffTool?: DiffTool // Which tool to use for displaying diffs (terminal or vscode)
245
- language: 'en' | 'zh' // User's preferred language for CLI menus
245
+ language: 'en' | 'zh' | 'ja' // User's preferred language for CLI menus
246
246
 
247
247
  // Terminal setup state tracking
248
248
  iterm2SetupInProgress?: boolean