bingocode 1.0.21 → 1.0.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bingocode",
3
- "version": "1.0.21",
3
+ "version": "1.0.22",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "claude": "bin/claude-win.cjs",
@@ -5,7 +5,7 @@ import { setupTerminal, shouldOfferTerminalSetup } from '../commands/terminalSet
5
5
  import { useExitOnCtrlCDWithKeybindings } from '../hooks/useExitOnCtrlCDWithKeybindings.js';
6
6
  import { Box, Link, Newline, Text, useTheme } from '../ink.js';
7
7
  import { useKeybindings } from '../keybindings/useKeybinding.js';
8
- import { isAnthropicAuthEnabled } from '../utils/auth.js';
8
+ import { isAnthropicAuthEnabled, isManagedOAuthContext } from '../utils/auth.js';
9
9
  import { normalizeApiKeyForConfig } from '../utils/authPortable.js';
10
10
  import { getCustomApiKeyStatus } from '../utils/config.js';
11
11
  import { env } from '../utils/env.js';
@@ -114,7 +114,11 @@ export function Onboarding({
114
114
  goToNextStep();
115
115
  }
116
116
  const steps: OnboardingStep[] = [];
117
- if (oauthEnabled) {
117
+ // Skip preflight in managed OAuth context (claude-desktop / CCR):
118
+ // The desktop app already verifies local server connectivity,
119
+ // and the Anthropic /api/hello endpoint may return 400 before OAuth login,
120
+ // which would incorrectly kill the onboarding flow.
121
+ if (oauthEnabled && !isManagedOAuthContext()) {
118
122
  steps.push({
119
123
  id: 'preflight',
120
124
  component: preflightStep
@@ -17,6 +17,9 @@ import { TopToolbar } from '../manager/TopToolbar.tsx';
17
17
 
18
18
  // 主题切换(Hook)
19
19
  import { useTheme } from '../components/design-system/ThemeProvider.js';
20
+ // Markdown 渲染(纯函数,不依赖 AppStateProvider context)
21
+ import { applyMarkdown } from '../utils/markdown.js';
22
+ import { Ansi } from '../ink/Ansi.js';
20
23
 
21
24
  // 配置相关(仅使用可用接口)
22
25
  import { getGlobalConfig, saveGlobalConfig } from '../utils/config.ts';
@@ -132,21 +135,57 @@ function loadMarkedSessionIds(): Set<string> {
132
135
  }
133
136
  }
134
137
 
135
- //@C:F ID=F.CM.saveMarkedSessionIds;K=F;V=1.0;P=save marked ids;D=CLI;M=cli;S=persist;In=Set<string>;Out=void
138
+ //@C:F ID=F.CM.saveMarkedSessionIds;K=F;V=1.1;P=save marked ids;D=CLI;M=cli;S=persist;In=Set<string>;Out=void
136
139
  function saveMarkedSessionIds(set: Set<string>) {
137
140
  try {
141
+ const dir = path.dirname(MARKED_FILE);
142
+ if (!fs.existsSync(dir)) {
143
+ fs.mkdirSync(dir, { recursive: true });
144
+ }
138
145
  fs.writeFileSync(MARKED_FILE, JSON.stringify([...set]), 'utf-8');
139
- } catch {}
146
+ } catch (err) {
147
+ console.error('[saveMarkedSessionIds] 写入失败:', err);
148
+ }
140
149
  }
141
150
 
142
- // 通用消息结构,供 <Messages />
143
- type ChatMessage = {
151
+ // 消息条目(与后端 MessageEntry 对齐)
152
+ type MessageEntry = {
144
153
  id: string;
145
- userId: string;
146
- content: string;
147
- createdAt?: string;
154
+ type: 'user' | 'assistant' | 'system' | 'tool_use' | 'tool_result';
155
+ content: unknown; // string 或 ContentBlock[]
156
+ timestamp: string;
157
+ model?: string;
158
+ parentUuid?: string;
159
+ parentToolUseId?: string;
160
+ isSidechain?: boolean;
148
161
  };
149
162
 
163
+ /** 从 MessageEntry.content 提取纯文本 */
164
+ function extractTextFromContent(content: unknown): string {
165
+ if (typeof content === 'string') return content;
166
+ if (Array.isArray(content)) {
167
+ return content
168
+ .map((block: any) => {
169
+ if (block.type === 'text' && typeof block.text === 'string') return block.text;
170
+ if (block.type === 'tool_use') return `[Tool: ${block.name || 'unknown'}]`;
171
+ if (block.type === 'tool_result') {
172
+ if (typeof block.content === 'string') return block.content;
173
+ if (Array.isArray(block.content)) {
174
+ return block.content
175
+ .filter((b: any) => b.type === 'text')
176
+ .map((b: any) => b.text)
177
+ .join('\n');
178
+ }
179
+ return '[Tool Result]';
180
+ }
181
+ return '';
182
+ })
183
+ .filter(Boolean)
184
+ .join('\n');
185
+ }
186
+ return String(content ?? '');
187
+ }
188
+
150
189
  //@C:F ID=F.CM.CliMenuManager;K=F;V=1.5;P=CLI 主菜单;D=CLI;M=cli;S=main;In=;Out=JSX.Element
151
190
  export const CliMenuManager: React.FC = () => {
152
191
  const { stdout } = useStdout();
@@ -208,8 +247,8 @@ export const CliMenuManager: React.FC = () => {
208
247
  const [historyMenuStage, setHistoryMenuStage] = useState<'list'|'window'|'deleteConfirm'>('list');
209
248
  const [selectedHistory, setSelectedHistory] = useState<any|null>(null);
210
249
 
211
- // 历史-消息内容(分页视口)
212
- const [sessionMessages, setSessionMessages] = useState<ChatMessage[]>([]);
250
+ // 历史-消息内容
251
+ const [sessionMessages, setSessionMessages] = useState<MessageEntry[]>([]);
213
252
  const [loadingMsgs, setLoadingMsgs] = useState(false);
214
253
  const [msgsErr, setMsgsErr] = useState<string | null>(null);
215
254
  const [msgsPage, setMsgsPage] = useState(0); // 0=最新页(底部),1=向上翻一页
@@ -380,9 +419,26 @@ export const CliMenuManager: React.FC = () => {
380
419
 
381
420
  // 会话消息获取
382
421
  useEffect(() => {
383
- // 进入 window 不再加载消息(只看属性)
384
- if (false) {
385
- /* 保留原实现以备将来恢复消息视图时启用 */
422
+ if (page === 'history' && historyMenuStage === 'window' && selectedHistory && apiUrl) {
423
+ let cancelled = false;
424
+ setLoadingMsgs(true);
425
+ setMsgsErr(null);
426
+ setSessionMessages([]);
427
+ setMsgsPage(0);
428
+ (async () => {
429
+ try {
430
+ const resp = await axios.get(`${apiUrl}/api/sessions/${selectedHistory.id}/messages`);
431
+ if (!cancelled) {
432
+ const msgs: MessageEntry[] = resp.data?.messages ?? [];
433
+ setSessionMessages(msgs);
434
+ }
435
+ } catch (e: any) {
436
+ if (!cancelled) setMsgsErr(e.message || '消息加载失败');
437
+ } finally {
438
+ if (!cancelled) setLoadingMsgs(false);
439
+ }
440
+ })();
441
+ return () => { cancelled = true; };
386
442
  } else {
387
443
  setSessionMessages([]);
388
444
  setLoadingMsgs(false);
@@ -531,6 +587,15 @@ export const CliMenuManager: React.FC = () => {
531
587
  handleHistoryMenuAction('__back');
532
588
  return;
533
589
  }
590
+ // 消息滚动:↑/k 向上翻页,↓/j 向下翻页
591
+ if (key.upArrow || input === 'k') {
592
+ setMsgsPage(p => Math.max(0, p - 1));
593
+ return;
594
+ }
595
+ if (key.downArrow || input === 'j') {
596
+ setMsgsPage(p => p + 1);
597
+ return;
598
+ }
534
599
 
535
600
  } else if (historyMenuStage === 'deleteConfirm') {
536
601
  if (input === 'q') {
@@ -860,54 +925,62 @@ export const CliMenuManager: React.FC = () => {
860
925
  }
861
926
 
862
927
  if (historyMenuStage === 'window' && selectedHistory) {
863
- const halfH = Math.floor(MID_H / 2);
864
928
  const isMarked = markedSessionIds.has(selectedHistory.id);
865
- const menu = secondaryMenu ?? {
866
- title: lang === 'zh' ? '会话操作' : 'Session Actions',
867
- items: [
868
- { label: (isMarked ? i18nMap[lang].unmark : i18nMap[lang].mark), value: '__toggle_mark' },
869
- { label: '→ 继续会话聊天', value: '__continue' },
870
- { label: '→ 删除会话聊天', value: '__delete' },
871
- { label: '← 返回历史列表', value: '__back' },
872
- ],
873
- onSelect: (item: any) => {
874
- if (item.value === '__back') {
875
- setHistoryMenuStage('list');
876
- setSelectedHistory(null);
877
- setMsgsPage(0);
878
- } else if (item.value === '__continue' && selectedHistory) {
879
- resumeSession(selectedHistory.id);
880
- } else if (item.value === '__delete') {
881
- setHistoryMenuStage('deleteConfirm');
882
- } else if (item.value === '__toggle_mark' && selectedHistory) {
883
- toggleMarkSession(selectedHistory.id);
884
- }
885
- }
886
- };
929
+
930
+ // ── 信息栏高度固定 3 行(标题 + 元信息 + 提示) ──
931
+ const INFO_H = 3;
932
+ const MSGS_H = Math.max(3, MID_H - INFO_H);
933
+
934
+ // ── 过滤出可展示的消息(user / assistant / system,跳过 tool_use / tool_result) ──
935
+ const displayMsgs = sessionMessages.filter(
936
+ m => m.type === 'user' || m.type === 'assistant' || m.type === 'system'
937
+ );
938
+
939
+ // ── 分页:每页 MSGS_PAGE_SIZE 条消息 ──
940
+ const totalPages = Math.max(1, Math.ceil(displayMsgs.length / MSGS_PAGE_SIZE));
941
+ const safePage = Math.min(msgsPage, totalPages - 1);
942
+ const pageStart = safePage * MSGS_PAGE_SIZE;
943
+ const pageMsgs = displayMsgs.slice(pageStart, pageStart + MSGS_PAGE_SIZE);
887
944
 
888
945
  return (
889
946
  <Box width={VIEW_W} height={MID_H} flexDirection="column">
890
- {/* 上半区:只显示会话属性 */}
891
- <Box height={halfH} flexDirection="column">
947
+ {/* ── 顶部信息栏 ── */}
948
+ <Box height={INFO_H} flexDirection="column">
892
949
  <Text color={isMarked ? 'yellow' : 'cyan'}>
893
- {isMarked ? '★ ' : ''}会话属性
950
+ {isMarked ? '★ ' : ''}{selectedHistory.title || 'Untitled'}
951
+ <Text dimColor> {selectedHistory.createdAt?.slice(0, 16).replace('T', ' ') || ''} · {displayMsgs.length} msgs</Text>
894
952
  </Text>
895
- <Text>id: {selectedHistory.id}</Text>
896
- <Text>标题: {selectedHistory.title}</Text>
897
- <Text>创建时间: {selectedHistory.createdAt}</Text>
898
- <Text>消息数: {selectedHistory.messageCount ?? 0}</Text>
899
- <Text>已标记: {isMarked ? '是' : '否'}</Text>
900
- <Hint>m 标记/取消 · c 继续 · d 删除 · q 返回</Hint>
953
+ <Hint>
954
+ j/↓ 下翻 · k/↑ 上翻 · m 标记 · c 继续 · d 删除 · q 返回
955
+ {displayMsgs.length > MSGS_PAGE_SIZE ? ` [${safePage + 1}/${totalPages}]` : ''}
956
+ </Hint>
901
957
  </Box>
902
958
 
903
- {/* 下半区:三级菜单 */}
904
- <Box height={MID_H - halfH} flexDirection="column" borderStyle="round" borderColor="cyan" paddingLeft={1} paddingRight={1}>
905
- <Text>{menu.title}</Text>
906
- <SelectInput
907
- items={menu.items.map(it => ({ label: it.label, value: it.value }))}
908
- onSelect={(item) => handleHistoryMenuAction(String(item.value))}
909
- />
910
- <Hint>↩ 执行 · q 返回</Hint>
959
+ {/* ── 消息区 ── */}
960
+ <Box height={MSGS_H} flexDirection="column" overflow="hidden">
961
+ {loadingMsgs && <Text color="yellow">加载消息中...</Text>}
962
+ {msgsErr && <Text color="red">错误: {msgsErr}</Text>}
963
+ {!loadingMsgs && !msgsErr && displayMsgs.length === 0 && (
964
+ <Text dimColor>无消息记录</Text>
965
+ )}
966
+ {pageMsgs.map((msg) => {
967
+ const text = extractTextFromContent(msg.content);
968
+ if (!text.trim()) return null;
969
+ const isUser = msg.type === 'user';
970
+ const isSystem = msg.type === 'system';
971
+ const roleLabel = isUser ? '👤 You' : isSystem ? '⚙ System' : '🤖 Assistant';
972
+ const roleColor = isUser ? 'green' : isSystem ? 'gray' : 'cyan';
973
+ return (
974
+ <Box key={msg.id} flexDirection="column" marginBottom={1}>
975
+ <Text color={roleColor} bold>{roleLabel}</Text>
976
+ {isUser ? (
977
+ <Text>{text}</Text>
978
+ ) : (
979
+ <Ansi>{applyMarkdown(text, theme)}</Ansi>
980
+ )}
981
+ </Box>
982
+ );
983
+ })}
911
984
  </Box>
912
985
  </Box>
913
986
  );
package/src/utils/auth.ts CHANGED
@@ -88,7 +88,7 @@ const DEFAULT_API_KEY_HELPER_TTL = 5 * 60 * 1000
88
88
  * who runs `claude` in their terminal with an API key sees every CCD session
89
89
  * also use that key — and fail if it's stale/wrong-org.
90
90
  */
91
- function isManagedOAuthContext(): boolean {
91
+ export function isManagedOAuthContext(): boolean {
92
92
  return (
93
93
  isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) ||
94
94
  process.env.CLAUDE_CODE_ENTRYPOINT === 'claude-desktop'