rebrand better-clawd and ship initial npm-ready release
This commit is contained in:
@@ -241,7 +241,7 @@ export function ConsoleOAuthFlow({
|
||||
state: 'success'
|
||||
});
|
||||
void sendNotification({
|
||||
message: 'Claude Code login successful',
|
||||
message: 'Better-Clawd login successful',
|
||||
notificationType: 'auth_success'
|
||||
}, terminal);
|
||||
}
|
||||
@@ -364,7 +364,7 @@ function OAuthStatusMessage(t0) {
|
||||
switch (oauthStatus.state) {
|
||||
case "idle":
|
||||
{
|
||||
const t1 = startingMessage ? startingMessage : "Claude Code can be used with your Claude subscription or billed based on API usage through your Console account.";
|
||||
const t1 = startingMessage ? startingMessage : "Better-Clawd can be used with your Anthropic subscription, OpenAI/Codex, OpenRouter, or billed API usage depending on the provider you choose.";
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = <Text bold={true}>{t1}</Text>;
|
||||
@@ -460,7 +460,7 @@ function OAuthStatusMessage(t0) {
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text>Claude Code supports Amazon Bedrock, Microsoft Foundry, and Vertex AI. Set the required environment variables, then restart Claude Code.</Text>;
|
||||
t2 = <Text>Better-Clawd supports Amazon Bedrock, Microsoft Foundry, and Vertex AI. Set the required environment variables, then restart Better-Clawd.</Text>;
|
||||
t3 = <Text>If you are part of an enterprise organization, contact your administrator for setup instructions.</Text>;
|
||||
$[13] = t2;
|
||||
$[14] = t3;
|
||||
@@ -554,7 +554,7 @@ function OAuthStatusMessage(t0) {
|
||||
{
|
||||
let t1;
|
||||
if ($[37] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box flexDirection="column" gap={1}><Box><Spinner /><Text>Creating API key for Claude Code…</Text></Box></Box>;
|
||||
t1 = <Box flexDirection="column" gap={1}><Box><Spinner /><Text>Creating API key for Better-Clawd…</Text></Box></Box>;
|
||||
$[37] = t1;
|
||||
} else {
|
||||
t1 = $[37];
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import axios from 'axios';
|
||||
import { readFile, stat } from 'fs/promises';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@@ -13,14 +12,12 @@ import { useKeybinding } from '../keybindings/useKeybinding.js';
|
||||
import { queryHaiku } from '../services/api/claude.js';
|
||||
import { startsWithApiErrorPrefix } from '../services/api/errors.js';
|
||||
import type { Message } from '../types/message.js';
|
||||
import { checkAndRefreshOAuthTokenIfNeeded } from '../utils/auth.js';
|
||||
import { openBrowser } from '../utils/browser.js';
|
||||
import { PRODUCT_ISSUES_URL, PRODUCT_NAME } from '../constants/product.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { env } from '../utils/env.js';
|
||||
import { type GitRepoState, getGitState, getIsGit } from '../utils/git.js';
|
||||
import { getAuthHeaders, getUserAgent } from '../utils/http.js';
|
||||
import { getInMemoryErrors, logError } from '../utils/log.js';
|
||||
import { isEssentialTrafficOnly } from '../utils/privacyLevel.js';
|
||||
import { extractTeammateTranscriptsFromTasks, getTranscriptPath, loadAllSubagentTranscriptsFromDisk, MAX_TRANSCRIPT_READ_BYTES } from '../utils/sessionStorage.js';
|
||||
import { jsonStringify } from '../utils/slowOperations.js';
|
||||
import { asSystemPrompt } from '../utils/systemPromptType.js';
|
||||
@@ -32,7 +29,7 @@ import TextInput from './TextInput.js';
|
||||
|
||||
// This value was determined experimentally by testing the URL length limit
|
||||
const GITHUB_URL_LIMIT = 7250;
|
||||
const GITHUB_ISSUES_REPO_URL = "external" === 'ant' ? 'https://github.com/anthropics/claude-cli-internal/issues' : 'https://github.com/anthropics/claude-code/issues';
|
||||
const GITHUB_ISSUES_REPO_URL = PRODUCT_ISSUES_URL;
|
||||
type Props = {
|
||||
abortSignal: AbortSignal;
|
||||
messages: Message[];
|
||||
@@ -231,7 +228,6 @@ export function Feedback({
|
||||
feedback_id: result.feedbackId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
last_assistant_message_id: lastAssistantMessageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
// 1P-only: freeform text approved for BQ. Join on feedback_id.
|
||||
logEventTo1P('tengu_bug_report_description', {
|
||||
feedback_id: result.feedbackId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
description: redactSensitiveInfo(description) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
@@ -239,11 +235,7 @@ export function Feedback({
|
||||
}
|
||||
setStep('done');
|
||||
} else {
|
||||
if (result.isZdrOrg) {
|
||||
setError('Feedback collection is not available for organizations with custom data retention policies.');
|
||||
} else {
|
||||
setError('Could not submit feedback. Please try again later.');
|
||||
}
|
||||
setError('Could not prepare the issue draft. Please try again later.');
|
||||
// Stay on userInput step so user can retry with their content preserved
|
||||
setStep('userInput');
|
||||
}
|
||||
@@ -334,7 +326,7 @@ export function Feedback({
|
||||
</Box>}
|
||||
|
||||
{step === 'consent' && <Box flexDirection="column">
|
||||
<Text>This report will include:</Text>
|
||||
<Text>This issue draft will include:</Text>
|
||||
<Box marginLeft={2} flexDirection="column">
|
||||
<Text>
|
||||
- Your feedback / bug description:{' '}
|
||||
@@ -360,24 +352,24 @@ export function Feedback({
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text wrap="wrap" dimColor>
|
||||
We will use your feedback to debug related issues or to improve{' '}
|
||||
Claude Code's functionality (eg. to reduce the risk of bugs
|
||||
occurring in the future).
|
||||
Better-Clawd no longer uploads bug reports to an upstream service.
|
||||
Press Enter to prepare a GitHub issue draft with the details shown
|
||||
above.
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text>
|
||||
Press <Text bold>Enter</Text> to confirm and submit.
|
||||
Press <Text bold>Enter</Text> to continue.
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>}
|
||||
|
||||
{step === 'submitting' && <Box flexDirection="row" gap={1}>
|
||||
<Text>Submitting report…</Text>
|
||||
<Text>Preparing issue draft…</Text>
|
||||
</Box>}
|
||||
|
||||
{step === 'done' && <Box flexDirection="column">
|
||||
{error ? <Text color="error">{error}</Text> : <Text color="success">Thank you for your report!</Text>}
|
||||
{error ? <Text color="error">{error}</Text> : <Text color="success">Issue draft ready.</Text>}
|
||||
{feedbackId && <Text dimColor>Feedback ID: {feedbackId}</Text>}
|
||||
<Box marginTop={1}>
|
||||
<Text>Press </Text>
|
||||
@@ -396,7 +388,7 @@ export function createGitHubIssueUrl(feedbackId: string, title: string, descript
|
||||
}>): string {
|
||||
const sanitizedTitle = redactSensitiveInfo(title);
|
||||
const sanitizedDescription = redactSensitiveInfo(description);
|
||||
const bodyPrefix = `**Bug Description**\n${sanitizedDescription}\n\n` + `**Environment Info**\n` + `- Platform: ${env.platform}\n` + `- Terminal: ${env.terminal}\n` + `- Version: ${MACRO.VERSION || 'unknown'}\n` + `- Feedback ID: ${feedbackId}\n` + `\n**Errors**\n\`\`\`json\n`;
|
||||
const bodyPrefix = `**Bug Description**\n${sanitizedDescription}\n\n` + `**Environment Info**\n` + `- Product: ${PRODUCT_NAME}\n` + `- Platform: ${env.platform}\n` + `- Terminal: ${env.terminal}\n` + `- Version: ${MACRO.VERSION || 'unknown'}\n` + `- Feedback ID: ${feedbackId}\n` + `\n**Errors**\n\`\`\`json\n`;
|
||||
const errorSuffix = `\n\`\`\`\n`;
|
||||
const errorsJson = jsonStringify(errors);
|
||||
const baseUrl = `${GITHUB_ISSUES_REPO_URL}/new?title=${encodeURIComponent(sanitizedTitle)}&labels=user-reported,bug&body=`;
|
||||
@@ -446,8 +438,8 @@ export function createGitHubIssueUrl(feedbackId: string, title: string, descript
|
||||
}
|
||||
async function generateTitle(description: string, abortSignal: AbortSignal): Promise<string> {
|
||||
try {
|
||||
const response = await queryHaiku({
|
||||
systemPrompt: asSystemPrompt(['Generate a concise, technical issue title (max 80 chars) for a public GitHub issue based on this bug report for Claude Code.', 'Claude Code is an agentic coding CLI based on the Anthropic API.', 'The title should:', '- Include the type of issue [Bug] or [Feature Request] as the first thing in the title', '- Be concise, specific and descriptive of the actual problem', '- Use technical terminology appropriate for a software issue', '- For error messages, extract the key error (e.g., "Missing Tool Result Block" rather than the full message)', '- Be direct and clear for developers to understand the problem', '- If you cannot determine a clear issue, use "Bug Report: [brief description]"', '- Any LLM API errors are from the Anthropic API, not from any other model provider', 'Your response will be directly used as the title of the Github issue, and as such should not contain any other commentary or explaination', 'Examples of good titles include: "[Bug] Auto-Compact triggers to soon", "[Bug] Anthropic API Error: Missing Tool Result Block", "[Bug] Error: Invalid Model Name for Opus"']),
|
||||
const response = await queryHaiku({
|
||||
systemPrompt: asSystemPrompt([`Generate a concise, technical issue title (max 80 chars) for a public GitHub issue based on this bug report for ${PRODUCT_NAME}.`, `${PRODUCT_NAME} is an agentic coding CLI with multiple model providers.`, 'The title should:', '- Include the type of issue [Bug] or [Feature Request] as the first thing in the title', '- Be concise, specific and descriptive of the actual problem', '- Use technical terminology appropriate for a software issue', '- For error messages, extract the key error (e.g., "Missing Tool Result Block" rather than the full message)', '- Be direct and clear for developers to understand the problem', '- If you cannot determine a clear issue, use "Bug Report: [brief description]"', 'Your response will be directly used as the title of the GitHub issue, and should not contain any extra commentary', 'Examples of good titles include: "[Bug] Auto-compact triggers too soon", "[Bug] Missing tool result block after retry", "[Bug] Invalid model name for GPT-5.4"']),
|
||||
userPrompt: description,
|
||||
signal: abortSignal,
|
||||
options: {
|
||||
@@ -520,69 +512,14 @@ async function submitFeedback(data: FeedbackData, signal?: AbortSignal): Promise
|
||||
feedbackId?: string;
|
||||
isZdrOrg?: boolean;
|
||||
}> {
|
||||
if (isEssentialTrafficOnly()) {
|
||||
return {
|
||||
success: false
|
||||
};
|
||||
}
|
||||
try {
|
||||
// Ensure OAuth token is fresh before getting auth headers
|
||||
// This prevents 401 errors from stale cached tokens
|
||||
await checkAndRefreshOAuthTokenIfNeeded();
|
||||
const authResult = getAuthHeaders();
|
||||
if (authResult.error) {
|
||||
return {
|
||||
success: false
|
||||
};
|
||||
}
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': getUserAgent(),
|
||||
...authResult.headers
|
||||
};
|
||||
const response = await axios.post('https://api.anthropic.com/api/claude_cli_feedback', {
|
||||
content: jsonStringify(data)
|
||||
}, {
|
||||
headers,
|
||||
timeout: 30000,
|
||||
// 30 second timeout to prevent hanging
|
||||
signal
|
||||
});
|
||||
if (response.status === 200) {
|
||||
const result = response.data;
|
||||
if (result?.feedback_id) {
|
||||
return {
|
||||
success: true,
|
||||
feedbackId: result.feedback_id
|
||||
};
|
||||
}
|
||||
sanitizeAndLogError(new Error('Failed to submit feedback: request did not return feedback_id'));
|
||||
return {
|
||||
success: false
|
||||
};
|
||||
}
|
||||
sanitizeAndLogError(new Error('Failed to submit feedback:' + response.status));
|
||||
void signal;
|
||||
void data;
|
||||
return {
|
||||
success: false
|
||||
success: true,
|
||||
feedbackId: `better-clawd-${Date.now().toString(36)}`
|
||||
};
|
||||
} catch (err) {
|
||||
// Handle cancellation/abort - don't log as error
|
||||
if (axios.isCancel(err)) {
|
||||
return {
|
||||
success: false
|
||||
};
|
||||
}
|
||||
if (axios.isAxiosError(err) && err.response?.status === 403) {
|
||||
const errorData = err.response.data;
|
||||
if (errorData?.error?.type === 'permission_error' && errorData?.error?.message?.includes('Custom data retention settings')) {
|
||||
sanitizeAndLogError(new Error('Cannot submit feedback because custom data retention settings are enabled'));
|
||||
return {
|
||||
success: false,
|
||||
isZdrOrg: true
|
||||
};
|
||||
}
|
||||
}
|
||||
// Use our safe error logging function to avoid leaking API keys
|
||||
sanitizeAndLogError(err);
|
||||
return {
|
||||
success: false
|
||||
|
||||
@@ -138,7 +138,7 @@ export function HelpV2(t0) {
|
||||
const t5 = insideModal ? undefined : maxHeight;
|
||||
let t6;
|
||||
if ($[31] !== tabs) {
|
||||
t6 = <Tabs title={false ? "/help" : `Claude Code v${MACRO.VERSION}`} color="professionalBlue" defaultTab="general">{tabs}</Tabs>;
|
||||
t6 = <Tabs title={false ? "/help" : `Better-Clawd v${MACRO.VERSION}`} color="professionalBlue" defaultTab="general">{tabs}</Tabs>;
|
||||
$[31] = tabs;
|
||||
$[32] = t6;
|
||||
} else {
|
||||
|
||||
@@ -88,7 +88,7 @@ export function CondensedLogo() {
|
||||
}
|
||||
let t5;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text bold={true}>Claude Code</Text>;
|
||||
t5 = <Text bold={true}>Better-Clawd</Text>;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
t5 = $[8];
|
||||
|
||||
@@ -248,8 +248,8 @@ export function LogoV2() {
|
||||
}
|
||||
const layoutMode = getLayoutMode(columns);
|
||||
const userTheme = resolveThemeSetting(getGlobalConfig().theme);
|
||||
const borderTitle = ` ${color("claude", userTheme)("Claude Code")} ${color("inactive", userTheme)(`v${version}`)} `;
|
||||
const compactBorderTitle = color("claude", userTheme)(" Claude Code ");
|
||||
const borderTitle = ` ${color("claude", userTheme)("Better-Clawd")} ${color("inactive", userTheme)(`v${version}`)} `;
|
||||
const compactBorderTitle = color("claude", userTheme)(" Better-Clawd ");
|
||||
if (layoutMode === "compact") {
|
||||
let welcomeMessage = formatWelcomeMessage(username);
|
||||
if (stringWidth(welcomeMessage) > columns - 4) {
|
||||
|
||||
@@ -9,7 +9,7 @@ export function WelcomeV2() {
|
||||
if (env.terminal === "Apple_Terminal") {
|
||||
let t0;
|
||||
if ($[0] !== theme) {
|
||||
t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="Welcome to Claude Code" />;
|
||||
t0 = <AppleTerminalWelcomeV2 theme={theme} welcomeMessage="Welcome to Better-Clawd" />;
|
||||
$[0] = theme;
|
||||
$[1] = t0;
|
||||
} else {
|
||||
@@ -28,7 +28,7 @@ export function WelcomeV2() {
|
||||
let t7;
|
||||
let t8;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text><Text color="claude">{"Welcome to Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
|
||||
t0 = <Text><Text color="claude">{"Welcome to Better-Clawd"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
|
||||
t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>;
|
||||
t2 = <Text>{" "}</Text>;
|
||||
t3 = <Text>{" "}</Text>;
|
||||
@@ -113,7 +113,7 @@ export function WelcomeV2() {
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text><Text color="claude">{"Welcome to Claude Code"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
|
||||
t0 = <Text><Text color="claude">{"Welcome to Better-Clawd"} </Text><Text dimColor={true}>v{MACRO.VERSION} </Text></Text>;
|
||||
t1 = <Text>{"\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026\u2026"}</Text>;
|
||||
t2 = <Text>{" "}</Text>;
|
||||
t3 = <Text>{" * \u2588\u2588\u2588\u2588\u2588\u2593\u2593\u2591 "}</Text>;
|
||||
|
||||
@@ -282,10 +282,11 @@ const MAX_MESSAGES_TO_SHOW_IN_TRANSCRIPT_MODE = 30;
|
||||
// of fibers, and per-frame write costs that push the process into a GC
|
||||
// death spiral (observed: 59 GB RSS, 14k mmap/munmap/sec). Content dropped
|
||||
// from this slice has already been printed to terminal scrollback — users
|
||||
// can still scroll up natively. VirtualMessageList (the default ant path)
|
||||
// bypasses this cap entirely. Headless one-shot renders (e.g. /export)
|
||||
// pass disableRenderCap to opt out — they have no scrollback and the
|
||||
// memory concern doesn't apply to renderToString.
|
||||
// can still scroll up natively. Better-Clawd's default external path is the
|
||||
// main-screen renderer (no VirtualMessageList), so keep the live window
|
||||
// smaller to reduce typing-time diff/write work in long sessions. Headless
|
||||
// one-shot renders (e.g. /export) pass disableRenderCap to opt out — they
|
||||
// have no scrollback and the memory concern doesn't apply to renderToString.
|
||||
//
|
||||
// The slice boundary is tracked as a UUID anchor, not a count-derived
|
||||
// index. Count-based slicing (slice(-200)) drops one message from the
|
||||
@@ -302,9 +303,9 @@ const MAX_MESSAGES_TO_SHOW_IN_TRANSCRIPT_MODE = 30;
|
||||
// as tool results stream in, changing which summary is first. When the
|
||||
// uuid vanishes, falling back to the stored index (clamped) keeps the
|
||||
// slice roughly where it was instead of resetting to 0 — which would
|
||||
// jump from ~200 rendered messages to the full history, orphaning
|
||||
// jump from ~120 rendered messages to the full history, orphaning
|
||||
// in-progress badge snapshots in scrollback.
|
||||
const MAX_MESSAGES_WITHOUT_VIRTUALIZATION = 200;
|
||||
const MAX_MESSAGES_WITHOUT_VIRTUALIZATION = 120;
|
||||
const MESSAGE_CAP_STEP = 50;
|
||||
export type SliceAnchor = {
|
||||
uuid: string;
|
||||
@@ -500,7 +501,7 @@ const MessagesImpl = ({
|
||||
// CC-724: drop attachment messages that AttachmentMessage renders as
|
||||
// null (hook_success, hook_additional_context, hook_cancelled, etc.)
|
||||
// BEFORE counting/slicing so they don't inflate the "N messages"
|
||||
// count in ctrl-o or consume slots in the 200-message render cap.
|
||||
// count in ctrl-o or consume slots in the non-virtualized render cap.
|
||||
.filter(msg_3 => !isNullRenderingAttachment(msg_3)).filter(_ => shouldShowUserMessage(_, isTranscriptMode)), syntheticStreamingToolUseMessages);
|
||||
// Three-tier filtering. Transcript mode (ctrl+o screen) is truly unfiltered.
|
||||
// Brief-only: SendUserMessage + user input only. Default: drop redundant
|
||||
@@ -730,7 +731,8 @@ function expandKey(msg: RenderableMessage): string {
|
||||
// Default React.memo does shallow comparison which fails when:
|
||||
// 1. onOpenRateLimitOptions callback is recreated (doesn't affect render output)
|
||||
// 2. streamingToolUses array is recreated on every delta, but only contentBlock matters for rendering
|
||||
// 3. streamingThinking changes on every delta - we DO want to re-render for this
|
||||
// 3. commands/tool queues get rebuilt with the same semantic contents
|
||||
// 4. streamingThinking changes on every delta - we DO want to re-render for this
|
||||
function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
||||
if (a.size !== b.size) return false;
|
||||
for (const item of a) {
|
||||
@@ -738,6 +740,12 @@ function setsEqual<T>(a: Set<T>, b: Set<T>): boolean {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function commandArraysEqual(a: Command[], b: Command[]): boolean {
|
||||
return a.length === b.length && a.every((command, i) => command.name === b[i]?.name);
|
||||
}
|
||||
function toolUseConfirmQueuesEqual(a: ToolUseConfirm[], b: ToolUseConfirm[]): boolean {
|
||||
return a.length === b.length && a.every((item, i) => item.toolUseID === b[i]?.toolUseID);
|
||||
}
|
||||
export const Messages = React.memo(MessagesImpl, (prev, next) => {
|
||||
const keys = Object.keys(prev) as (keyof typeof prev)[];
|
||||
for (const key of keys) {
|
||||
@@ -769,6 +777,16 @@ export const Messages = React.memo(MessagesImpl, (prev, next) => {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (key === 'commands') {
|
||||
if (commandArraysEqual(prev.commands, next.commands)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (key === 'toolUseConfirmQueue') {
|
||||
if (toolUseConfirmQueuesEqual(prev.toolUseConfirmQueue, next.toolUseConfirmQueue)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// streamingThinking changes frequently - always re-render when it changes
|
||||
// (no special handling needed, default behavior is correct)
|
||||
return false;
|
||||
|
||||
215
src/components/OpenAILoginFlow.tsx
Normal file
215
src/components/OpenAILoginFlow.tsx
Normal file
@@ -0,0 +1,215 @@
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { Box, Text } from '../ink.js'
|
||||
import {
|
||||
importOpenAIAuthFromCodexCache,
|
||||
runCodexLogin,
|
||||
saveOpenAIApiKey,
|
||||
saveOpenAIAuthTokens,
|
||||
} from '../utils/auth.js'
|
||||
import { Select } from './CustomSelect/select.js'
|
||||
import TextInput from './TextInput.js'
|
||||
import { Spinner } from './Spinner.js'
|
||||
|
||||
type OpenAILoginFlowProps = {
|
||||
onDone: () => void
|
||||
startingMessage?: string
|
||||
}
|
||||
|
||||
type LoginMode = 'menu' | 'api_key' | 'access_token'
|
||||
|
||||
export function OpenAILoginFlow({
|
||||
onDone,
|
||||
startingMessage,
|
||||
}: OpenAILoginFlowProps): React.ReactNode {
|
||||
const [mode, setMode] = useState<LoginMode>('menu')
|
||||
const [isBusy, setIsBusy] = useState(false)
|
||||
const [status, setStatus] = useState<string | null>(null)
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Use cached Codex login{' '}
|
||||
<Text dimColor={true}>Import `~/.codex/auth.json`</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'import_cache',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Sign in with Codex in browser{' '}
|
||||
<Text dimColor={true}>Runs `codex login`</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'browser_login',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Sign in with device code{' '}
|
||||
<Text dimColor={true}>Runs `codex login --device-auth`</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'device_login',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Paste OpenAI API key{' '}
|
||||
<Text dimColor={true}>Usage-based billing</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'api_key',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Paste Codex access token{' '}
|
||||
<Text dimColor={true}>Manual fallback for ChatGPT auth</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'access_token',
|
||||
},
|
||||
] as const
|
||||
|
||||
async function handleMenuSelection(value: string): Promise<void> {
|
||||
setStatus(null)
|
||||
|
||||
if (value === 'api_key') {
|
||||
setInputValue('')
|
||||
setCursorOffset(0)
|
||||
setMode('api_key')
|
||||
return
|
||||
}
|
||||
|
||||
if (value === 'access_token') {
|
||||
setInputValue('')
|
||||
setCursorOffset(0)
|
||||
setMode('access_token')
|
||||
return
|
||||
}
|
||||
|
||||
setIsBusy(true)
|
||||
try {
|
||||
if (value === 'import_cache') {
|
||||
await importOpenAIAuthFromCodexCache()
|
||||
} else if (value === 'browser_login') {
|
||||
await runCodexLogin()
|
||||
} else if (value === 'device_login') {
|
||||
await runCodexLogin({ deviceAuth: true })
|
||||
}
|
||||
onDone()
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
setIsBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(value: string): Promise<void> {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) {
|
||||
return
|
||||
}
|
||||
|
||||
setIsBusy(true)
|
||||
setStatus(null)
|
||||
try {
|
||||
if (mode === 'api_key') {
|
||||
await saveOpenAIApiKey(trimmed)
|
||||
} else {
|
||||
saveOpenAIAuthTokens({ accessToken: trimmed })
|
||||
}
|
||||
onDone()
|
||||
} catch (error) {
|
||||
setStatus(error instanceof Error ? error.message : String(error))
|
||||
} finally {
|
||||
setIsBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (isBusy) {
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Configuring OpenAI login for Better-Clawd…</Text>
|
||||
</Box>
|
||||
<Text dimColor={true}>
|
||||
ChatGPT login uses Codex's shared auth cache and API-key login uses
|
||||
your OpenAI Platform key.
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (mode === 'api_key' || mode === 'access_token') {
|
||||
const prompt =
|
||||
mode === 'api_key'
|
||||
? 'Paste your OpenAI API key:'
|
||||
: 'Paste your Codex access token:'
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
{mode === 'api_key'
|
||||
? 'OpenAI API keys use standard platform billing.'
|
||||
: 'Codex access tokens are cached by Codex after ChatGPT login.'}
|
||||
</Text>
|
||||
<Box>
|
||||
<Text>{prompt}</Text>
|
||||
<TextInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleSubmit}
|
||||
onExit={() => {
|
||||
setMode('menu')
|
||||
setInputValue('')
|
||||
setCursorOffset(0)
|
||||
}}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
columns={72}
|
||||
mask="*"
|
||||
/>
|
||||
</Box>
|
||||
{status ? <Text color="error">{status}</Text> : null}
|
||||
<Text dimColor={true}>
|
||||
Press <Text bold={true}>Enter</Text> to save, or <Text bold={true}>Esc</Text>{' '}
|
||||
to cancel.
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
{startingMessage ??
|
||||
'Better-Clawd can use OpenAI via ChatGPT-managed Codex login or with a standard OpenAI API key.'}
|
||||
</Text>
|
||||
<Text dimColor={true}>
|
||||
Codex shares cached credentials between the CLI and IDE. If browser login
|
||||
is unavailable, device-auth and auth-cache import are supported too.
|
||||
</Text>
|
||||
{status ? <Text color="error">{status}</Text> : null}
|
||||
<Box>
|
||||
<Select
|
||||
options={menuOptions}
|
||||
onChange={value => {
|
||||
void handleMenuSelection(value)
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
@@ -73,7 +73,7 @@ export function PackageManagerAutoUpdater(t0) {
|
||||
if (!updateAvailable) {
|
||||
return null;
|
||||
}
|
||||
const updateCommand = packageManager === "homebrew" ? "brew upgrade claude-code" : packageManager === "winget" ? "winget upgrade Anthropic.ClaudeCode" : packageManager === "apk" ? "apk upgrade claude-code" : "your package manager update command";
|
||||
const updateCommand = packageManager === "homebrew" ? "brew upgrade <better-clawd-package>" : packageManager === "winget" ? "winget upgrade <better-clawd-package>" : packageManager === "apk" ? "apk upgrade <better-clawd-package>" : "your package manager update command";
|
||||
let t4;
|
||||
if ($[3] !== verbose) {
|
||||
t4 = verbose && <Text dimColor={true} wrap="truncate">currentVersion: {MACRO.VERSION}</Text>;
|
||||
|
||||
@@ -464,6 +464,8 @@ function PromptInput({
|
||||
// immediately; the useEffect below clears the raw state so it doesn't
|
||||
// resurrect when the same pill reappears (new task starts → focus stolen).
|
||||
const rawFooterSelection = useAppState(s => s.footerSelection);
|
||||
const footerSelectionRef = useRef<FooterItem | null>(null);
|
||||
footerSelectionRef.current = rawFooterSelection;
|
||||
const footerItemSelected = rawFooterSelection && footerItems.includes(rawFooterSelection) ? rawFooterSelection : null;
|
||||
useEffect(() => {
|
||||
if (rawFooterSelection && !footerItemSelected) {
|
||||
@@ -473,6 +475,15 @@ function PromptInput({
|
||||
});
|
||||
}
|
||||
}, [rawFooterSelection, footerItemSelected, setAppState]);
|
||||
const clearFooterSelectionIfNeeded = useCallback(() => {
|
||||
if (footerSelectionRef.current === null) {
|
||||
return;
|
||||
}
|
||||
setAppState(prev => prev.footerSelection === null ? prev : {
|
||||
...prev,
|
||||
footerSelection: null
|
||||
});
|
||||
}, [setAppState]);
|
||||
const tasksSelected = footerItemSelected === 'tasks';
|
||||
const tmuxSelected = footerItemSelected === 'tmux';
|
||||
const bagelSelected = footerItemSelected === 'bagel';
|
||||
@@ -892,13 +903,11 @@ function PromptInput({
|
||||
pushToBuffer(input, cursorOffset, pastedContents);
|
||||
}
|
||||
|
||||
// Deselect footer items when user types
|
||||
setAppState(prev => prev.footerSelection === null ? prev : {
|
||||
...prev,
|
||||
footerSelection: null
|
||||
});
|
||||
// Deselect footer items when user types, but skip the store write when
|
||||
// nothing is selected so routine keystrokes stay inside the input subtree.
|
||||
clearFooterSelectionIfNeeded();
|
||||
trackAndSetInput(processedValue);
|
||||
}, [trackAndSetInput, onModeChange, input, cursorOffset, pushToBuffer, pastedContents, dismissStashHint, setAppState]);
|
||||
}, [trackAndSetInput, onModeChange, input, cursorOffset, pushToBuffer, pastedContents, dismissStashHint, clearFooterSelectionIfNeeded]);
|
||||
const {
|
||||
resetHistory,
|
||||
onHistoryUp,
|
||||
|
||||
@@ -45,9 +45,9 @@ export function RemoteCallout({
|
||||
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
||||
<Box marginBottom={1} flexDirection="column">
|
||||
<Text>
|
||||
Remote Control lets you access this CLI session from the web
|
||||
(claude.ai/code) or the Claude app, so you can pick up where you
|
||||
left off on any device.
|
||||
Remote Control lets you access this CLI session from a compatible
|
||||
remote bridge so you can pick up where you left off on another
|
||||
device.
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
<Text>
|
||||
|
||||
@@ -18,7 +18,8 @@ import { Dialog } from './design-system/Dialog.js';
|
||||
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
||||
import { LoadingState } from './design-system/LoadingState.js';
|
||||
const DIALOG_TITLE = 'Select Remote Environment';
|
||||
const SETUP_HINT = `Configure environments at: https://claude.ai/code`;
|
||||
const SETUP_HINT =
|
||||
'Remote environments are not configured by Better-Clawd. Use your existing remote session provider setup.';
|
||||
type Props = {
|
||||
onDone: (message?: string) => void;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user