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 +1 -1
- package/src/components/Onboarding.tsx +6 -2
- package/src/manager/CliMenuManager.tsx +125 -52
- package/src/utils/auth.ts +1 -1
package/package.json
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
//
|
|
143
|
-
type
|
|
151
|
+
// 消息条目(与后端 MessageEntry 对齐)
|
|
152
|
+
type MessageEntry = {
|
|
144
153
|
id: string;
|
|
145
|
-
|
|
146
|
-
content: string
|
|
147
|
-
|
|
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<
|
|
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
|
-
|
|
384
|
-
|
|
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
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
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={
|
|
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
|
-
<
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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={
|
|
905
|
-
<Text>
|
|
906
|
-
<
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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'
|