mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
style(B1-2): 格式化 commands (79 files)
纯格式化:移除分号、React Compiler import、import 多行展开。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,125 +1,154 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import figures from 'figures';
|
||||
import React, { useEffect } from 'react';
|
||||
import { getAdditionalDirectoriesForClaudeMd, setAdditionalDirectoriesForClaudeMd } from '../../bootstrap/state.js';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
||||
import { AddWorkspaceDirectory } from '../../components/permissions/rules/AddWorkspaceDirectory.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { applyPermissionUpdate, persistPermissionUpdate } from '../../utils/permissions/PermissionUpdate.js';
|
||||
import type { PermissionUpdateDestination } from '../../utils/permissions/PermissionUpdateSchema.js';
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { addDirHelpMessage, validateDirectoryForWorkspace } from './validation.js';
|
||||
function AddDirError(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
message,
|
||||
args,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = () => {
|
||||
const timer = setTimeout(onDone, 0);
|
||||
return () => clearTimeout(timer);
|
||||
};
|
||||
t2 = [onDone];
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== args) {
|
||||
t3 = <Text dimColor={true}>{figures.pointer} /add-dir {args}</Text>;
|
||||
$[3] = args;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== message) {
|
||||
t4 = <MessageResponse><Text>{message}</Text></MessageResponse>;
|
||||
$[5] = message;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t3 || $[8] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t3}{t4}</Box>;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
import chalk from 'chalk'
|
||||
import figures from 'figures'
|
||||
import React, { useEffect } from 'react'
|
||||
import {
|
||||
getAdditionalDirectoriesForClaudeMd,
|
||||
setAdditionalDirectoriesForClaudeMd,
|
||||
} from '../../bootstrap/state.js'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { AddWorkspaceDirectory } from '../../components/permissions/rules/AddWorkspaceDirectory.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
applyPermissionUpdate,
|
||||
persistPermissionUpdate,
|
||||
} from '../../utils/permissions/PermissionUpdate.js'
|
||||
import type { PermissionUpdateDestination } from '../../utils/permissions/PermissionUpdateSchema.js'
|
||||
import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import {
|
||||
addDirHelpMessage,
|
||||
validateDirectoryForWorkspace,
|
||||
} from './validation.js'
|
||||
|
||||
function AddDirError({
|
||||
message,
|
||||
args,
|
||||
onDone,
|
||||
}: {
|
||||
message: string
|
||||
args: string
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
useEffect(() => {
|
||||
// We need to defer calling onDone to avoid the "return null" bug where
|
||||
// the component unmounts before React can render the error message.
|
||||
// Using setTimeout ensures the error displays before the command exits.
|
||||
const timer = setTimeout(onDone, 0)
|
||||
return () => clearTimeout(timer)
|
||||
}, [onDone])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>
|
||||
{figures.pointer} /add-dir {args}
|
||||
</Text>
|
||||
<MessageResponse>
|
||||
<Text>{message}</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args?: string): Promise<React.ReactNode> {
|
||||
const directoryPath = (args ?? '').trim();
|
||||
const appState = context.getAppState();
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const directoryPath = (args ?? '').trim()
|
||||
const appState = context.getAppState()
|
||||
|
||||
// Helper to handle adding a directory (shared by both with-path and no-path cases)
|
||||
const handleAddDirectory = async (path: string, remember = false) => {
|
||||
const destination: PermissionUpdateDestination = remember ? 'localSettings' : 'session';
|
||||
const destination: PermissionUpdateDestination = remember
|
||||
? 'localSettings'
|
||||
: 'session'
|
||||
|
||||
const permissionUpdate = {
|
||||
type: 'addDirectories' as const,
|
||||
directories: [path],
|
||||
destination
|
||||
};
|
||||
destination,
|
||||
}
|
||||
|
||||
// Apply to session context
|
||||
const latestAppState = context.getAppState();
|
||||
const updatedContext = applyPermissionUpdate(latestAppState.toolPermissionContext, permissionUpdate);
|
||||
const latestAppState = context.getAppState()
|
||||
const updatedContext = applyPermissionUpdate(
|
||||
latestAppState.toolPermissionContext,
|
||||
permissionUpdate,
|
||||
)
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
toolPermissionContext: updatedContext
|
||||
}));
|
||||
toolPermissionContext: updatedContext,
|
||||
}))
|
||||
|
||||
// Update sandbox config so Bash commands can access the new directory.
|
||||
// Bootstrap state is the source of truth for session-only dirs; persisted
|
||||
// dirs are picked up via the settings subscription, but we refresh
|
||||
// eagerly here to avoid a race when the user acts immediately.
|
||||
const currentDirs = getAdditionalDirectoriesForClaudeMd();
|
||||
const currentDirs = getAdditionalDirectoriesForClaudeMd()
|
||||
if (!currentDirs.includes(path)) {
|
||||
setAdditionalDirectoriesForClaudeMd([...currentDirs, path]);
|
||||
setAdditionalDirectoriesForClaudeMd([...currentDirs, path])
|
||||
}
|
||||
SandboxManager.refreshConfig();
|
||||
let message: string;
|
||||
SandboxManager.refreshConfig()
|
||||
|
||||
let message: string
|
||||
|
||||
if (remember) {
|
||||
try {
|
||||
persistPermissionUpdate(permissionUpdate);
|
||||
message = `Added ${chalk.bold(path)} as a working directory and saved to local settings`;
|
||||
persistPermissionUpdate(permissionUpdate)
|
||||
message = `Added ${chalk.bold(path)} as a working directory and saved to local settings`
|
||||
} catch (error) {
|
||||
message = `Added ${chalk.bold(path)} as a working directory. Failed to save to local settings: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
message = `Added ${chalk.bold(path)} as a working directory. Failed to save to local settings: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
}
|
||||
} else {
|
||||
message = `Added ${chalk.bold(path)} as a working directory for this session`;
|
||||
message = `Added ${chalk.bold(path)} as a working directory for this session`
|
||||
}
|
||||
const messageWithHint = `${message} ${chalk.dim('· /permissions to manage')}`;
|
||||
onDone(messageWithHint);
|
||||
};
|
||||
|
||||
const messageWithHint = `${message} ${chalk.dim('· /permissions to manage')}`
|
||||
onDone(messageWithHint)
|
||||
}
|
||||
|
||||
// When no path is provided, show AddWorkspaceDirectory input form directly
|
||||
// and return to REPL after confirmation
|
||||
if (!directoryPath) {
|
||||
return <AddWorkspaceDirectory permissionContext={appState.toolPermissionContext} onAddDirectory={handleAddDirectory} onCancel={() => {
|
||||
onDone('Did not add a working directory.');
|
||||
}} />;
|
||||
return (
|
||||
<AddWorkspaceDirectory
|
||||
permissionContext={appState.toolPermissionContext}
|
||||
onAddDirectory={handleAddDirectory}
|
||||
onCancel={() => {
|
||||
onDone('Did not add a working directory.')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
const result = await validateDirectoryForWorkspace(directoryPath, appState.toolPermissionContext);
|
||||
|
||||
const result = await validateDirectoryForWorkspace(
|
||||
directoryPath,
|
||||
appState.toolPermissionContext,
|
||||
)
|
||||
|
||||
if (result.resultType !== 'success') {
|
||||
const message = addDirHelpMessage(result);
|
||||
return <AddDirError message={message} args={args ?? ''} onDone={() => onDone(message)} />;
|
||||
const message = addDirHelpMessage(result)
|
||||
|
||||
return (
|
||||
<AddDirError
|
||||
message={message}
|
||||
args={args ?? ''}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <AddWorkspaceDirectory directoryPath={result.absolutePath} permissionContext={appState.toolPermissionContext} onAddDirectory={handleAddDirectory} onCancel={() => {
|
||||
onDone(`Did not add ${chalk.bold(result.absolutePath)} as a working directory.`);
|
||||
}} />;
|
||||
|
||||
return (
|
||||
<AddWorkspaceDirectory
|
||||
directoryPath={result.absolutePath}
|
||||
permissionContext={appState.toolPermissionContext}
|
||||
onAddDirectory={handleAddDirectory}
|
||||
onCancel={() => {
|
||||
onDone(
|
||||
`Did not add ${chalk.bold(result.absolutePath)} as a working directory.`,
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import { AgentsMenu } from '../../components/agents/AgentsMenu.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import { getTools } from '../../tools.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext): Promise<React.ReactNode> {
|
||||
const appState = context.getAppState();
|
||||
const permissionContext = appState.toolPermissionContext;
|
||||
const tools = getTools(permissionContext);
|
||||
return <AgentsMenu tools={tools} onExit={onDone} />;
|
||||
import * as React from 'react'
|
||||
import { AgentsMenu } from '../../components/agents/AgentsMenu.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import { getTools } from '../../tools.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext,
|
||||
): Promise<React.ReactNode> {
|
||||
const appState = context.getAppState()
|
||||
const permissionContext = appState.toolPermissionContext
|
||||
const tools = getTools(permissionContext)
|
||||
|
||||
return <AgentsMenu tools={tools} onExit={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,27 +1,40 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import { toString as qrToString } from 'qrcode';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getBridgeAccessToken } from '../../bridge/bridgeConfig.js';
|
||||
import { checkBridgeMinVersion, getBridgeDisabledReason, isEnvLessBridgeEnabled } from '../../bridge/bridgeEnabled.js';
|
||||
import { checkEnvLessBridgeMinVersion } from '../../bridge/envLessBridgeConfig.js';
|
||||
import { BRIDGE_LOGIN_INSTRUCTION, REMOTE_CONTROL_DISCONNECTED_MSG } from '../../bridge/types.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { ListItem } from '../../components/design-system/ListItem.js';
|
||||
import { shouldShowRemoteCallout } from '../../components/RemoteCallout.js';
|
||||
import { useRegisterOverlay } from '../../context/overlayContext.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getBridgeAccessToken } from '../../bridge/bridgeConfig.js'
|
||||
import {
|
||||
checkBridgeMinVersion,
|
||||
getBridgeDisabledReason,
|
||||
isEnvLessBridgeEnabled,
|
||||
} from '../../bridge/bridgeEnabled.js'
|
||||
import { checkEnvLessBridgeMinVersion } from '../../bridge/envLessBridgeConfig.js'
|
||||
import {
|
||||
BRIDGE_LOGIN_INSTRUCTION,
|
||||
REMOTE_CONTROL_DISCONNECTED_MSG,
|
||||
} from '../../bridge/types.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { ListItem } from '../../components/design-system/ListItem.js'
|
||||
import { shouldShowRemoteCallout } from '../../components/RemoteCallout.js'
|
||||
import { useRegisterOverlay } from '../../context/overlayContext.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type {
|
||||
LocalJSXCommandContext,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
|
||||
type Props = {
|
||||
onDone: LocalJSXCommandOnDone;
|
||||
name?: string;
|
||||
};
|
||||
onDone: LocalJSXCommandOnDone
|
||||
name?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* /remote-control command — manages the bidirectional bridge connection.
|
||||
@@ -35,392 +48,194 @@ type Props = {
|
||||
* Running /remote-control when already connected shows a dialog with the session
|
||||
* URL and options to disconnect or continue.
|
||||
*/
|
||||
function BridgeToggle(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
onDone,
|
||||
name
|
||||
} = t0;
|
||||
const setAppState = useSetAppState();
|
||||
const replBridgeConnected = useAppState(_temp);
|
||||
const replBridgeEnabled = useAppState(_temp2);
|
||||
const replBridgeOutboundOnly = useAppState(_temp3);
|
||||
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false);
|
||||
let t1;
|
||||
if ($[0] !== name || $[1] !== onDone || $[2] !== replBridgeConnected || $[3] !== replBridgeEnabled || $[4] !== replBridgeOutboundOnly || $[5] !== setAppState) {
|
||||
t1 = () => {
|
||||
if ((replBridgeConnected || replBridgeEnabled) && !replBridgeOutboundOnly) {
|
||||
setShowDisconnectDialog(true);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
const error = await checkBridgePrerequisites();
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
logEvent("tengu_bridge_command", {
|
||||
action: "preflight_failed" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
onDone(error, {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (shouldShowRemoteCallout()) {
|
||||
setAppState(prev => {
|
||||
if (prev.showRemoteCallout) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
showRemoteCallout: true,
|
||||
replBridgeInitialName: name
|
||||
};
|
||||
});
|
||||
onDone("", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
logEvent("tengu_bridge_command", {
|
||||
action: "connect" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
setAppState(prev_0 => {
|
||||
if (prev_0.replBridgeEnabled && !prev_0.replBridgeOutboundOnly) {
|
||||
return prev_0;
|
||||
}
|
||||
return {
|
||||
...prev_0,
|
||||
replBridgeEnabled: true,
|
||||
replBridgeExplicit: true,
|
||||
replBridgeOutboundOnly: false,
|
||||
replBridgeInitialName: name
|
||||
};
|
||||
});
|
||||
onDone("Remote Control connecting\u2026", {
|
||||
display: "system"
|
||||
});
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
};
|
||||
$[0] = name;
|
||||
$[1] = onDone;
|
||||
$[2] = replBridgeConnected;
|
||||
$[3] = replBridgeEnabled;
|
||||
$[4] = replBridgeOutboundOnly;
|
||||
$[5] = setAppState;
|
||||
$[6] = t1;
|
||||
} else {
|
||||
t1 = $[6];
|
||||
}
|
||||
let t2;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = [];
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
if (showDisconnectDialog) {
|
||||
let t3;
|
||||
if ($[8] !== onDone) {
|
||||
t3 = <BridgeDisconnectDialog onDone={onDone} />;
|
||||
$[8] = onDone;
|
||||
$[9] = t3;
|
||||
} else {
|
||||
t3 = $[9];
|
||||
function BridgeToggle({ onDone, name }: Props): React.ReactNode {
|
||||
const setAppState = useSetAppState()
|
||||
const replBridgeConnected = useAppState(s => s.replBridgeConnected)
|
||||
const replBridgeEnabled = useAppState(s => s.replBridgeEnabled)
|
||||
const replBridgeOutboundOnly = useAppState(s => s.replBridgeOutboundOnly)
|
||||
const [showDisconnectDialog, setShowDisconnectDialog] = useState(false)
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: bridge starts once, should not restart on state changes
|
||||
useEffect(() => {
|
||||
// If already connected or enabled in full bidirectional mode, show
|
||||
// disconnect confirmation. Outbound-only (CCR mirror) doesn't count —
|
||||
// /remote-control upgrades it to full RC instead.
|
||||
if ((replBridgeConnected || replBridgeEnabled) && !replBridgeOutboundOnly) {
|
||||
setShowDisconnectDialog(true)
|
||||
return
|
||||
}
|
||||
return t3;
|
||||
|
||||
let cancelled = false
|
||||
void (async () => {
|
||||
// Pre-flight checks before enabling (awaits GrowthBook init if disk
|
||||
// cache is stale — so Max users don't get a false "not enabled" error)
|
||||
const error = await checkBridgePrerequisites()
|
||||
if (cancelled) return
|
||||
if (error) {
|
||||
logEvent('tengu_bridge_command', {
|
||||
action:
|
||||
'preflight_failed' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
onDone(error, { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
// Show first-time remote dialog if not yet seen.
|
||||
// Store the name now so it's in AppState when the callout handler later
|
||||
// enables the bridge (the handler only sets replBridgeEnabled, not the name).
|
||||
if (shouldShowRemoteCallout()) {
|
||||
setAppState(prev => {
|
||||
if (prev.showRemoteCallout) return prev
|
||||
return {
|
||||
...prev,
|
||||
showRemoteCallout: true,
|
||||
replBridgeInitialName: name,
|
||||
}
|
||||
})
|
||||
onDone('', { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
// Enable the bridge — useReplBridge in REPL.tsx handles the rest:
|
||||
// registers environment, creates session with conversation, connects WebSocket
|
||||
logEvent('tengu_bridge_command', {
|
||||
action:
|
||||
'connect' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
setAppState(prev => {
|
||||
if (prev.replBridgeEnabled && !prev.replBridgeOutboundOnly) return prev
|
||||
return {
|
||||
...prev,
|
||||
replBridgeEnabled: true,
|
||||
replBridgeExplicit: true,
|
||||
replBridgeOutboundOnly: false,
|
||||
replBridgeInitialName: name,
|
||||
}
|
||||
})
|
||||
onDone('Remote Control connecting\u2026', {
|
||||
display: 'system',
|
||||
})
|
||||
})()
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, []) // eslint-disable-line react-hooks/exhaustive-deps -- run once on mount
|
||||
|
||||
if (showDisconnectDialog) {
|
||||
return <BridgeDisconnectDialog onDone={onDone} />
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Dialog shown when /remote-control is used while the bridge is already connected.
|
||||
* Shows the session URL and lets the user disconnect or continue.
|
||||
*/
|
||||
function _temp3(s_1) {
|
||||
return s_1.replBridgeOutboundOnly;
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.replBridgeEnabled;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.replBridgeConnected;
|
||||
}
|
||||
function BridgeDisconnectDialog(t0) {
|
||||
const $ = _c(61);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
useRegisterOverlay("bridge-disconnect-dialog", undefined);
|
||||
const setAppState = useSetAppState();
|
||||
const sessionUrl = useAppState(_temp4);
|
||||
const connectUrl = useAppState(_temp5);
|
||||
const sessionActive = useAppState(_temp6);
|
||||
const [focusIndex, setFocusIndex] = useState(2);
|
||||
const [showQR, setShowQR] = useState(false);
|
||||
const [qrText, setQrText] = useState("");
|
||||
const displayUrl = sessionActive ? sessionUrl : connectUrl;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== displayUrl || $[1] !== showQR) {
|
||||
t1 = () => {
|
||||
if (!showQR || !displayUrl) {
|
||||
setQrText("");
|
||||
return;
|
||||
}
|
||||
qrToString(displayUrl, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L",
|
||||
small: true
|
||||
}).then(setQrText).catch(() => setQrText(""));
|
||||
};
|
||||
t2 = [showQR, displayUrl];
|
||||
$[0] = displayUrl;
|
||||
$[1] = showQR;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] !== onDone || $[5] !== setAppState) {
|
||||
t3 = function handleDisconnect() {
|
||||
setAppState(_temp7);
|
||||
logEvent("tengu_bridge_command", {
|
||||
action: "disconnect" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
onDone(REMOTE_CONTROL_DISCONNECTED_MSG, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[4] = onDone;
|
||||
$[5] = setAppState;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
const handleDisconnect = t3;
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = function handleShowQR() {
|
||||
setShowQR(_temp8);
|
||||
};
|
||||
$[7] = t4;
|
||||
} else {
|
||||
t4 = $[7];
|
||||
}
|
||||
const handleShowQR = t4;
|
||||
let t5;
|
||||
if ($[8] !== onDone) {
|
||||
t5 = function handleContinue() {
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
};
|
||||
$[8] = onDone;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
const handleContinue = t5;
|
||||
let t6;
|
||||
let t7;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = () => setFocusIndex(_temp9);
|
||||
t7 = () => setFocusIndex(_temp0);
|
||||
$[10] = t6;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
t7 = $[11];
|
||||
}
|
||||
let t8;
|
||||
if ($[12] !== focusIndex || $[13] !== handleContinue || $[14] !== handleDisconnect) {
|
||||
t8 = {
|
||||
"select:next": t6,
|
||||
"select:previous": t7,
|
||||
"select:accept": () => {
|
||||
if (focusIndex === 0) {
|
||||
handleDisconnect();
|
||||
} else {
|
||||
if (focusIndex === 1) {
|
||||
handleShowQR();
|
||||
} else {
|
||||
handleContinue();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
$[12] = focusIndex;
|
||||
$[13] = handleContinue;
|
||||
$[14] = handleDisconnect;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
let t9;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = {
|
||||
context: "Select"
|
||||
};
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
useKeybindings(t8, t9);
|
||||
let T0;
|
||||
let T1;
|
||||
let t10;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t14;
|
||||
let t15;
|
||||
let t16;
|
||||
if ($[17] !== displayUrl || $[18] !== handleContinue || $[19] !== qrText || $[20] !== showQR) {
|
||||
const qrLines = qrText ? qrText.split("\n").filter(_temp1) : [];
|
||||
T1 = Dialog;
|
||||
t14 = "Remote Control";
|
||||
t15 = handleContinue;
|
||||
t16 = true;
|
||||
T0 = Box;
|
||||
t10 = "column";
|
||||
t11 = 1;
|
||||
const t17 = displayUrl ? ` at ${displayUrl}` : "";
|
||||
if ($[30] !== t17) {
|
||||
t12 = <Text>This session is available via Remote Control{t17}.</Text>;
|
||||
$[30] = t17;
|
||||
$[31] = t12;
|
||||
} else {
|
||||
t12 = $[31];
|
||||
function BridgeDisconnectDialog({ onDone }: Props): React.ReactNode {
|
||||
useRegisterOverlay('bridge-disconnect-dialog')
|
||||
const setAppState = useSetAppState()
|
||||
const sessionUrl = useAppState(s => s.replBridgeSessionUrl)
|
||||
const connectUrl = useAppState(s => s.replBridgeConnectUrl)
|
||||
const sessionActive = useAppState(s => s.replBridgeSessionActive)
|
||||
const [focusIndex, setFocusIndex] = useState(2)
|
||||
const [showQR, setShowQR] = useState(false)
|
||||
const [qrText, setQrText] = useState('')
|
||||
|
||||
const displayUrl = sessionActive ? sessionUrl : connectUrl
|
||||
|
||||
// Generate QR code when URL changes or QR is toggled on
|
||||
useEffect(() => {
|
||||
if (!showQR || !displayUrl) {
|
||||
setQrText('')
|
||||
return
|
||||
}
|
||||
t13 = showQR && qrLines.length > 0 && <Box flexDirection="column">{qrLines.map(_temp10)}</Box>;
|
||||
$[17] = displayUrl;
|
||||
$[18] = handleContinue;
|
||||
$[19] = qrText;
|
||||
$[20] = showQR;
|
||||
$[21] = T0;
|
||||
$[22] = T1;
|
||||
$[23] = t10;
|
||||
$[24] = t11;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
$[27] = t14;
|
||||
$[28] = t15;
|
||||
$[29] = t16;
|
||||
} else {
|
||||
T0 = $[21];
|
||||
T1 = $[22];
|
||||
t10 = $[23];
|
||||
t11 = $[24];
|
||||
t12 = $[25];
|
||||
t13 = $[26];
|
||||
t14 = $[27];
|
||||
t15 = $[28];
|
||||
t16 = $[29];
|
||||
qrToString(displayUrl, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
small: true,
|
||||
})
|
||||
.then(setQrText)
|
||||
.catch(() => setQrText(''))
|
||||
}, [showQR, displayUrl])
|
||||
|
||||
function handleDisconnect(): void {
|
||||
setAppState(prev => {
|
||||
if (!prev.replBridgeEnabled) return prev
|
||||
return {
|
||||
...prev,
|
||||
replBridgeEnabled: false,
|
||||
replBridgeExplicit: false,
|
||||
replBridgeOutboundOnly: false,
|
||||
}
|
||||
})
|
||||
logEvent('tengu_bridge_command', {
|
||||
action:
|
||||
'disconnect' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
onDone(REMOTE_CONTROL_DISCONNECTED_MSG, { display: 'system' })
|
||||
}
|
||||
const t17 = focusIndex === 0;
|
||||
let t18;
|
||||
if ($[32] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t18 = <Text>Disconnect this session</Text>;
|
||||
$[32] = t18;
|
||||
} else {
|
||||
t18 = $[32];
|
||||
|
||||
function handleShowQR(): void {
|
||||
setShowQR(prev => !prev)
|
||||
}
|
||||
let t19;
|
||||
if ($[33] !== t17) {
|
||||
t19 = <ListItem isFocused={t17}>{t18}</ListItem>;
|
||||
$[33] = t17;
|
||||
$[34] = t19;
|
||||
} else {
|
||||
t19 = $[34];
|
||||
|
||||
function handleContinue(): void {
|
||||
onDone(undefined, { display: 'skip' })
|
||||
}
|
||||
const t20 = focusIndex === 1;
|
||||
const t21 = showQR ? "Hide QR code" : "Show QR code";
|
||||
let t22;
|
||||
if ($[35] !== t21) {
|
||||
t22 = <Text>{t21}</Text>;
|
||||
$[35] = t21;
|
||||
$[36] = t22;
|
||||
} else {
|
||||
t22 = $[36];
|
||||
}
|
||||
let t23;
|
||||
if ($[37] !== t20 || $[38] !== t22) {
|
||||
t23 = <ListItem isFocused={t20}>{t22}</ListItem>;
|
||||
$[37] = t20;
|
||||
$[38] = t22;
|
||||
$[39] = t23;
|
||||
} else {
|
||||
t23 = $[39];
|
||||
}
|
||||
const t24 = focusIndex === 2;
|
||||
let t25;
|
||||
if ($[40] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t25 = <Text>Continue</Text>;
|
||||
$[40] = t25;
|
||||
} else {
|
||||
t25 = $[40];
|
||||
}
|
||||
let t26;
|
||||
if ($[41] !== t24) {
|
||||
t26 = <ListItem isFocused={t24}>{t25}</ListItem>;
|
||||
$[41] = t24;
|
||||
$[42] = t26;
|
||||
} else {
|
||||
t26 = $[42];
|
||||
}
|
||||
let t27;
|
||||
if ($[43] !== t19 || $[44] !== t23 || $[45] !== t26) {
|
||||
t27 = <Box flexDirection="column">{t19}{t23}{t26}</Box>;
|
||||
$[43] = t19;
|
||||
$[44] = t23;
|
||||
$[45] = t26;
|
||||
$[46] = t27;
|
||||
} else {
|
||||
t27 = $[46];
|
||||
}
|
||||
let t28;
|
||||
if ($[47] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t28 = <Text dimColor={true}>Enter to select · Esc to continue</Text>;
|
||||
$[47] = t28;
|
||||
} else {
|
||||
t28 = $[47];
|
||||
}
|
||||
let t29;
|
||||
if ($[48] !== T0 || $[49] !== t10 || $[50] !== t11 || $[51] !== t12 || $[52] !== t13 || $[53] !== t27) {
|
||||
t29 = <T0 flexDirection={t10} gap={t11}>{t12}{t13}{t27}{t28}</T0>;
|
||||
$[48] = T0;
|
||||
$[49] = t10;
|
||||
$[50] = t11;
|
||||
$[51] = t12;
|
||||
$[52] = t13;
|
||||
$[53] = t27;
|
||||
$[54] = t29;
|
||||
} else {
|
||||
t29 = $[54];
|
||||
}
|
||||
let t30;
|
||||
if ($[55] !== T1 || $[56] !== t14 || $[57] !== t15 || $[58] !== t16 || $[59] !== t29) {
|
||||
t30 = <T1 title={t14} onCancel={t15} hideInputGuide={t16}>{t29}</T1>;
|
||||
$[55] = T1;
|
||||
$[56] = t14;
|
||||
$[57] = t15;
|
||||
$[58] = t16;
|
||||
$[59] = t29;
|
||||
$[60] = t30;
|
||||
} else {
|
||||
t30 = $[60];
|
||||
}
|
||||
return t30;
|
||||
|
||||
const ITEM_COUNT = 3
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'select:next': () => setFocusIndex(i => (i + 1) % ITEM_COUNT),
|
||||
'select:previous': () =>
|
||||
setFocusIndex(i => (i - 1 + ITEM_COUNT) % ITEM_COUNT),
|
||||
'select:accept': () => {
|
||||
if (focusIndex === 0) {
|
||||
handleDisconnect()
|
||||
} else if (focusIndex === 1) {
|
||||
handleShowQR()
|
||||
} else {
|
||||
handleContinue()
|
||||
}
|
||||
},
|
||||
},
|
||||
{ context: 'Select' },
|
||||
)
|
||||
|
||||
const qrLines = qrText ? qrText.split('\n').filter(l => l.length > 0) : []
|
||||
|
||||
return (
|
||||
<Dialog title="Remote Control" onCancel={handleContinue} hideInputGuide>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
This session is available via Remote Control
|
||||
{displayUrl ? ` at ${displayUrl}` : ''}.
|
||||
</Text>
|
||||
{showQR && qrLines.length > 0 && (
|
||||
<Box flexDirection="column">
|
||||
{qrLines.map((line, i) => (
|
||||
<Text key={i}>{line}</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box flexDirection="column">
|
||||
<ListItem isFocused={focusIndex === 0}>
|
||||
<Text>Disconnect this session</Text>
|
||||
</ListItem>
|
||||
<ListItem isFocused={focusIndex === 1}>
|
||||
<Text>{showQR ? 'Hide QR code' : 'Show QR code'}</Text>
|
||||
</ListItem>
|
||||
<ListItem isFocused={focusIndex === 2}>
|
||||
<Text>Continue</Text>
|
||||
</ListItem>
|
||||
</Box>
|
||||
<Text dimColor>Enter to select · Esc to continue</Text>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -429,80 +244,52 @@ function BridgeDisconnectDialog(t0) {
|
||||
* cache is stale, so a user who just became entitled (e.g. upgraded to Max,
|
||||
* or the flag just launched) gets an accurate result on the first try.
|
||||
*/
|
||||
function _temp10(line, i_1) {
|
||||
return <Text key={i_1}>{line}</Text>;
|
||||
}
|
||||
function _temp1(l) {
|
||||
return l.length > 0;
|
||||
}
|
||||
function _temp0(i_0) {
|
||||
return (i_0 - 1 + 3) % 3;
|
||||
}
|
||||
function _temp9(i) {
|
||||
return (i + 1) % 3;
|
||||
}
|
||||
function _temp8(prev_0) {
|
||||
return !prev_0;
|
||||
}
|
||||
function _temp7(prev) {
|
||||
if (!prev.replBridgeEnabled) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
replBridgeEnabled: false,
|
||||
replBridgeExplicit: false,
|
||||
replBridgeOutboundOnly: false
|
||||
};
|
||||
}
|
||||
function _temp6(s_1) {
|
||||
return s_1.replBridgeSessionActive;
|
||||
}
|
||||
function _temp5(s_0) {
|
||||
return s_0.replBridgeConnectUrl;
|
||||
}
|
||||
function _temp4(s) {
|
||||
return s.replBridgeSessionUrl;
|
||||
}
|
||||
async function checkBridgePrerequisites(): Promise<string | null> {
|
||||
// Check organization policy — remote control may be disabled
|
||||
const {
|
||||
waitForPolicyLimitsToLoad,
|
||||
isPolicyAllowed
|
||||
} = await import('../../services/policyLimits/index.js');
|
||||
await waitForPolicyLimitsToLoad();
|
||||
const { waitForPolicyLimitsToLoad, isPolicyAllowed } = await import(
|
||||
'../../services/policyLimits/index.js'
|
||||
)
|
||||
await waitForPolicyLimitsToLoad()
|
||||
if (!isPolicyAllowed('allow_remote_control')) {
|
||||
return "Remote Control is disabled by your organization's policy.";
|
||||
return "Remote Control is disabled by your organization's policy."
|
||||
}
|
||||
const disabledReason = await getBridgeDisabledReason();
|
||||
|
||||
const disabledReason = await getBridgeDisabledReason()
|
||||
if (disabledReason) {
|
||||
return disabledReason;
|
||||
return disabledReason
|
||||
}
|
||||
|
||||
// Mirror the v1/v2 branching logic in initReplBridge: env-less (v2) is used
|
||||
// only when the flag is on AND the session is not perpetual. In assistant
|
||||
// mode (KAIROS) useReplBridge sets perpetual=true, which forces
|
||||
// initReplBridge onto the v1 path — so the prerequisite check must match.
|
||||
let useV2 = isEnvLessBridgeEnabled();
|
||||
let useV2 = isEnvLessBridgeEnabled()
|
||||
if (feature('KAIROS') && useV2) {
|
||||
const {
|
||||
isAssistantMode
|
||||
} = await import('../../assistant/index.js');
|
||||
const { isAssistantMode } = await import('../../assistant/index.js')
|
||||
if (isAssistantMode()) {
|
||||
useV2 = false;
|
||||
useV2 = false
|
||||
}
|
||||
}
|
||||
const versionError = useV2 ? await checkEnvLessBridgeMinVersion() : checkBridgeMinVersion();
|
||||
const versionError = useV2
|
||||
? await checkEnvLessBridgeMinVersion()
|
||||
: checkBridgeMinVersion()
|
||||
if (versionError) {
|
||||
return versionError;
|
||||
return versionError
|
||||
}
|
||||
|
||||
if (!getBridgeAccessToken()) {
|
||||
return BRIDGE_LOGIN_INSTRUCTION;
|
||||
return BRIDGE_LOGIN_INSTRUCTION
|
||||
}
|
||||
logForDebugging('[bridge] Prerequisites passed, enabling bridge');
|
||||
return null;
|
||||
|
||||
logForDebugging('[bridge] Prerequisites passed, enabling bridge')
|
||||
return null
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: ToolUseContext & LocalJSXCommandContext, args: string): Promise<React.ReactNode> {
|
||||
const name = args.trim() || undefined;
|
||||
return <BridgeToggle onDone={onDone} name={name} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: ToolUseContext & LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const name = args.trim() || undefined
|
||||
return <BridgeToggle onDone={onDone} name={name} />
|
||||
}
|
||||
|
||||
@@ -1,183 +1,151 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useInterval } from 'usehooks-ts';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Markdown } from '../../components/Markdown.js';
|
||||
import { SpinnerGlyph } from '../../components/Spinner/SpinnerGlyph.js';
|
||||
import { DOWN_ARROW, UP_ARROW } from '../../constants/figures.js';
|
||||
import { getSystemPrompt } from '../../constants/prompts.js';
|
||||
import { useModalOrTerminalSize } from '../../context/modalContext.js';
|
||||
import { getSystemContext, getUserContext } from '../../context.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import ScrollBox, { type ScrollBoxHandle } from '../../ink/components/ScrollBox.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { createAbortController } from '../../utils/abortController.js';
|
||||
import { saveGlobalConfig } from '../../utils/config.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { type CacheSafeParams, getLastCacheSafeParams } from '../../utils/forkedAgent.js';
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
|
||||
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js';
|
||||
import { runSideQuestion } from '../../utils/sideQuestion.js';
|
||||
import { asSystemPrompt } from '../../utils/systemPromptType.js';
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useInterval } from 'usehooks-ts'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Markdown } from '../../components/Markdown.js'
|
||||
import { SpinnerGlyph } from '../../components/Spinner/SpinnerGlyph.js'
|
||||
import { DOWN_ARROW, UP_ARROW } from '../../constants/figures.js'
|
||||
import { getSystemPrompt } from '../../constants/prompts.js'
|
||||
import { useModalOrTerminalSize } from '../../context/modalContext.js'
|
||||
import { getSystemContext, getUserContext } from '../../context.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import ScrollBox, {
|
||||
type ScrollBoxHandle,
|
||||
} from '../../ink/components/ScrollBox.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { createAbortController } from '../../utils/abortController.js'
|
||||
import { saveGlobalConfig } from '../../utils/config.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import {
|
||||
type CacheSafeParams,
|
||||
getLastCacheSafeParams,
|
||||
} from '../../utils/forkedAgent.js'
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||
import type { ProcessUserInputContext } from '../../utils/processUserInput/processUserInput.js'
|
||||
import { runSideQuestion } from '../../utils/sideQuestion.js'
|
||||
import { asSystemPrompt } from '../../utils/systemPromptType.js'
|
||||
|
||||
type BtwComponentProps = {
|
||||
question: string;
|
||||
context: ProcessUserInputContext;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
const CHROME_ROWS = 5;
|
||||
const OUTER_CHROME_ROWS = 6;
|
||||
const SCROLL_LINES = 3;
|
||||
function BtwSideQuestion(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
question,
|
||||
context,
|
||||
onDone
|
||||
} = t0;
|
||||
const [response, setResponse] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
const [frame, setFrame] = useState(0);
|
||||
const scrollRef = useRef(null);
|
||||
const {
|
||||
rows
|
||||
} = useModalOrTerminalSize(useTerminalSize());
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = () => setFrame(_temp);
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
question: string
|
||||
context: ProcessUserInputContext
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
const CHROME_ROWS = 5
|
||||
const OUTER_CHROME_ROWS = 6
|
||||
const SCROLL_LINES = 3
|
||||
|
||||
function BtwSideQuestion({
|
||||
question,
|
||||
context,
|
||||
onDone,
|
||||
}: BtwComponentProps): React.ReactNode {
|
||||
const [response, setResponse] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [frame, setFrame] = useState(0)
|
||||
const scrollRef = useRef<ScrollBoxHandle>(null)
|
||||
const { rows } = useModalOrTerminalSize(useTerminalSize())
|
||||
|
||||
// Animate spinner while loading
|
||||
useInterval(() => setFrame(f => f + 1), response || error ? null : 80)
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (
|
||||
e.key === 'escape' ||
|
||||
e.key === 'return' ||
|
||||
e.key === ' ' ||
|
||||
(e.ctrl && (e.key === 'c' || e.key === 'd'))
|
||||
) {
|
||||
e.preventDefault()
|
||||
onDone(undefined, { display: 'skip' })
|
||||
return
|
||||
}
|
||||
if (e.key === 'up' || (e.ctrl && e.key === 'p')) {
|
||||
e.preventDefault()
|
||||
scrollRef.current?.scrollBy(-SCROLL_LINES)
|
||||
}
|
||||
if (e.key === 'down' || (e.ctrl && e.key === 'n')) {
|
||||
e.preventDefault()
|
||||
scrollRef.current?.scrollBy(SCROLL_LINES)
|
||||
}
|
||||
}
|
||||
useInterval(t1, response || error ? null : 80);
|
||||
let t2;
|
||||
if ($[1] !== onDone) {
|
||||
t2 = function handleKeyDown(e) {
|
||||
if (e.key === "escape" || e.key === "return" || e.key === " " || e.ctrl && (e.key === "c" || e.key === "d")) {
|
||||
e.preventDefault();
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (e.key === "up" || e.ctrl && e.key === "p") {
|
||||
e.preventDefault();
|
||||
scrollRef.current?.scrollBy(-SCROLL_LINES);
|
||||
}
|
||||
if (e.key === "down" || e.ctrl && e.key === "n") {
|
||||
e.preventDefault();
|
||||
scrollRef.current?.scrollBy(SCROLL_LINES);
|
||||
}
|
||||
};
|
||||
$[1] = onDone;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const handleKeyDown = t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] !== context || $[4] !== question) {
|
||||
t3 = () => {
|
||||
const abortController = createAbortController();
|
||||
const fetchResponse = async function fetchResponse() {
|
||||
;
|
||||
try {
|
||||
const cacheSafeParams = await buildCacheSafeParams(context);
|
||||
const result = await runSideQuestion({
|
||||
question,
|
||||
cacheSafeParams
|
||||
});
|
||||
if (!abortController.signal.aborted) {
|
||||
if (result.response) {
|
||||
setResponse(result.response);
|
||||
} else {
|
||||
setError("No response received");
|
||||
}
|
||||
}
|
||||
} catch (t5) {
|
||||
const err = t5;
|
||||
if (!abortController.signal.aborted) {
|
||||
setError(errorMessage(err) || "Failed to get response");
|
||||
|
||||
useEffect(() => {
|
||||
const abortController = createAbortController()
|
||||
|
||||
async function fetchResponse(): Promise<void> {
|
||||
try {
|
||||
const cacheSafeParams = await buildCacheSafeParams(context)
|
||||
const result = await runSideQuestion({ question, cacheSafeParams })
|
||||
|
||||
if (!abortController.signal.aborted) {
|
||||
if (result.response) {
|
||||
setResponse(result.response)
|
||||
} else {
|
||||
setError('No response received')
|
||||
}
|
||||
}
|
||||
};
|
||||
fetchResponse();
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
};
|
||||
t4 = [question, context];
|
||||
$[3] = context;
|
||||
$[4] = question;
|
||||
$[5] = t3;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
t4 = $[6];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
const maxContentHeight = Math.max(5, rows - CHROME_ROWS - OUTER_CHROME_ROWS);
|
||||
let t5;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text color="warning" bold={true}>/btw{" "}</Text>;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== question) {
|
||||
t6 = <Box>{t5}<Text dimColor={true}>{question}</Text></Box>;
|
||||
$[8] = question;
|
||||
$[9] = t6;
|
||||
} else {
|
||||
t6 = $[9];
|
||||
}
|
||||
let t7;
|
||||
if ($[10] !== error || $[11] !== frame || $[12] !== response) {
|
||||
t7 = <ScrollBox ref={scrollRef} flexDirection="column" flexGrow={1}>{error ? <Text color="error">{error}</Text> : response ? <Markdown>{response}</Markdown> : <Box><SpinnerGlyph frame={frame} messageColor="warning" /><Text color="warning">Answering...</Text></Box>}</ScrollBox>;
|
||||
$[10] = error;
|
||||
$[11] = frame;
|
||||
$[12] = response;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
let t8;
|
||||
if ($[14] !== maxContentHeight || $[15] !== t7) {
|
||||
t8 = <Box marginTop={1} marginLeft={2} maxHeight={maxContentHeight}>{t7}</Box>;
|
||||
$[14] = maxContentHeight;
|
||||
$[15] = t7;
|
||||
$[16] = t8;
|
||||
} else {
|
||||
t8 = $[16];
|
||||
}
|
||||
let t9;
|
||||
if ($[17] !== error || $[18] !== response) {
|
||||
t9 = (response || error) && <Box marginTop={1}><Text dimColor={true}>{UP_ARROW}/{DOWN_ARROW} to scroll · Space, Enter, or Escape to dismiss</Text></Box>;
|
||||
$[17] = error;
|
||||
$[18] = response;
|
||||
$[19] = t9;
|
||||
} else {
|
||||
t9 = $[19];
|
||||
}
|
||||
let t10;
|
||||
if ($[20] !== handleKeyDown || $[21] !== t6 || $[22] !== t8 || $[23] !== t9) {
|
||||
t10 = <Box flexDirection="column" paddingLeft={2} marginTop={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t6}{t8}{t9}</Box>;
|
||||
$[20] = handleKeyDown;
|
||||
$[21] = t6;
|
||||
$[22] = t8;
|
||||
$[23] = t9;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
return t10;
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
setError(errorMessage(err) || 'Failed to get response')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void fetchResponse()
|
||||
|
||||
return () => {
|
||||
abortController.abort()
|
||||
}
|
||||
}, [question, context])
|
||||
|
||||
const maxContentHeight = Math.max(5, rows - CHROME_ROWS - OUTER_CHROME_ROWS)
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
paddingLeft={2}
|
||||
marginTop={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Box>
|
||||
<Text color="warning" bold>
|
||||
/btw{' '}
|
||||
</Text>
|
||||
<Text dimColor>{question}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} marginLeft={2} maxHeight={maxContentHeight}>
|
||||
<ScrollBox ref={scrollRef} flexDirection="column" flexGrow={1}>
|
||||
{error ? (
|
||||
<Text color="error">{error}</Text>
|
||||
) : response ? (
|
||||
<Markdown>{response}</Markdown>
|
||||
) : (
|
||||
<Box>
|
||||
<SpinnerGlyph frame={frame} messageColor="warning" />
|
||||
<Text color="warning">Answering...</Text>
|
||||
</Box>
|
||||
)}
|
||||
</ScrollBox>
|
||||
</Box>
|
||||
{(response || error) && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
{UP_ARROW}/{DOWN_ARROW} to scroll · Space, Enter, or Escape to
|
||||
dismiss
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,48 +163,67 @@ function BtwSideQuestion(t0) {
|
||||
* applied buildEffectiveSystemPrompt extras (--agent, --system-prompt,
|
||||
* --append-system-prompt, coordinator mode).
|
||||
*/
|
||||
function _temp(f) {
|
||||
return f + 1;
|
||||
}
|
||||
function stripInProgressAssistantMessage(messages: Message[]): Message[] {
|
||||
const last = messages.at(-1);
|
||||
const last = messages.at(-1)
|
||||
if (last?.type === 'assistant' && last.message.stop_reason === null) {
|
||||
return messages.slice(0, -1);
|
||||
return messages.slice(0, -1)
|
||||
}
|
||||
return messages;
|
||||
return messages
|
||||
}
|
||||
async function buildCacheSafeParams(context: ProcessUserInputContext): Promise<CacheSafeParams> {
|
||||
const forkContextMessages = getMessagesAfterCompactBoundary(stripInProgressAssistantMessage(context.messages));
|
||||
const saved = getLastCacheSafeParams();
|
||||
|
||||
async function buildCacheSafeParams(
|
||||
context: ProcessUserInputContext,
|
||||
): Promise<CacheSafeParams> {
|
||||
const forkContextMessages = getMessagesAfterCompactBoundary(
|
||||
stripInProgressAssistantMessage(context.messages),
|
||||
)
|
||||
const saved = getLastCacheSafeParams()
|
||||
if (saved) {
|
||||
return {
|
||||
systemPrompt: saved.systemPrompt,
|
||||
userContext: saved.userContext,
|
||||
systemContext: saved.systemContext,
|
||||
toolUseContext: context,
|
||||
forkContextMessages
|
||||
};
|
||||
forkContextMessages,
|
||||
}
|
||||
}
|
||||
const [rawSystemPrompt, userContext, systemContext] = await Promise.all([getSystemPrompt(context.options.tools, context.options.mainLoopModel, [], context.options.mcpClients), getUserContext(), getSystemContext()]);
|
||||
const [rawSystemPrompt, userContext, systemContext] = await Promise.all([
|
||||
getSystemPrompt(
|
||||
context.options.tools,
|
||||
context.options.mainLoopModel,
|
||||
[],
|
||||
context.options.mcpClients,
|
||||
),
|
||||
getUserContext(),
|
||||
getSystemContext(),
|
||||
])
|
||||
return {
|
||||
systemPrompt: asSystemPrompt(rawSystemPrompt),
|
||||
userContext,
|
||||
systemContext,
|
||||
toolUseContext: context,
|
||||
forkContextMessages
|
||||
};
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ProcessUserInputContext, args: string): Promise<React.ReactNode> {
|
||||
const question = args?.trim();
|
||||
if (!question) {
|
||||
onDone('Usage: /btw <your question>', {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
forkContextMessages,
|
||||
}
|
||||
}
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ProcessUserInputContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const question = args?.trim()
|
||||
|
||||
if (!question) {
|
||||
onDone('Usage: /btw <your question>', { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
btwUseCount: current.btwUseCount + 1
|
||||
}));
|
||||
return <BtwSideQuestion question={question} context={context} onDone={onDone} />;
|
||||
btwUseCount: current.btwUseCount + 1,
|
||||
}))
|
||||
|
||||
return (
|
||||
<BtwSideQuestion question={question} context={context} onDone={onDone} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,284 +1,240 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useState } from 'react';
|
||||
import { type OptionWithDescription, Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import { isClaudeAISubscriber } from '../../utils/auth.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { CLAUDE_IN_CHROME_MCP_SERVER_NAME, openInChrome } from '../../utils/claudeInChrome/common.js';
|
||||
import { isChromeExtensionInstalled } from '../../utils/claudeInChrome/setup.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { env } from '../../utils/env.js';
|
||||
import { isRunningOnHomespace } from '../../utils/envUtils.js';
|
||||
const CHROME_EXTENSION_URL = 'https://claude.ai/chrome';
|
||||
const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions';
|
||||
const CHROME_RECONNECT_URL = 'https://clau.de/chrome/reconnect';
|
||||
type MenuAction = 'install-extension' | 'reconnect' | 'manage-permissions' | 'toggle-default';
|
||||
import React, { useState } from 'react'
|
||||
import {
|
||||
type OptionWithDescription,
|
||||
Select,
|
||||
} from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import { isClaudeAISubscriber } from '../../utils/auth.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import {
|
||||
CLAUDE_IN_CHROME_MCP_SERVER_NAME,
|
||||
openInChrome,
|
||||
} from '../../utils/claudeInChrome/common.js'
|
||||
import { isChromeExtensionInstalled } from '../../utils/claudeInChrome/setup.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { isRunningOnHomespace } from '../../utils/envUtils.js'
|
||||
|
||||
const CHROME_EXTENSION_URL = 'https://claude.ai/chrome'
|
||||
const CHROME_PERMISSIONS_URL = 'https://clau.de/chrome/permissions'
|
||||
const CHROME_RECONNECT_URL = 'https://clau.de/chrome/reconnect'
|
||||
|
||||
type MenuAction =
|
||||
| 'install-extension'
|
||||
| 'reconnect'
|
||||
| 'manage-permissions'
|
||||
| 'toggle-default'
|
||||
|
||||
type Props = {
|
||||
onDone: (result?: string) => void;
|
||||
isExtensionInstalled: boolean;
|
||||
configEnabled: boolean | undefined;
|
||||
isClaudeAISubscriber: boolean;
|
||||
isWSL: boolean;
|
||||
};
|
||||
function ClaudeInChromeMenu(t0) {
|
||||
const $ = _c(41);
|
||||
const {
|
||||
onDone,
|
||||
isExtensionInstalled: installed,
|
||||
configEnabled,
|
||||
isClaudeAISubscriber,
|
||||
isWSL
|
||||
} = t0;
|
||||
const mcpClients = useAppState(_temp);
|
||||
const [selectKey, setSelectKey] = useState(0);
|
||||
const [enabledByDefault, setEnabledByDefault] = useState(configEnabled ?? false);
|
||||
const [showInstallHint, setShowInstallHint] = useState(false);
|
||||
const [isExtensionInstalled, setIsExtensionInstalled] = useState(installed);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = false && isRunningOnHomespace();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
onDone: (result?: string) => void
|
||||
isExtensionInstalled: boolean
|
||||
configEnabled: boolean | undefined
|
||||
isClaudeAISubscriber: boolean
|
||||
isWSL: boolean
|
||||
}
|
||||
|
||||
function ClaudeInChromeMenu({
|
||||
onDone,
|
||||
isExtensionInstalled: installed,
|
||||
configEnabled,
|
||||
isClaudeAISubscriber,
|
||||
isWSL,
|
||||
}: Props): React.ReactNode {
|
||||
const mcpClients = useAppState(s => s.mcp.clients)
|
||||
const [selectKey, setSelectKey] = useState(0)
|
||||
const [enabledByDefault, setEnabledByDefault] = useState(
|
||||
configEnabled ?? false,
|
||||
)
|
||||
const [showInstallHint, setShowInstallHint] = useState(false)
|
||||
const [isExtensionInstalled, setIsExtensionInstalled] = useState(installed)
|
||||
|
||||
const isHomespace = process.env.USER_TYPE === 'ant' && isRunningOnHomespace()
|
||||
|
||||
const chromeClient = mcpClients.find(
|
||||
c => c.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME,
|
||||
)
|
||||
const isConnected = chromeClient?.type === 'connected'
|
||||
|
||||
function openUrl(url: string): void {
|
||||
if (isHomespace) {
|
||||
void openBrowser(url)
|
||||
} else {
|
||||
void openInChrome(url)
|
||||
}
|
||||
}
|
||||
const isHomespace = t1;
|
||||
let t2;
|
||||
if ($[1] !== mcpClients) {
|
||||
t2 = mcpClients.find(_temp2);
|
||||
$[1] = mcpClients;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
const chromeClient = t2;
|
||||
const isConnected = chromeClient?.type === "connected";
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = function openUrl(url) {
|
||||
if (isHomespace) {
|
||||
openBrowser(url);
|
||||
} else {
|
||||
openInChrome(url);
|
||||
|
||||
function handleAction(action: MenuAction): void {
|
||||
switch (action) {
|
||||
case 'install-extension':
|
||||
setSelectKey(k => k + 1)
|
||||
setShowInstallHint(true)
|
||||
openUrl(CHROME_EXTENSION_URL)
|
||||
break
|
||||
case 'reconnect':
|
||||
setSelectKey(k => k + 1)
|
||||
void isChromeExtensionInstalled().then(installed => {
|
||||
setIsExtensionInstalled(installed)
|
||||
if (installed) {
|
||||
setShowInstallHint(false)
|
||||
}
|
||||
})
|
||||
openUrl(CHROME_RECONNECT_URL)
|
||||
break
|
||||
case 'manage-permissions':
|
||||
setSelectKey(k => k + 1)
|
||||
openUrl(CHROME_PERMISSIONS_URL)
|
||||
break
|
||||
case 'toggle-default': {
|
||||
const newValue = !enabledByDefault
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
claudeInChromeDefaultEnabled: newValue,
|
||||
}))
|
||||
setEnabledByDefault(newValue)
|
||||
break
|
||||
}
|
||||
};
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
const openUrl = t3;
|
||||
let t4;
|
||||
if ($[4] !== enabledByDefault) {
|
||||
t4 = function handleAction(action) {
|
||||
bb22: switch (action) {
|
||||
case "install-extension":
|
||||
{
|
||||
setSelectKey(_temp3);
|
||||
setShowInstallHint(true);
|
||||
openUrl(CHROME_EXTENSION_URL);
|
||||
break bb22;
|
||||
}
|
||||
case "reconnect":
|
||||
{
|
||||
setSelectKey(_temp4);
|
||||
isChromeExtensionInstalled().then(installed_0 => {
|
||||
setIsExtensionInstalled(installed_0);
|
||||
if (installed_0) {
|
||||
setShowInstallHint(false);
|
||||
}
|
||||
});
|
||||
openUrl(CHROME_RECONNECT_URL);
|
||||
break bb22;
|
||||
}
|
||||
case "manage-permissions":
|
||||
{
|
||||
setSelectKey(_temp5);
|
||||
openUrl(CHROME_PERMISSIONS_URL);
|
||||
break bb22;
|
||||
}
|
||||
case "toggle-default":
|
||||
{
|
||||
const newValue = !enabledByDefault;
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
claudeInChromeDefaultEnabled: newValue
|
||||
}));
|
||||
setEnabledByDefault(newValue);
|
||||
}
|
||||
}
|
||||
};
|
||||
$[4] = enabledByDefault;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
const handleAction = t4;
|
||||
let options;
|
||||
if ($[6] !== enabledByDefault || $[7] !== isExtensionInstalled) {
|
||||
options = [];
|
||||
const requiresExtensionSuffix = isExtensionInstalled ? "" : " (requires extension)";
|
||||
if (!isExtensionInstalled && !isHomespace) {
|
||||
let t5;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
label: "Install Chrome extension",
|
||||
value: "install-extension"
|
||||
};
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
options.push(t5);
|
||||
}
|
||||
let t5;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>Manage permissions</Text>;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
let t6;
|
||||
if ($[11] !== requiresExtensionSuffix) {
|
||||
t6 = {
|
||||
label: <>{t5}<Text dimColor={true}>{requiresExtensionSuffix}</Text></>,
|
||||
value: "manage-permissions"
|
||||
};
|
||||
$[11] = requiresExtensionSuffix;
|
||||
$[12] = t6;
|
||||
} else {
|
||||
t6 = $[12];
|
||||
}
|
||||
let t7;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text>Reconnect extension</Text>;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
let t8;
|
||||
if ($[14] !== requiresExtensionSuffix) {
|
||||
t8 = {
|
||||
label: <>{t7}<Text dimColor={true}>{requiresExtensionSuffix}</Text></>,
|
||||
value: "reconnect"
|
||||
};
|
||||
$[14] = requiresExtensionSuffix;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
const t9 = `Enabled by default: ${enabledByDefault ? "Yes" : "No"}`;
|
||||
let t10;
|
||||
if ($[16] !== t9) {
|
||||
t10 = {
|
||||
label: t9,
|
||||
value: "toggle-default"
|
||||
};
|
||||
$[16] = t9;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
options.push(t6, t8, t10);
|
||||
$[6] = enabledByDefault;
|
||||
$[7] = isExtensionInstalled;
|
||||
$[8] = options;
|
||||
} else {
|
||||
options = $[8];
|
||||
}
|
||||
const isDisabled = isWSL;
|
||||
let t5;
|
||||
if ($[18] !== onDone) {
|
||||
t5 = () => onDone();
|
||||
$[18] = onDone;
|
||||
$[19] = t5;
|
||||
} else {
|
||||
t5 = $[19];
|
||||
|
||||
const options: OptionWithDescription<MenuAction>[] = []
|
||||
const requiresExtensionSuffix = isExtensionInstalled
|
||||
? ''
|
||||
: ' (requires extension)'
|
||||
|
||||
if (!isExtensionInstalled && !isHomespace) {
|
||||
options.push({
|
||||
label: 'Install Chrome extension',
|
||||
value: 'install-extension',
|
||||
})
|
||||
}
|
||||
let t6;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text>Claude in Chrome works with the Chrome extension to let you control your browser directly from Claude Code. Navigate websites, fill forms, capture screenshots, record GIFs, and debug with console logs and network requests.</Text>;
|
||||
$[20] = t6;
|
||||
} else {
|
||||
t6 = $[20];
|
||||
}
|
||||
let t7;
|
||||
if ($[21] !== isWSL) {
|
||||
t7 = isWSL && <Text color="error">Claude in Chrome is not supported in WSL at this time.</Text>;
|
||||
$[21] = isWSL;
|
||||
$[22] = t7;
|
||||
} else {
|
||||
t7 = $[22];
|
||||
}
|
||||
let t8;
|
||||
if ($[23] !== isClaudeAISubscriber) {
|
||||
t8 = false;
|
||||
$[23] = isClaudeAISubscriber;
|
||||
$[24] = t8;
|
||||
} else {
|
||||
t8 = $[24];
|
||||
}
|
||||
let t9;
|
||||
if ($[25] !== handleAction || $[26] !== isConnected || $[27] !== isDisabled || $[28] !== isExtensionInstalled || $[29] !== options || $[30] !== selectKey || $[31] !== showInstallHint) {
|
||||
t9 = !isDisabled && <>{!isHomespace && <Box flexDirection="column"><Text>Status:{" "}{isConnected ? <Text color="success">Enabled</Text> : <Text color="inactive">Disabled</Text>}</Text><Text>Extension:{" "}{isExtensionInstalled ? <Text color="success">Installed</Text> : <Text color="warning">Not detected</Text>}</Text></Box>}<Select key={selectKey} options={options} onChange={handleAction} hideIndexes={true} />{showInstallHint && <Text color="warning">Once installed, select {"\"Reconnect extension\""} to connect.</Text>}<Text><Text dimColor={true}>Usage: </Text><Text>claude --chrome</Text><Text dimColor={true}> or </Text><Text>claude --no-chrome</Text></Text><Text dimColor={true}>Site-level permissions are inherited from the Chrome extension. Manage permissions in the Chrome extension settings to control which sites Claude can browse, click, and type on.</Text></>;
|
||||
$[25] = handleAction;
|
||||
$[26] = isConnected;
|
||||
$[27] = isDisabled;
|
||||
$[28] = isExtensionInstalled;
|
||||
$[29] = options;
|
||||
$[30] = selectKey;
|
||||
$[31] = showInstallHint;
|
||||
$[32] = t9;
|
||||
} else {
|
||||
t9 = $[32];
|
||||
}
|
||||
let t10;
|
||||
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text dimColor={true}>Learn more: https://code.claude.com/docs/en/chrome</Text>;
|
||||
$[33] = t10;
|
||||
} else {
|
||||
t10 = $[33];
|
||||
}
|
||||
let t11;
|
||||
if ($[34] !== t7 || $[35] !== t8 || $[36] !== t9) {
|
||||
t11 = <Box flexDirection="column" gap={1}>{t6}{t7}{t8}{t9}{t10}</Box>;
|
||||
$[34] = t7;
|
||||
$[35] = t8;
|
||||
$[36] = t9;
|
||||
$[37] = t11;
|
||||
} else {
|
||||
t11 = $[37];
|
||||
}
|
||||
let t12;
|
||||
if ($[38] !== t11 || $[39] !== t5) {
|
||||
t12 = <Dialog title="Claude in Chrome (Beta)" onCancel={t5} color="chromeYellow">{t11}</Dialog>;
|
||||
$[38] = t11;
|
||||
$[39] = t5;
|
||||
$[40] = t12;
|
||||
} else {
|
||||
t12 = $[40];
|
||||
}
|
||||
return t12;
|
||||
|
||||
options.push(
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Text>Manage permissions</Text>
|
||||
<Text dimColor>{requiresExtensionSuffix}</Text>
|
||||
</>
|
||||
),
|
||||
value: 'manage-permissions',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<>
|
||||
<Text>Reconnect extension</Text>
|
||||
<Text dimColor>{requiresExtensionSuffix}</Text>
|
||||
</>
|
||||
),
|
||||
value: 'reconnect',
|
||||
},
|
||||
{
|
||||
label: `Enabled by default: ${enabledByDefault ? 'Yes' : 'No'}`,
|
||||
value: 'toggle-default',
|
||||
},
|
||||
)
|
||||
|
||||
const isDisabled =
|
||||
isWSL || ("external" !== 'ant' && !isClaudeAISubscriber)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Claude in Chrome (Beta)"
|
||||
onCancel={() => onDone()}
|
||||
color="chromeYellow"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
Claude in Chrome works with the Chrome extension to let you control
|
||||
your browser directly from Claude Code. Navigate websites, fill forms,
|
||||
capture screenshots, record GIFs, and debug with console logs and
|
||||
network requests.
|
||||
</Text>
|
||||
|
||||
{isWSL && (
|
||||
<Text color="error">
|
||||
Claude in Chrome is not supported in WSL at this time.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
|
||||
{"external" !== 'ant' && !isClaudeAISubscriber && (
|
||||
<Text color="error">
|
||||
Claude in Chrome requires a claude.ai subscription.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!isDisabled && (
|
||||
<>
|
||||
{!isHomespace && (
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
Status:{' '}
|
||||
{isConnected ? (
|
||||
<Text color="success">Enabled</Text>
|
||||
) : (
|
||||
<Text color="inactive">Disabled</Text>
|
||||
)}
|
||||
</Text>
|
||||
<Text>
|
||||
Extension:{' '}
|
||||
{isExtensionInstalled ? (
|
||||
<Text color="success">Installed</Text>
|
||||
) : (
|
||||
<Text color="warning">Not detected</Text>
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Select
|
||||
key={selectKey}
|
||||
options={options}
|
||||
onChange={handleAction}
|
||||
hideIndexes
|
||||
/>
|
||||
|
||||
{showInstallHint && (
|
||||
<Text color="warning">
|
||||
Once installed, select {'"Reconnect extension"'} to connect.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Text>
|
||||
<Text dimColor>Usage: </Text>
|
||||
<Text>claude --chrome</Text>
|
||||
<Text dimColor> or </Text>
|
||||
<Text>claude --no-chrome</Text>
|
||||
</Text>
|
||||
|
||||
<Text dimColor>
|
||||
Site-level permissions are inherited from the Chrome extension.
|
||||
Manage permissions in the Chrome extension settings to control
|
||||
which sites Claude can browse, click, and type on.
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
<Text dimColor>Learn more: https://code.claude.com/docs/en/chrome</Text>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
function _temp5(k) {
|
||||
return k + 1;
|
||||
|
||||
export const call = async function (
|
||||
onDone: (result?: string) => void,
|
||||
): Promise<React.ReactNode> {
|
||||
const isExtensionInstalled = await isChromeExtensionInstalled()
|
||||
const config = getGlobalConfig()
|
||||
const isSubscriber = isClaudeAISubscriber()
|
||||
const isWSL = env.isWslEnvironment()
|
||||
|
||||
return (
|
||||
<ClaudeInChromeMenu
|
||||
onDone={onDone}
|
||||
isExtensionInstalled={isExtensionInstalled}
|
||||
configEnabled={config.claudeInChromeDefaultEnabled}
|
||||
isClaudeAISubscriber={isSubscriber}
|
||||
isWSL={isWSL}
|
||||
/>
|
||||
)
|
||||
}
|
||||
function _temp4(k_0) {
|
||||
return k_0 + 1;
|
||||
}
|
||||
function _temp3(k_1) {
|
||||
return k_1 + 1;
|
||||
}
|
||||
function _temp2(c) {
|
||||
return c.name === CLAUDE_IN_CHROME_MCP_SERVER_NAME;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mcp.clients;
|
||||
}
|
||||
export const call = async function (onDone: (result?: string) => void): Promise<React.ReactNode> {
|
||||
const isExtensionInstalled = await isChromeExtensionInstalled();
|
||||
const config = getGlobalConfig();
|
||||
const isSubscriber = isClaudeAISubscriber();
|
||||
const isWSL = env.isWslEnvironment();
|
||||
return <ClaudeInChromeMenu onDone={onDone} isExtensionInstalled={isExtensionInstalled} configEnabled={config.claudeInChromeDefaultEnabled} isClaudeAISubscriber={isSubscriber} isWSL={isWSL} />;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Settings } from '../../components/Settings/Settings.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { Settings } from '../../components/Settings/Settings.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Config" />;
|
||||
};
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Config" />
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { ContextVisualization } from '../../components/ContextVisualization.js';
|
||||
import { microcompactMessages } from '../../services/compact/microCompact.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { analyzeContextUsage } from '../../utils/analyzeContext.js';
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js';
|
||||
import { renderToAnsiString } from '../../utils/staticRender.js';
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { ContextVisualization } from '../../components/ContextVisualization.js'
|
||||
import { microcompactMessages } from '../../services/compact/microCompact.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { analyzeContextUsage } from '../../utils/analyzeContext.js'
|
||||
import { getMessagesAfterCompactBoundary } from '../../utils/messages.js'
|
||||
import { renderToAnsiString } from '../../utils/staticRender.js'
|
||||
|
||||
/**
|
||||
* Apply the same context transforms query.ts does before the API call, so
|
||||
@@ -16,48 +16,53 @@ import { renderToAnsiString } from '../../utils/staticRender.js';
|
||||
* was collapsed — user sees "180k, 3 spans collapsed" when the API sees 120k.
|
||||
*/
|
||||
function toApiView(messages: Message[]): Message[] {
|
||||
let view = getMessagesAfterCompactBoundary(messages);
|
||||
let view = getMessagesAfterCompactBoundary(messages)
|
||||
if (feature('CONTEXT_COLLAPSE')) {
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const {
|
||||
projectView
|
||||
} = require('../../services/contextCollapse/operations.js') as typeof import('../../services/contextCollapse/operations.js');
|
||||
const { projectView } =
|
||||
require('../../services/contextCollapse/operations.js') as typeof import('../../services/contextCollapse/operations.js')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
view = projectView(view);
|
||||
view = projectView(view)
|
||||
}
|
||||
return view;
|
||||
return view
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
const {
|
||||
messages,
|
||||
getAppState,
|
||||
options: {
|
||||
mainLoopModel,
|
||||
tools
|
||||
}
|
||||
} = context;
|
||||
const apiView = toApiView(messages);
|
||||
options: { mainLoopModel, tools },
|
||||
} = context
|
||||
|
||||
const apiView = toApiView(messages)
|
||||
|
||||
// Apply microcompact to get accurate representation of messages sent to API
|
||||
const {
|
||||
messages: compactedMessages
|
||||
} = await microcompactMessages(apiView);
|
||||
const { messages: compactedMessages } = await microcompactMessages(apiView)
|
||||
|
||||
// Get terminal width for responsive sizing
|
||||
const terminalWidth = process.stdout.columns || 80;
|
||||
const appState = getAppState();
|
||||
const terminalWidth = process.stdout.columns || 80
|
||||
|
||||
const appState = getAppState()
|
||||
|
||||
// Analyze context with compacted messages
|
||||
// Pass original messages as last parameter for accurate API usage extraction
|
||||
const data = await analyzeContextUsage(compactedMessages, mainLoopModel, async () => appState.toolPermissionContext, tools, appState.agentDefinitions, terminalWidth, context,
|
||||
// Pass full context for system prompt calculation
|
||||
undefined,
|
||||
// mainThreadAgentDefinition
|
||||
apiView // Original messages for API usage extraction
|
||||
);
|
||||
const data = await analyzeContextUsage(
|
||||
compactedMessages,
|
||||
mainLoopModel,
|
||||
async () => appState.toolPermissionContext,
|
||||
tools,
|
||||
appState.agentDefinitions,
|
||||
terminalWidth,
|
||||
context, // Pass full context for system prompt calculation
|
||||
undefined, // mainThreadAgentDefinition
|
||||
apiView, // Original messages for API usage extraction
|
||||
)
|
||||
|
||||
// Render to ANSI string to preserve colors and pass to onDone like local commands do
|
||||
const output = await renderToAnsiString(<ContextVisualization data={data} />);
|
||||
onDone(output);
|
||||
return null;
|
||||
const output = await renderToAnsiString(<ContextVisualization data={data} />)
|
||||
onDone(output)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,45 +1,44 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import { marked, type Tokens } from 'marked';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import React, { useRef } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import type { OptionWithDescription } from '../../components/CustomSelect/select.js';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { AssistantMessage, Message } from '../../types/message.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { extractTextContent, stripPromptXMLTags } from '../../utils/messages.js';
|
||||
import { countCharInString } from '../../utils/stringUtils.js';
|
||||
const COPY_DIR = join(tmpdir(), 'claude');
|
||||
const RESPONSE_FILENAME = 'response.md';
|
||||
const MAX_LOOKBACK = 20;
|
||||
import { mkdir, writeFile } from 'fs/promises'
|
||||
import { marked, type Tokens } from 'marked'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import React, { useRef } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import type { OptionWithDescription } from '../../components/CustomSelect/select.js'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Byline } from '../../components/design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { AssistantMessage, Message } from '../../types/message.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { extractTextContent, stripPromptXMLTags } from '../../utils/messages.js'
|
||||
import { countCharInString } from '../../utils/stringUtils.js'
|
||||
|
||||
const COPY_DIR = join(tmpdir(), 'claude')
|
||||
const RESPONSE_FILENAME = 'response.md'
|
||||
const MAX_LOOKBACK = 20
|
||||
|
||||
type CodeBlock = {
|
||||
code: string;
|
||||
lang: string | undefined;
|
||||
};
|
||||
code: string
|
||||
lang: string | undefined
|
||||
}
|
||||
|
||||
function extractCodeBlocks(markdown: string): CodeBlock[] {
|
||||
const tokens = marked.lexer(stripPromptXMLTags(markdown));
|
||||
const blocks: CodeBlock[] = [];
|
||||
const tokens = marked.lexer(stripPromptXMLTags(markdown))
|
||||
const blocks: CodeBlock[] = []
|
||||
for (const token of tokens) {
|
||||
if (token.type === 'code') {
|
||||
const codeToken = token as Tokens.Code;
|
||||
blocks.push({
|
||||
code: codeToken.text,
|
||||
lang: codeToken.lang
|
||||
});
|
||||
const codeToken = token as Tokens.Code
|
||||
blocks.push({ code: codeToken.text, lang: codeToken.lang })
|
||||
}
|
||||
}
|
||||
return blocks;
|
||||
return blocks
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,323 +47,267 @@ function extractCodeBlocks(markdown: string): CodeBlock[] {
|
||||
* Index 0 = latest, 1 = second-to-latest, etc. Caps at MAX_LOOKBACK.
|
||||
*/
|
||||
export function collectRecentAssistantTexts(messages: Message[]): string[] {
|
||||
const texts: string[] = [];
|
||||
for (let i = messages.length - 1; i >= 0 && texts.length < MAX_LOOKBACK; i--) {
|
||||
const msg = messages[i];
|
||||
if (msg?.type !== 'assistant' || msg.isApiErrorMessage) continue;
|
||||
const content = (msg as AssistantMessage).message.content;
|
||||
if (!Array.isArray(content)) continue;
|
||||
const text = extractTextContent(content, '\n\n');
|
||||
if (text) texts.push(text);
|
||||
const texts: string[] = []
|
||||
for (
|
||||
let i = messages.length - 1;
|
||||
i >= 0 && texts.length < MAX_LOOKBACK;
|
||||
i--
|
||||
) {
|
||||
const msg = messages[i]
|
||||
if (msg?.type !== 'assistant' || msg.isApiErrorMessage) continue
|
||||
const content = (msg as AssistantMessage).message.content
|
||||
if (!Array.isArray(content)) continue
|
||||
const text = extractTextContent(content, '\n\n')
|
||||
if (text) texts.push(text)
|
||||
}
|
||||
return texts;
|
||||
return texts
|
||||
}
|
||||
|
||||
export function fileExtension(lang: string | undefined): string {
|
||||
if (lang) {
|
||||
// Sanitize to prevent path traversal (e.g. ```../../etc/passwd)
|
||||
// Language identifiers are alphanumeric: python, tsx, jsonc, etc.
|
||||
const sanitized = lang.replace(/[^a-zA-Z0-9]/g, '');
|
||||
const sanitized = lang.replace(/[^a-zA-Z0-9]/g, '')
|
||||
if (sanitized && sanitized !== 'plaintext') {
|
||||
return `.${sanitized}`;
|
||||
return `.${sanitized}`
|
||||
}
|
||||
}
|
||||
return '.txt';
|
||||
return '.txt'
|
||||
}
|
||||
|
||||
async function writeToFile(text: string, filename: string): Promise<string> {
|
||||
const filePath = join(COPY_DIR, filename);
|
||||
await mkdir(COPY_DIR, {
|
||||
recursive: true
|
||||
});
|
||||
await writeFile(filePath, text, 'utf-8');
|
||||
return filePath;
|
||||
const filePath = join(COPY_DIR, filename)
|
||||
await mkdir(COPY_DIR, { recursive: true })
|
||||
await writeFile(filePath, text, 'utf-8')
|
||||
return filePath
|
||||
}
|
||||
async function copyOrWriteToFile(text: string, filename: string): Promise<string> {
|
||||
const raw = await setClipboard(text);
|
||||
if (raw) process.stdout.write(raw);
|
||||
const lineCount = countCharInString(text, '\n') + 1;
|
||||
const charCount = text.length;
|
||||
|
||||
async function copyOrWriteToFile(
|
||||
text: string,
|
||||
filename: string,
|
||||
): Promise<string> {
|
||||
const raw = await setClipboard(text)
|
||||
if (raw) process.stdout.write(raw)
|
||||
const lineCount = countCharInString(text, '\n') + 1
|
||||
const charCount = text.length
|
||||
// Also write to a temp file — clipboard paths are best-effort (OSC 52 needs
|
||||
// terminal support), so the file provides a reliable fallback.
|
||||
try {
|
||||
const filePath = await writeToFile(text, filename);
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)\nAlso written to ${filePath}`;
|
||||
const filePath = await writeToFile(text, filename)
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)\nAlso written to ${filePath}`
|
||||
} catch {
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)`;
|
||||
return `Copied to clipboard (${charCount} characters, ${lineCount} lines)`
|
||||
}
|
||||
}
|
||||
|
||||
function truncateLine(text: string, maxLen: number): string {
|
||||
const firstLine = text.split('\n')[0] ?? '';
|
||||
const firstLine = text.split('\n')[0] ?? ''
|
||||
if (stringWidth(firstLine) <= maxLen) {
|
||||
return firstLine;
|
||||
return firstLine
|
||||
}
|
||||
let result = '';
|
||||
let width = 0;
|
||||
const targetWidth = maxLen - 1;
|
||||
let result = ''
|
||||
let width = 0
|
||||
const targetWidth = maxLen - 1
|
||||
for (const char of firstLine) {
|
||||
const charWidth = stringWidth(char);
|
||||
if (width + charWidth > targetWidth) break;
|
||||
result += char;
|
||||
width += charWidth;
|
||||
const charWidth = stringWidth(char)
|
||||
if (width + charWidth > targetWidth) break
|
||||
result += char
|
||||
width += charWidth
|
||||
}
|
||||
return result + '\u2026';
|
||||
return result + '\u2026'
|
||||
}
|
||||
|
||||
type PickerProps = {
|
||||
fullText: string;
|
||||
codeBlocks: CodeBlock[];
|
||||
messageAge: number;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
type PickerSelection = number | 'full' | 'always';
|
||||
function CopyPicker(t0) {
|
||||
const $ = _c(33);
|
||||
const {
|
||||
fullText,
|
||||
codeBlocks,
|
||||
messageAge,
|
||||
onDone
|
||||
} = t0;
|
||||
const focusedRef = useRef("full");
|
||||
const t1 = `${fullText.length} chars, ${countCharInString(fullText, "\n") + 1} lines`;
|
||||
let t2;
|
||||
if ($[0] !== t1) {
|
||||
t2 = {
|
||||
label: "Full response",
|
||||
value: "full" as const,
|
||||
description: t1
|
||||
};
|
||||
$[0] = t1;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== codeBlocks || $[3] !== t2) {
|
||||
let t4;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Always copy full response",
|
||||
value: "always" as const,
|
||||
description: "Skip this picker in the future (revert via /config)"
|
||||
};
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
t3 = [t2, ...codeBlocks.map(_temp), t4];
|
||||
$[2] = codeBlocks;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const options = t3;
|
||||
let t4;
|
||||
if ($[6] !== codeBlocks || $[7] !== fullText) {
|
||||
t4 = function getSelectionContent(selected) {
|
||||
if (selected === "full" || selected === "always") {
|
||||
return {
|
||||
text: fullText,
|
||||
filename: RESPONSE_FILENAME
|
||||
};
|
||||
}
|
||||
const block_0 = codeBlocks[selected];
|
||||
fullText: string
|
||||
codeBlocks: CodeBlock[]
|
||||
messageAge: number
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
type PickerSelection = number | 'full' | 'always'
|
||||
|
||||
function CopyPicker({
|
||||
fullText,
|
||||
codeBlocks,
|
||||
messageAge,
|
||||
onDone,
|
||||
}: PickerProps): React.ReactNode {
|
||||
const focusedRef = useRef<PickerSelection>('full')
|
||||
|
||||
const options: OptionWithDescription<PickerSelection>[] = [
|
||||
{
|
||||
label: 'Full response',
|
||||
value: 'full' as const,
|
||||
description: `${fullText.length} chars, ${countCharInString(fullText, '\n') + 1} lines`,
|
||||
},
|
||||
...codeBlocks.map((block, index) => {
|
||||
const blockLines = countCharInString(block.code, '\n') + 1
|
||||
return {
|
||||
text: block_0.code,
|
||||
filename: `copy${fileExtension(block_0.lang)}`,
|
||||
blockIndex: selected
|
||||
};
|
||||
};
|
||||
$[6] = codeBlocks;
|
||||
$[7] = fullText;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const getSelectionContent = t4;
|
||||
let t5;
|
||||
if ($[9] !== codeBlocks.length || $[10] !== getSelectionContent || $[11] !== messageAge || $[12] !== onDone) {
|
||||
t5 = async function handleSelect(selected_0) {
|
||||
const content = getSelectionContent(selected_0);
|
||||
if (selected_0 === "always") {
|
||||
if (!getGlobalConfig().copyFullResponse) {
|
||||
saveGlobalConfig(_temp2);
|
||||
}
|
||||
logEvent("tengu_copy", {
|
||||
block_count: codeBlocks.length,
|
||||
always: true,
|
||||
message_age: messageAge
|
||||
});
|
||||
const result = await copyOrWriteToFile(content.text, content.filename);
|
||||
onDone(`${result}\nPreference saved. Use /config to change copyFullResponse`);
|
||||
return;
|
||||
label: truncateLine(block.code, 60),
|
||||
value: index,
|
||||
description:
|
||||
[block.lang, blockLines > 1 ? `${blockLines} lines` : undefined]
|
||||
.filter(Boolean)
|
||||
.join(', ') || undefined,
|
||||
}
|
||||
logEvent("tengu_copy", {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge
|
||||
});
|
||||
const result_0 = await copyOrWriteToFile(content.text, content.filename);
|
||||
onDone(result_0);
|
||||
};
|
||||
$[9] = codeBlocks.length;
|
||||
$[10] = getSelectionContent;
|
||||
$[11] = messageAge;
|
||||
$[12] = onDone;
|
||||
$[13] = t5;
|
||||
} else {
|
||||
t5 = $[13];
|
||||
}),
|
||||
{
|
||||
label: 'Always copy full response',
|
||||
value: 'always' as const,
|
||||
description: 'Skip this picker in the future (revert via /config)',
|
||||
},
|
||||
]
|
||||
|
||||
function getSelectionContent(selected: PickerSelection): {
|
||||
text: string
|
||||
filename: string
|
||||
blockIndex?: number
|
||||
} {
|
||||
if (selected === 'full' || selected === 'always') {
|
||||
return { text: fullText, filename: RESPONSE_FILENAME }
|
||||
}
|
||||
const block = codeBlocks[selected]!
|
||||
return {
|
||||
text: block.code,
|
||||
filename: `copy${fileExtension(block.lang)}`,
|
||||
blockIndex: selected,
|
||||
}
|
||||
}
|
||||
const handleSelect = t5;
|
||||
let t6;
|
||||
if ($[14] !== codeBlocks.length || $[15] !== getSelectionContent || $[16] !== messageAge || $[17] !== onDone) {
|
||||
const handleWrite = async function handleWrite(selected_1) {
|
||||
const content_0 = getSelectionContent(selected_1);
|
||||
logEvent("tengu_copy", {
|
||||
selected_block: content_0.blockIndex,
|
||||
|
||||
async function handleSelect(selected: PickerSelection): Promise<void> {
|
||||
const content = getSelectionContent(selected)
|
||||
if (selected === 'always') {
|
||||
if (!getGlobalConfig().copyFullResponse) {
|
||||
saveGlobalConfig(c => ({ ...c, copyFullResponse: true }))
|
||||
}
|
||||
logEvent('tengu_copy', {
|
||||
block_count: codeBlocks.length,
|
||||
always: true,
|
||||
message_age: messageAge,
|
||||
write_shortcut: true
|
||||
});
|
||||
;
|
||||
try {
|
||||
const filePath = await writeToFile(content_0.text, content_0.filename);
|
||||
onDone(`Written to ${filePath}`);
|
||||
} catch (t7) {
|
||||
const e = t7;
|
||||
onDone(`Failed to write file: ${e instanceof Error ? e.message : e}`);
|
||||
}
|
||||
};
|
||||
t6 = function handleKeyDown(e_0) {
|
||||
if (e_0.key === "w") {
|
||||
e_0.preventDefault();
|
||||
handleWrite(focusedRef.current);
|
||||
}
|
||||
};
|
||||
$[14] = codeBlocks.length;
|
||||
$[15] = getSelectionContent;
|
||||
$[16] = messageAge;
|
||||
$[17] = onDone;
|
||||
$[18] = t6;
|
||||
} else {
|
||||
t6 = $[18];
|
||||
})
|
||||
const result = await copyOrWriteToFile(content.text, content.filename)
|
||||
onDone(
|
||||
`${result}\nPreference saved. Use /config to change copyFullResponse`,
|
||||
)
|
||||
return
|
||||
}
|
||||
logEvent('tengu_copy', {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge,
|
||||
})
|
||||
const result = await copyOrWriteToFile(content.text, content.filename)
|
||||
onDone(result)
|
||||
}
|
||||
const handleKeyDown = t6;
|
||||
let t7;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text dimColor={true}>Select content to copy:</Text>;
|
||||
$[19] = t7;
|
||||
} else {
|
||||
t7 = $[19];
|
||||
|
||||
async function handleWrite(selected: PickerSelection): Promise<void> {
|
||||
const content = getSelectionContent(selected)
|
||||
logEvent('tengu_copy', {
|
||||
selected_block: content.blockIndex,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: messageAge,
|
||||
write_shortcut: true,
|
||||
})
|
||||
try {
|
||||
const filePath = await writeToFile(content.text, content.filename)
|
||||
onDone(`Written to ${filePath}`)
|
||||
} catch (e) {
|
||||
onDone(`Failed to write file: ${e instanceof Error ? e.message : e}`)
|
||||
}
|
||||
}
|
||||
let t8;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = value => {
|
||||
focusedRef.current = value;
|
||||
};
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (e.key === 'w') {
|
||||
e.preventDefault()
|
||||
void handleWrite(focusedRef.current)
|
||||
}
|
||||
}
|
||||
let t9;
|
||||
if ($[21] !== handleSelect) {
|
||||
t9 = selected_2 => {
|
||||
handleSelect(selected_2);
|
||||
};
|
||||
$[21] = handleSelect;
|
||||
$[22] = t9;
|
||||
} else {
|
||||
t9 = $[22];
|
||||
}
|
||||
let t10;
|
||||
if ($[23] !== onDone) {
|
||||
t10 = () => {
|
||||
onDone("Copy cancelled", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[23] = onDone;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
let t11;
|
||||
if ($[25] !== options || $[26] !== t10 || $[27] !== t9) {
|
||||
t11 = <Select options={options} hideIndexes={false} onFocus={t8} onChange={t9} onCancel={t10} />;
|
||||
$[25] = options;
|
||||
$[26] = t10;
|
||||
$[27] = t9;
|
||||
$[28] = t11;
|
||||
} else {
|
||||
t11 = $[28];
|
||||
}
|
||||
let t12;
|
||||
if ($[29] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t12 = <Text dimColor={true}><Byline><KeyboardShortcutHint shortcut="enter" action="copy" /><KeyboardShortcutHint shortcut="w" action="write to file" /><KeyboardShortcutHint shortcut="esc" action="cancel" /></Byline></Text>;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
let t13;
|
||||
if ($[30] !== handleKeyDown || $[31] !== t11) {
|
||||
t13 = <Pane><Box flexDirection="column" gap={1} tabIndex={0} autoFocus={true} onKeyDown={handleKeyDown}>{t7}{t11}{t12}</Box></Pane>;
|
||||
$[30] = handleKeyDown;
|
||||
$[31] = t11;
|
||||
$[32] = t13;
|
||||
} else {
|
||||
t13 = $[32];
|
||||
}
|
||||
return t13;
|
||||
}
|
||||
function _temp2(c) {
|
||||
return {
|
||||
...c,
|
||||
copyFullResponse: true
|
||||
};
|
||||
}
|
||||
function _temp(block, index) {
|
||||
const blockLines = countCharInString(block.code, "\n") + 1;
|
||||
return {
|
||||
label: truncateLine(block.code, 60),
|
||||
value: index,
|
||||
description: [block.lang, blockLines > 1 ? `${blockLines} lines` : undefined].filter(Boolean).join(", ") || undefined
|
||||
};
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text dimColor>Select content to copy:</Text>
|
||||
<Select<PickerSelection>
|
||||
options={options}
|
||||
hideIndexes={false}
|
||||
onFocus={value => {
|
||||
focusedRef.current = value
|
||||
}}
|
||||
onChange={selected => {
|
||||
void handleSelect(selected)
|
||||
}}
|
||||
onCancel={() => {
|
||||
onDone('Copy cancelled', { display: 'system' })
|
||||
}}
|
||||
/>
|
||||
<Text dimColor>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="enter" action="copy" />
|
||||
<KeyboardShortcutHint shortcut="w" action="write to file" />
|
||||
<KeyboardShortcutHint shortcut="esc" action="cancel" />
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const texts = collectRecentAssistantTexts(context.messages);
|
||||
const texts = collectRecentAssistantTexts(context.messages)
|
||||
|
||||
if (texts.length === 0) {
|
||||
onDone('No assistant message to copy');
|
||||
return null;
|
||||
onDone('No assistant message to copy')
|
||||
return null
|
||||
}
|
||||
|
||||
// /copy N reaches back N-1 messages (1 = latest, 2 = second-to-latest, ...)
|
||||
let age = 0;
|
||||
const arg = args?.trim();
|
||||
let age = 0
|
||||
const arg = args?.trim()
|
||||
if (arg) {
|
||||
const n = Number(arg);
|
||||
const n = Number(arg)
|
||||
if (!Number.isInteger(n) || n < 1) {
|
||||
onDone(`Usage: /copy [N] where N is 1 (latest), 2, 3, \u2026 Got: ${arg}`);
|
||||
return null;
|
||||
onDone(`Usage: /copy [N] where N is 1 (latest), 2, 3, \u2026 Got: ${arg}`)
|
||||
return null
|
||||
}
|
||||
if (n > texts.length) {
|
||||
onDone(`Only ${texts.length} assistant ${texts.length === 1 ? 'message' : 'messages'} available to copy`);
|
||||
return null;
|
||||
onDone(
|
||||
`Only ${texts.length} assistant ${texts.length === 1 ? 'message' : 'messages'} available to copy`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
age = n - 1;
|
||||
age = n - 1
|
||||
}
|
||||
const text = texts[age]!;
|
||||
const codeBlocks = extractCodeBlocks(text);
|
||||
const config = getGlobalConfig();
|
||||
|
||||
const text = texts[age]!
|
||||
const codeBlocks = extractCodeBlocks(text)
|
||||
const config = getGlobalConfig()
|
||||
|
||||
if (codeBlocks.length === 0 || config.copyFullResponse) {
|
||||
logEvent('tengu_copy', {
|
||||
always: config.copyFullResponse,
|
||||
block_count: codeBlocks.length,
|
||||
message_age: age
|
||||
});
|
||||
const result = await copyOrWriteToFile(text, RESPONSE_FILENAME);
|
||||
onDone(result);
|
||||
return null;
|
||||
message_age: age,
|
||||
})
|
||||
const result = await copyOrWriteToFile(text, RESPONSE_FILENAME)
|
||||
onDone(result)
|
||||
return null
|
||||
}
|
||||
return <CopyPicker fullText={text} codeBlocks={codeBlocks} messageAge={age} onDone={onDone} />;
|
||||
};
|
||||
|
||||
return (
|
||||
<CopyPicker
|
||||
fullText={text}
|
||||
codeBlocks={codeBlocks}
|
||||
messageAge={age}
|
||||
onDone={onDone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { DesktopHandoff } from '../../components/DesktopHandoff.js';
|
||||
export async function call(onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void): Promise<React.ReactNode> {
|
||||
return <DesktopHandoff onDone={onDone} />;
|
||||
import React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { DesktopHandoff } from '../../components/DesktopHandoff.js'
|
||||
|
||||
export async function call(
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void,
|
||||
): Promise<React.ReactNode> {
|
||||
return <DesktopHandoff onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
const {
|
||||
DiffDialog
|
||||
} = await import('../../components/diff/DiffDialog.js');
|
||||
return <DiffDialog messages={context.messages} onDone={onDone} />;
|
||||
};
|
||||
const { DiffDialog } = await import('../../components/diff/DiffDialog.js')
|
||||
return <DiffDialog messages={context.messages} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Doctor } from '../../screens/Doctor.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import React from 'react'
|
||||
import { Doctor } from '../../screens/Doctor.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = (onDone, _context, _args) => {
|
||||
return Promise.resolve(<Doctor onDone={onDone} />);
|
||||
};
|
||||
return Promise.resolve(<Doctor onDone={onDone} />)
|
||||
}
|
||||
|
||||
@@ -1,182 +1,183 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { type EffortValue, getDisplayedEffortLevel, getEffortEnvOverride, getEffortValueDescription, isEffortLevel, toPersistableEffort } from '../../utils/effort.js';
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js';
|
||||
const COMMON_HELP_ARGS = ['help', '-h', '--help'];
|
||||
import * as React from 'react'
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
type EffortValue,
|
||||
getDisplayedEffortLevel,
|
||||
getEffortEnvOverride,
|
||||
getEffortValueDescription,
|
||||
isEffortLevel,
|
||||
toPersistableEffort,
|
||||
} from '../../utils/effort.js'
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js'
|
||||
|
||||
const COMMON_HELP_ARGS = ['help', '-h', '--help']
|
||||
|
||||
type EffortCommandResult = {
|
||||
message: string;
|
||||
effortUpdate?: {
|
||||
value: EffortValue | undefined;
|
||||
};
|
||||
};
|
||||
message: string
|
||||
effortUpdate?: { value: EffortValue | undefined }
|
||||
}
|
||||
|
||||
function setEffortValue(effortValue: EffortValue): EffortCommandResult {
|
||||
const persistable = toPersistableEffort(effortValue);
|
||||
const persistable = toPersistableEffort(effortValue)
|
||||
if (persistable !== undefined) {
|
||||
const result = updateSettingsForSource('userSettings', {
|
||||
effortLevel: persistable
|
||||
});
|
||||
effortLevel: persistable,
|
||||
})
|
||||
if (result.error) {
|
||||
return {
|
||||
message: `Failed to set effort level: ${result.error.message}`
|
||||
};
|
||||
message: `Failed to set effort level: ${result.error.message}`,
|
||||
}
|
||||
}
|
||||
}
|
||||
logEvent('tengu_effort_command', {
|
||||
effort: effortValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
effort:
|
||||
effortValue as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
// Env var wins at resolveAppliedEffort time. Only flag it when it actually
|
||||
// conflicts — if env matches what the user just asked for, the outcome is
|
||||
// the same, so "Set effort to X" is true and the note is noise.
|
||||
const envOverride = getEffortEnvOverride();
|
||||
const envOverride = getEffortEnvOverride()
|
||||
if (envOverride !== undefined && envOverride !== effortValue) {
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL;
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL
|
||||
if (persistable === undefined) {
|
||||
return {
|
||||
message: `Not applied: CLAUDE_CODE_EFFORT_LEVEL=${envRaw} overrides effort this session, and ${effortValue} is session-only (nothing saved)`,
|
||||
effortUpdate: {
|
||||
value: effortValue
|
||||
}
|
||||
};
|
||||
effortUpdate: { value: effortValue },
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: `CLAUDE_CODE_EFFORT_LEVEL=${envRaw} overrides this session — clear it and ${effortValue} takes over`,
|
||||
effortUpdate: {
|
||||
value: effortValue
|
||||
}
|
||||
};
|
||||
effortUpdate: { value: effortValue },
|
||||
}
|
||||
}
|
||||
const description = getEffortValueDescription(effortValue);
|
||||
const suffix = persistable !== undefined ? '' : ' (this session only)';
|
||||
|
||||
const description = getEffortValueDescription(effortValue)
|
||||
const suffix = persistable !== undefined ? '' : ' (this session only)'
|
||||
return {
|
||||
message: `Set effort level to ${effortValue}${suffix}: ${description}`,
|
||||
effortUpdate: {
|
||||
value: effortValue
|
||||
}
|
||||
};
|
||||
}
|
||||
export function showCurrentEffort(appStateEffort: EffortValue | undefined, model: string): EffortCommandResult {
|
||||
const envOverride = getEffortEnvOverride();
|
||||
const effectiveValue = envOverride === null ? undefined : envOverride ?? appStateEffort;
|
||||
if (effectiveValue === undefined) {
|
||||
const level = getDisplayedEffortLevel(model, appStateEffort);
|
||||
return {
|
||||
message: `Effort level: auto (currently ${level})`
|
||||
};
|
||||
effortUpdate: { value: effortValue },
|
||||
}
|
||||
const description = getEffortValueDescription(effectiveValue);
|
||||
return {
|
||||
message: `Current effort level: ${effectiveValue} (${description})`
|
||||
};
|
||||
}
|
||||
|
||||
export function showCurrentEffort(
|
||||
appStateEffort: EffortValue | undefined,
|
||||
model: string,
|
||||
): EffortCommandResult {
|
||||
const envOverride = getEffortEnvOverride()
|
||||
const effectiveValue =
|
||||
envOverride === null ? undefined : (envOverride ?? appStateEffort)
|
||||
if (effectiveValue === undefined) {
|
||||
const level = getDisplayedEffortLevel(model, appStateEffort)
|
||||
return { message: `Effort level: auto (currently ${level})` }
|
||||
}
|
||||
const description = getEffortValueDescription(effectiveValue)
|
||||
return {
|
||||
message: `Current effort level: ${effectiveValue} (${description})`,
|
||||
}
|
||||
}
|
||||
|
||||
function unsetEffortLevel(): EffortCommandResult {
|
||||
const result = updateSettingsForSource('userSettings', {
|
||||
effortLevel: undefined
|
||||
});
|
||||
effortLevel: undefined,
|
||||
})
|
||||
if (result.error) {
|
||||
return {
|
||||
message: `Failed to set effort level: ${result.error.message}`
|
||||
};
|
||||
message: `Failed to set effort level: ${result.error.message}`,
|
||||
}
|
||||
}
|
||||
logEvent('tengu_effort_command', {
|
||||
effort: 'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
effort:
|
||||
'auto' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
// env=auto/unset (null) matches what /effort auto asks for, so only warn
|
||||
// when env is pinning a specific level that will keep overriding.
|
||||
const envOverride = getEffortEnvOverride();
|
||||
const envOverride = getEffortEnvOverride()
|
||||
if (envOverride !== undefined && envOverride !== null) {
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL;
|
||||
const envRaw = process.env.CLAUDE_CODE_EFFORT_LEVEL
|
||||
return {
|
||||
message: `Cleared effort from settings, but CLAUDE_CODE_EFFORT_LEVEL=${envRaw} still controls this session`,
|
||||
effortUpdate: {
|
||||
value: undefined
|
||||
}
|
||||
};
|
||||
effortUpdate: { value: undefined },
|
||||
}
|
||||
}
|
||||
return {
|
||||
message: 'Effort level set to auto',
|
||||
effortUpdate: {
|
||||
value: undefined
|
||||
}
|
||||
};
|
||||
}
|
||||
export function executeEffort(args: string): EffortCommandResult {
|
||||
const normalized = args.toLowerCase();
|
||||
if (normalized === 'auto' || normalized === 'unset') {
|
||||
return unsetEffortLevel();
|
||||
effortUpdate: { value: undefined },
|
||||
}
|
||||
}
|
||||
|
||||
export function executeEffort(args: string): EffortCommandResult {
|
||||
const normalized = args.toLowerCase()
|
||||
if (normalized === 'auto' || normalized === 'unset') {
|
||||
return unsetEffortLevel()
|
||||
}
|
||||
|
||||
if (!isEffortLevel(normalized)) {
|
||||
return {
|
||||
message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, auto`
|
||||
};
|
||||
message: `Invalid argument: ${args}. Valid options are: low, medium, high, max, auto`,
|
||||
}
|
||||
}
|
||||
return setEffortValue(normalized);
|
||||
|
||||
return setEffortValue(normalized)
|
||||
}
|
||||
function ShowCurrentEffort(t0) {
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const effortValue = useAppState(_temp);
|
||||
const model = useMainLoopModel();
|
||||
const {
|
||||
message
|
||||
} = showCurrentEffort(effortValue, model);
|
||||
onDone(message);
|
||||
return null;
|
||||
|
||||
function ShowCurrentEffort({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (result: string) => void
|
||||
}): React.ReactNode {
|
||||
const effortValue = useAppState(s => s.effortValue)
|
||||
const model = useMainLoopModel()
|
||||
const { message } = showCurrentEffort(effortValue, model)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.effortValue;
|
||||
|
||||
function ApplyEffortAndClose({
|
||||
result,
|
||||
onDone,
|
||||
}: {
|
||||
result: EffortCommandResult
|
||||
onDone: (result: string) => void
|
||||
}): React.ReactNode {
|
||||
const setAppState = useSetAppState()
|
||||
const { effortUpdate, message } = result
|
||||
React.useEffect(() => {
|
||||
if (effortUpdate) {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
effortValue: effortUpdate.value,
|
||||
}))
|
||||
}
|
||||
onDone(message)
|
||||
}, [setAppState, effortUpdate, message, onDone])
|
||||
return null
|
||||
}
|
||||
function ApplyEffortAndClose(t0) {
|
||||
const $ = _c(6);
|
||||
const {
|
||||
result,
|
||||
onDone
|
||||
} = t0;
|
||||
const setAppState = useSetAppState();
|
||||
const {
|
||||
effortUpdate,
|
||||
message
|
||||
} = result;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== effortUpdate || $[1] !== message || $[2] !== onDone || $[3] !== setAppState) {
|
||||
t1 = () => {
|
||||
if (effortUpdate) {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
effortValue: effortUpdate.value
|
||||
}));
|
||||
}
|
||||
onDone(message);
|
||||
};
|
||||
t2 = [setAppState, effortUpdate, message, onDone];
|
||||
$[0] = effortUpdate;
|
||||
$[1] = message;
|
||||
$[2] = onDone;
|
||||
$[3] = setAppState;
|
||||
$[4] = t1;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
t2 = $[5];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
return null;
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
args = args?.trim() || '';
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
args = args?.trim() || ''
|
||||
|
||||
if (COMMON_HELP_ARGS.includes(args)) {
|
||||
onDone('Usage: /effort [low|medium|high|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- max: Maximum capability with deepest reasoning (Opus 4.6 only)\n- auto: Use the default effort level for your model');
|
||||
return;
|
||||
onDone(
|
||||
'Usage: /effort [low|medium|high|max|auto]\n\nEffort levels:\n- low: Quick, straightforward implementation\n- medium: Balanced approach with standard testing\n- high: Comprehensive implementation with extensive testing\n- max: Maximum capability with deepest reasoning (Opus 4.6 only)\n- auto: Use the default effort level for your model',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (!args || args === 'current' || args === 'status') {
|
||||
return <ShowCurrentEffort onDone={onDone} />;
|
||||
return <ShowCurrentEffort onDone={onDone} />
|
||||
}
|
||||
const result = executeEffort(args);
|
||||
return <ApplyEffortAndClose result={result} onDone={onDone} />;
|
||||
|
||||
const result = executeEffort(args)
|
||||
return <ApplyEffortAndClose result={result} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,32 +1,44 @@
|
||||
import { feature } from 'bun:bundle';
|
||||
import { spawnSync } from 'child_process';
|
||||
import sample from 'lodash-es/sample.js';
|
||||
import * as React from 'react';
|
||||
import { ExitFlow } from '../../components/ExitFlow.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { isBgSession } from '../../utils/concurrentSessions.js';
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js';
|
||||
import { getCurrentWorktreeSession } from '../../utils/worktree.js';
|
||||
const GOODBYE_MESSAGES = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!'];
|
||||
import { feature } from 'bun:bundle'
|
||||
import { spawnSync } from 'child_process'
|
||||
import sample from 'lodash-es/sample.js'
|
||||
import * as React from 'react'
|
||||
import { ExitFlow } from '../../components/ExitFlow.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { isBgSession } from '../../utils/concurrentSessions.js'
|
||||
import { gracefulShutdown } from '../../utils/gracefulShutdown.js'
|
||||
import { getCurrentWorktreeSession } from '../../utils/worktree.js'
|
||||
|
||||
const GOODBYE_MESSAGES = ['Goodbye!', 'See ya!', 'Bye!', 'Catch you later!']
|
||||
|
||||
function getRandomGoodbyeMessage(): string {
|
||||
return sample(GOODBYE_MESSAGES) ?? 'Goodbye!';
|
||||
return sample(GOODBYE_MESSAGES) ?? 'Goodbye!'
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
// Inside a `claude --bg` tmux session: detach instead of kill. The REPL
|
||||
// keeps running; `claude attach` can reconnect. Covers /exit, /quit,
|
||||
// ctrl+c, ctrl+d — all funnel through here via REPL's handleExit.
|
||||
if (feature('BG_SESSIONS') && isBgSession()) {
|
||||
onDone();
|
||||
spawnSync('tmux', ['detach-client'], {
|
||||
stdio: 'ignore'
|
||||
});
|
||||
return null;
|
||||
onDone()
|
||||
spawnSync('tmux', ['detach-client'], { stdio: 'ignore' })
|
||||
return null
|
||||
}
|
||||
const showWorktree = getCurrentWorktreeSession() !== null;
|
||||
|
||||
const showWorktree = getCurrentWorktreeSession() !== null
|
||||
|
||||
if (showWorktree) {
|
||||
return <ExitFlow showWorktree={showWorktree} onDone={onDone} onCancel={() => onDone()} />;
|
||||
return (
|
||||
<ExitFlow
|
||||
showWorktree={showWorktree}
|
||||
onDone={onDone}
|
||||
onCancel={() => onDone()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onDone(getRandomGoodbyeMessage());
|
||||
await gracefulShutdown(0, 'prompt_input_exit');
|
||||
return null;
|
||||
|
||||
onDone(getRandomGoodbyeMessage())
|
||||
await gracefulShutdown(0, 'prompt_input_exit')
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,90 +1,121 @@
|
||||
import { join } from 'path';
|
||||
import React from 'react';
|
||||
import { ExportDialog } from '../../components/ExportDialog.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import { getCwd } from '../../utils/cwd.js';
|
||||
import { renderMessagesToPlainText } from '../../utils/exportRenderer.js';
|
||||
import { writeFileSync_DEPRECATED } from '../../utils/slowOperations.js';
|
||||
import { join } from 'path'
|
||||
import React from 'react'
|
||||
import { ExportDialog } from '../../components/ExportDialog.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
import { getCwd } from '../../utils/cwd.js'
|
||||
import { renderMessagesToPlainText } from '../../utils/exportRenderer.js'
|
||||
import { writeFileSync_DEPRECATED } from '../../utils/slowOperations.js'
|
||||
|
||||
function formatTimestamp(date: Date): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}-${hours}${minutes}${seconds}`;
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}-${hours}${minutes}${seconds}`
|
||||
}
|
||||
|
||||
export function extractFirstPrompt(messages: Message[]): string {
|
||||
const firstUserMessage = messages.find(msg => msg.type === 'user');
|
||||
const firstUserMessage = messages.find(msg => msg.type === 'user')
|
||||
|
||||
if (!firstUserMessage || firstUserMessage.type !== 'user') {
|
||||
return '';
|
||||
return ''
|
||||
}
|
||||
const content = firstUserMessage.message?.content;
|
||||
let result = '';
|
||||
|
||||
const content = firstUserMessage.message?.content
|
||||
let result = ''
|
||||
|
||||
if (typeof content === 'string') {
|
||||
result = content.trim();
|
||||
result = content.trim()
|
||||
} else if (Array.isArray(content)) {
|
||||
const textContent = content.find(item => item.type === 'text');
|
||||
const textContent = content.find(item => item.type === 'text')
|
||||
if (textContent && 'text' in textContent) {
|
||||
result = textContent.text.trim();
|
||||
result = textContent.text.trim()
|
||||
}
|
||||
}
|
||||
|
||||
// Take first line only and limit length
|
||||
result = result.split('\n')[0] || '';
|
||||
result = result.split('\n')[0] || ''
|
||||
if (result.length > 50) {
|
||||
result = result.substring(0, 49) + '…';
|
||||
result = result.substring(0, 49) + '…'
|
||||
}
|
||||
return result;
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function sanitizeFilename(text: string): string {
|
||||
// Replace special characters with hyphens
|
||||
return text.toLowerCase().replace(/[^a-z0-9\s-]/g, '') // Remove special chars
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
|
||||
return text
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '') // Remove special chars
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Replace multiple hyphens with single
|
||||
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
||||
}
|
||||
async function exportWithReactRenderer(context: ToolUseContext): Promise<string> {
|
||||
const tools = context.options.tools || [];
|
||||
return renderMessagesToPlainText(context.messages, tools);
|
||||
|
||||
async function exportWithReactRenderer(
|
||||
context: ToolUseContext,
|
||||
): Promise<string> {
|
||||
const tools = context.options.tools || []
|
||||
return renderMessagesToPlainText(context.messages, tools)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext, args: string): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
// Render the conversation content
|
||||
const content = await exportWithReactRenderer(context);
|
||||
const content = await exportWithReactRenderer(context)
|
||||
|
||||
// If args are provided, write directly to file and skip dialog
|
||||
const filename = args.trim();
|
||||
const filename = args.trim()
|
||||
if (filename) {
|
||||
const finalFilename = filename.endsWith('.txt') ? filename : filename.replace(/\.[^.]+$/, '') + '.txt';
|
||||
const filepath = join(getCwd(), finalFilename);
|
||||
const finalFilename = filename.endsWith('.txt')
|
||||
? filename
|
||||
: filename.replace(/\.[^.]+$/, '') + '.txt'
|
||||
const filepath = join(getCwd(), finalFilename)
|
||||
|
||||
try {
|
||||
writeFileSync_DEPRECATED(filepath, content, {
|
||||
encoding: 'utf-8',
|
||||
flush: true
|
||||
});
|
||||
onDone(`Conversation exported to: ${filepath}`);
|
||||
return null;
|
||||
flush: true,
|
||||
})
|
||||
onDone(`Conversation exported to: ${filepath}`)
|
||||
return null
|
||||
} catch (error) {
|
||||
onDone(`Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
return null;
|
||||
onDone(
|
||||
`Failed to export conversation: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Generate default filename from first prompt or timestamp
|
||||
const firstPrompt = extractFirstPrompt(context.messages);
|
||||
const timestamp = formatTimestamp(new Date());
|
||||
let defaultFilename: string;
|
||||
const firstPrompt = extractFirstPrompt(context.messages)
|
||||
const timestamp = formatTimestamp(new Date())
|
||||
|
||||
let defaultFilename: string
|
||||
if (firstPrompt) {
|
||||
const sanitized = sanitizeFilename(firstPrompt);
|
||||
defaultFilename = sanitized ? `${timestamp}-${sanitized}.txt` : `conversation-${timestamp}.txt`;
|
||||
const sanitized = sanitizeFilename(firstPrompt)
|
||||
defaultFilename = sanitized
|
||||
? `${timestamp}-${sanitized}.txt`
|
||||
: `conversation-${timestamp}.txt`
|
||||
} else {
|
||||
defaultFilename = `conversation-${timestamp}.txt`;
|
||||
defaultFilename = `conversation-${timestamp}.txt`
|
||||
}
|
||||
|
||||
// Return the dialog component when no args provided
|
||||
return <ExportDialog content={content} defaultFilename={defaultFilename} onDone={result => {
|
||||
onDone(result.message);
|
||||
}} />;
|
||||
return (
|
||||
<ExportDialog
|
||||
content={content}
|
||||
defaultFilename={defaultFilename}
|
||||
onDone={result => {
|
||||
onDone(result.message)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,29 @@
|
||||
import React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { Login } from '../login/login.js';
|
||||
import { runExtraUsage } from './extra-usage-core.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode | null> {
|
||||
const result = await runExtraUsage();
|
||||
import React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { Login } from '../login/login.js'
|
||||
import { runExtraUsage } from './extra-usage-core.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode | null> {
|
||||
const result = await runExtraUsage()
|
||||
|
||||
if (result.type === 'message') {
|
||||
onDone(result.value);
|
||||
return null;
|
||||
onDone(result.value)
|
||||
return null
|
||||
}
|
||||
return <Login startingMessage={'Starting new login following /extra-usage. Exit with Ctrl-C to use existing account.'} onDone={success => {
|
||||
context.onChangeAPIKey();
|
||||
onDone(success ? 'Login successful' : 'Login interrupted');
|
||||
}} />;
|
||||
|
||||
return (
|
||||
<Login
|
||||
startingMessage={
|
||||
'Starting new login following /extra-usage. Exit with Ctrl-C to use existing account.'
|
||||
}
|
||||
onDone={success => {
|
||||
context.onChangeAPIKey()
|
||||
onDone(success ? 'Login successful' : 'Login interrupted')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,268 +1,260 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { FastIcon, getFastIconString } from '../../components/FastIcon.js';
|
||||
import { Box, Link, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { type AppState, useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { clearFastModeCooldown, FAST_MODE_MODEL_DISPLAY, getFastModeModel, getFastModeRuntimeState, getFastModeUnavailableReason, isFastModeEnabled, isFastModeSupportedByModel, prefetchFastModeStatus } from '../../utils/fastMode.js';
|
||||
import { formatDuration } from '../../utils/format.js';
|
||||
import { formatModelPricing, getOpus46CostTier } from '../../utils/modelCost.js';
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js';
|
||||
function applyFastMode(enable: boolean, setAppState: (f: (prev: AppState) => AppState) => void): void {
|
||||
clearFastModeCooldown();
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import type {
|
||||
CommandResultDisplay,
|
||||
LocalJSXCommandContext,
|
||||
} from '../../commands.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { FastIcon, getFastIconString } from '../../components/FastIcon.js'
|
||||
import { Box, Link, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
type AppState,
|
||||
useAppState,
|
||||
useSetAppState,
|
||||
} from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
clearFastModeCooldown,
|
||||
FAST_MODE_MODEL_DISPLAY,
|
||||
getFastModeModel,
|
||||
getFastModeRuntimeState,
|
||||
getFastModeUnavailableReason,
|
||||
isFastModeEnabled,
|
||||
isFastModeSupportedByModel,
|
||||
prefetchFastModeStatus,
|
||||
} from '../../utils/fastMode.js'
|
||||
import { formatDuration } from '../../utils/format.js'
|
||||
import { formatModelPricing, getOpus46CostTier } from '../../utils/modelCost.js'
|
||||
import { updateSettingsForSource } from '../../utils/settings/settings.js'
|
||||
|
||||
function applyFastMode(
|
||||
enable: boolean,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): void {
|
||||
clearFastModeCooldown()
|
||||
updateSettingsForSource('userSettings', {
|
||||
fastMode: enable ? true : undefined
|
||||
});
|
||||
fastMode: enable ? true : undefined,
|
||||
})
|
||||
if (enable) {
|
||||
setAppState(prev => {
|
||||
// Only switch model if current model doesn't support fast mode
|
||||
const needsModelSwitch = !isFastModeSupportedByModel(prev.mainLoopModel);
|
||||
const needsModelSwitch = !isFastModeSupportedByModel(prev.mainLoopModel)
|
||||
return {
|
||||
...prev,
|
||||
...(needsModelSwitch ? {
|
||||
mainLoopModel: getFastModeModel(),
|
||||
mainLoopModelForSession: null
|
||||
} : {}),
|
||||
fastMode: true
|
||||
};
|
||||
});
|
||||
} else {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
fastMode: false
|
||||
}));
|
||||
}
|
||||
}
|
||||
export function FastModePicker(t0) {
|
||||
const $ = _c(30);
|
||||
const {
|
||||
onDone,
|
||||
unavailableReason
|
||||
} = t0;
|
||||
const model = useAppState(_temp);
|
||||
const initialFastMode = useAppState(_temp2);
|
||||
const setAppState = useSetAppState();
|
||||
const [enableFastMode, setEnableFastMode] = useState(initialFastMode ?? false);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getFastModeRuntimeState();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const runtimeState = t1;
|
||||
const isCooldown = runtimeState.status === "cooldown";
|
||||
const isUnavailable = unavailableReason !== null;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = formatModelPricing(getOpus46CostTier(true));
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const pricing = t2;
|
||||
let t3;
|
||||
if ($[2] !== enableFastMode || $[3] !== isUnavailable || $[4] !== model || $[5] !== onDone || $[6] !== setAppState) {
|
||||
t3 = function handleConfirm() {
|
||||
if (isUnavailable) {
|
||||
return;
|
||||
...(needsModelSwitch
|
||||
? { mainLoopModel: getFastModeModel(), mainLoopModelForSession: null }
|
||||
: {}),
|
||||
fastMode: true,
|
||||
}
|
||||
applyFastMode(enableFastMode, setAppState);
|
||||
logEvent("tengu_fast_mode_toggled", {
|
||||
enabled: enableFastMode,
|
||||
source: "picker" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
if (enableFastMode) {
|
||||
const fastIcon = getFastIconString(enableFastMode);
|
||||
const modelUpdated = !isFastModeSupportedByModel(model) ? ` · model set to ${FAST_MODE_MODEL_DISPLAY}` : "";
|
||||
onDone(`${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`);
|
||||
} else {
|
||||
setAppState(_temp3);
|
||||
onDone("Fast mode OFF");
|
||||
})
|
||||
} else {
|
||||
setAppState(prev => ({ ...prev, fastMode: false }))
|
||||
}
|
||||
}
|
||||
|
||||
export function FastModePicker({
|
||||
onDone,
|
||||
unavailableReason,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
unavailableReason: string | null
|
||||
}): React.ReactNode {
|
||||
const model = useAppState(s => s.mainLoopModel)
|
||||
const initialFastMode = useAppState(s => s.fastMode)
|
||||
const setAppState = useSetAppState()
|
||||
const [enableFastMode, setEnableFastMode] = useState(initialFastMode ?? false)
|
||||
const runtimeState = getFastModeRuntimeState()
|
||||
const isCooldown = runtimeState.status === 'cooldown'
|
||||
const isUnavailable = unavailableReason !== null
|
||||
const pricing = formatModelPricing(getOpus46CostTier(true))
|
||||
|
||||
function handleConfirm(): void {
|
||||
if (isUnavailable) return
|
||||
applyFastMode(enableFastMode, setAppState)
|
||||
logEvent('tengu_fast_mode_toggled', {
|
||||
enabled: enableFastMode,
|
||||
source:
|
||||
'picker' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
if (enableFastMode) {
|
||||
const fastIcon = getFastIconString(enableFastMode)
|
||||
const modelUpdated = !isFastModeSupportedByModel(model)
|
||||
? ` · model set to ${FAST_MODE_MODEL_DISPLAY}`
|
||||
: ''
|
||||
onDone(`${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`)
|
||||
} else {
|
||||
setAppState(prev => ({ ...prev, fastMode: false }))
|
||||
onDone(`Fast mode OFF`)
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancel(): void {
|
||||
if (isUnavailable) {
|
||||
// Ensure fast mode is off if the org has disabled it
|
||||
if (initialFastMode) {
|
||||
applyFastMode(false, setAppState)
|
||||
}
|
||||
};
|
||||
$[2] = enableFastMode;
|
||||
$[3] = isUnavailable;
|
||||
$[4] = model;
|
||||
$[5] = onDone;
|
||||
$[6] = setAppState;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
onDone('Fast mode OFF', { display: 'system' })
|
||||
return
|
||||
}
|
||||
const message = initialFastMode
|
||||
? `${getFastIconString()} Kept Fast mode ON`
|
||||
: `Kept Fast mode OFF`
|
||||
onDone(message, { display: 'system' })
|
||||
}
|
||||
const handleConfirm = t3;
|
||||
let t4;
|
||||
if ($[8] !== initialFastMode || $[9] !== isUnavailable || $[10] !== onDone || $[11] !== setAppState) {
|
||||
t4 = function handleCancel() {
|
||||
if (isUnavailable) {
|
||||
if (initialFastMode) {
|
||||
applyFastMode(false, setAppState);
|
||||
}
|
||||
onDone("Fast mode OFF", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
|
||||
function handleToggle(): void {
|
||||
if (isUnavailable) return
|
||||
setEnableFastMode(prev => !prev)
|
||||
}
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:yes': handleConfirm,
|
||||
'confirm:nextField': handleToggle,
|
||||
'confirm:next': handleToggle,
|
||||
'confirm:previous': handleToggle,
|
||||
'confirm:cycleMode': handleToggle,
|
||||
'confirm:toggle': handleToggle,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
const title = (
|
||||
<Text>
|
||||
<FastIcon cooldown={isCooldown} /> Fast mode (research preview)
|
||||
</Text>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={title}
|
||||
subtitle={`High-speed mode for ${FAST_MODE_MODEL_DISPLAY}. Billed as extra usage at a premium rate. Separate rate limits apply.`}
|
||||
onCancel={handleCancel}
|
||||
color="fastMode"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : isUnavailable ? (
|
||||
<Text>Esc to cancel</Text>
|
||||
) : (
|
||||
<Text>Tab to toggle · Enter to confirm · Esc to cancel</Text>
|
||||
)
|
||||
}
|
||||
const message = initialFastMode ? `${getFastIconString()} Kept Fast mode ON` : "Kept Fast mode OFF";
|
||||
onDone(message, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[8] = initialFastMode;
|
||||
$[9] = isUnavailable;
|
||||
$[10] = onDone;
|
||||
$[11] = setAppState;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
const handleCancel = t4;
|
||||
let t5;
|
||||
if ($[13] !== isUnavailable) {
|
||||
t5 = function handleToggle() {
|
||||
if (isUnavailable) {
|
||||
return;
|
||||
}
|
||||
setEnableFastMode(_temp4);
|
||||
};
|
||||
$[13] = isUnavailable;
|
||||
$[14] = t5;
|
||||
} else {
|
||||
t5 = $[14];
|
||||
}
|
||||
const handleToggle = t5;
|
||||
let t6;
|
||||
if ($[15] !== handleConfirm || $[16] !== handleToggle) {
|
||||
t6 = {
|
||||
"confirm:yes": handleConfirm,
|
||||
"confirm:nextField": handleToggle,
|
||||
"confirm:next": handleToggle,
|
||||
"confirm:previous": handleToggle,
|
||||
"confirm:cycleMode": handleToggle,
|
||||
"confirm:toggle": handleToggle
|
||||
};
|
||||
$[15] = handleConfirm;
|
||||
$[16] = handleToggle;
|
||||
$[17] = t6;
|
||||
} else {
|
||||
t6 = $[17];
|
||||
}
|
||||
let t7;
|
||||
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
useKeybindings(t6, t7);
|
||||
let t8;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Text><FastIcon cooldown={isCooldown} /> Fast mode (research preview)</Text>;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
const title = t8;
|
||||
let t9;
|
||||
if ($[20] !== isUnavailable) {
|
||||
t9 = exitState => exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : isUnavailable ? <Text>Esc to cancel</Text> : <Text>Tab to toggle · Enter to confirm · Esc to cancel</Text>;
|
||||
$[20] = isUnavailable;
|
||||
$[21] = t9;
|
||||
} else {
|
||||
t9 = $[21];
|
||||
}
|
||||
let t10;
|
||||
if ($[22] !== enableFastMode || $[23] !== unavailableReason) {
|
||||
t10 = unavailableReason ? <Box marginLeft={2}><Text color="error">{unavailableReason}</Text></Box> : <><Box flexDirection="column" gap={0} marginLeft={2}><Box flexDirection="row" gap={2}><Text bold={true}>Fast mode</Text><Text color={enableFastMode ? "fastMode" : undefined} bold={enableFastMode}>{enableFastMode ? "ON " : "OFF"}</Text><Text dimColor={true}>{pricing}</Text></Box></Box>{isCooldown && runtimeState.status === "cooldown" && <Box marginLeft={2}><Text color="warning">{runtimeState.reason === "overloaded" ? "Fast mode overloaded and is temporarily unavailable" : "You've hit your fast limit"}{" \xB7 resets in "}{formatDuration(runtimeState.resetAt - Date.now(), {
|
||||
hideTrailingZeros: true
|
||||
})}</Text></Box>}</>;
|
||||
$[22] = enableFastMode;
|
||||
$[23] = unavailableReason;
|
||||
$[24] = t10;
|
||||
} else {
|
||||
t10 = $[24];
|
||||
}
|
||||
let t11;
|
||||
if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text dimColor={true}>Learn more:{" "}<Link url="https://code.claude.com/docs/en/fast-mode">https://code.claude.com/docs/en/fast-mode</Link></Text>;
|
||||
$[25] = t11;
|
||||
} else {
|
||||
t11 = $[25];
|
||||
}
|
||||
let t12;
|
||||
if ($[26] !== handleCancel || $[27] !== t10 || $[28] !== t9) {
|
||||
t12 = <Dialog title={title} subtitle={`High-speed mode for ${FAST_MODE_MODEL_DISPLAY}. Billed as extra usage at a premium rate. Separate rate limits apply.`} onCancel={handleCancel} color="fastMode" inputGuide={t9}>{t10}{t11}</Dialog>;
|
||||
$[26] = handleCancel;
|
||||
$[27] = t10;
|
||||
$[28] = t9;
|
||||
$[29] = t12;
|
||||
} else {
|
||||
t12 = $[29];
|
||||
}
|
||||
return t12;
|
||||
>
|
||||
{unavailableReason ? (
|
||||
<Box marginLeft={2}>
|
||||
<Text color="error">{unavailableReason}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box flexDirection="column" gap={0} marginLeft={2}>
|
||||
<Box flexDirection="row" gap={2}>
|
||||
<Text bold>Fast mode</Text>
|
||||
<Text
|
||||
color={enableFastMode ? 'fastMode' : undefined}
|
||||
bold={enableFastMode}
|
||||
>
|
||||
{enableFastMode ? 'ON ' : 'OFF'}
|
||||
</Text>
|
||||
<Text dimColor>{pricing}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{isCooldown && runtimeState.status === 'cooldown' && (
|
||||
<Box marginLeft={2}>
|
||||
<Text color="warning">
|
||||
{runtimeState.reason === 'overloaded'
|
||||
? 'Fast mode overloaded and is temporarily unavailable'
|
||||
: "You've hit your fast limit"}
|
||||
{' · resets in '}
|
||||
{formatDuration(runtimeState.resetAt - Date.now(), {
|
||||
hideTrailingZeros: true,
|
||||
})}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Text dimColor>
|
||||
Learn more:{' '}
|
||||
<Link url="https://code.claude.com/docs/en/fast-mode">
|
||||
https://code.claude.com/docs/en/fast-mode
|
||||
</Link>
|
||||
</Text>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
function _temp4(prev_0) {
|
||||
return !prev_0;
|
||||
}
|
||||
function _temp3(prev) {
|
||||
return {
|
||||
...prev,
|
||||
fastMode: false
|
||||
};
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.fastMode;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mainLoopModel;
|
||||
}
|
||||
async function handleFastModeShortcut(enable: boolean, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void): Promise<string> {
|
||||
const unavailableReason = getFastModeUnavailableReason();
|
||||
|
||||
async function handleFastModeShortcut(
|
||||
enable: boolean,
|
||||
getAppState: () => AppState,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): Promise<string> {
|
||||
const unavailableReason = getFastModeUnavailableReason()
|
||||
if (unavailableReason) {
|
||||
return `Fast mode unavailable: ${unavailableReason}`;
|
||||
return `Fast mode unavailable: ${unavailableReason}`
|
||||
}
|
||||
const {
|
||||
mainLoopModel
|
||||
} = getAppState();
|
||||
applyFastMode(enable, setAppState);
|
||||
|
||||
const { mainLoopModel } = getAppState()
|
||||
applyFastMode(enable, setAppState)
|
||||
logEvent('tengu_fast_mode_toggled', {
|
||||
enabled: enable,
|
||||
source: 'shortcut' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
source:
|
||||
'shortcut' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
if (enable) {
|
||||
const fastIcon = getFastIconString(true);
|
||||
const modelUpdated = !isFastModeSupportedByModel(mainLoopModel) ? ` · model set to ${FAST_MODE_MODEL_DISPLAY}` : '';
|
||||
const pricing = formatModelPricing(getOpus46CostTier(true));
|
||||
return `${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`;
|
||||
const fastIcon = getFastIconString(true)
|
||||
const modelUpdated = !isFastModeSupportedByModel(mainLoopModel)
|
||||
? ` · model set to ${FAST_MODE_MODEL_DISPLAY}`
|
||||
: ''
|
||||
const pricing = formatModelPricing(getOpus46CostTier(true))
|
||||
return `${fastIcon} Fast mode ON${modelUpdated} · ${pricing}`
|
||||
} else {
|
||||
return `Fast mode OFF`;
|
||||
return `Fast mode OFF`
|
||||
}
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args?: string): Promise<React.ReactNode | null> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode | null> {
|
||||
if (!isFastModeEnabled()) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Fetch org fast mode status before showing the picker. We must know
|
||||
// whether the org has disabled fast mode before allowing any toggle.
|
||||
// If a startup prefetch is already in flight, this awaits it.
|
||||
await prefetchFastModeStatus();
|
||||
const arg = args?.trim().toLowerCase();
|
||||
await prefetchFastModeStatus()
|
||||
|
||||
const arg = args?.trim().toLowerCase()
|
||||
if (arg === 'on' || arg === 'off') {
|
||||
const result = await handleFastModeShortcut(arg === 'on', context.getAppState, context.setAppState);
|
||||
onDone(result);
|
||||
return null;
|
||||
const result = await handleFastModeShortcut(
|
||||
arg === 'on',
|
||||
context.getAppState,
|
||||
context.setAppState,
|
||||
)
|
||||
onDone(result)
|
||||
return null
|
||||
}
|
||||
const unavailableReason = getFastModeUnavailableReason();
|
||||
|
||||
const unavailableReason = getFastModeUnavailableReason()
|
||||
logEvent('tengu_fast_mode_picker_shown', {
|
||||
unavailable_reason: (unavailableReason ?? '') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return <FastModePicker onDone={onDone} unavailableReason={unavailableReason} />;
|
||||
unavailable_reason: (unavailableReason ??
|
||||
'') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return (
|
||||
<FastModePicker onDone={onDone} unavailableReason={unavailableReason} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,24 +1,50 @@
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Feedback } from '../../components/Feedback.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { Message } from '../../types/message.js';
|
||||
import * as React from 'react'
|
||||
import type {
|
||||
CommandResultDisplay,
|
||||
LocalJSXCommandContext,
|
||||
} from '../../commands.js'
|
||||
import { Feedback } from '../../components/Feedback.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import type { Message } from '../../types/message.js'
|
||||
|
||||
// Shared function to render the Feedback component
|
||||
export function renderFeedbackComponent(onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void, abortSignal: AbortSignal, messages: Message[], initialDescription: string = '', backgroundTasks: {
|
||||
[taskId: string]: {
|
||||
type: string;
|
||||
identity?: {
|
||||
agentId: string;
|
||||
};
|
||||
messages?: Message[];
|
||||
};
|
||||
} = {}): React.ReactNode {
|
||||
return <Feedback abortSignal={abortSignal} messages={messages} initialDescription={initialDescription} onDone={onDone} backgroundTasks={backgroundTasks} />;
|
||||
export function renderFeedbackComponent(
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void,
|
||||
abortSignal: AbortSignal,
|
||||
messages: Message[],
|
||||
initialDescription: string = '',
|
||||
backgroundTasks: {
|
||||
[taskId: string]: {
|
||||
type: string
|
||||
identity?: { agentId: string }
|
||||
messages?: Message[]
|
||||
}
|
||||
} = {},
|
||||
): React.ReactNode {
|
||||
return (
|
||||
<Feedback
|
||||
abortSignal={abortSignal}
|
||||
messages={messages}
|
||||
initialDescription={initialDescription}
|
||||
onDone={onDone}
|
||||
backgroundTasks={backgroundTasks}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args?: string): Promise<React.ReactNode> {
|
||||
const initialDescription = args || '';
|
||||
return renderFeedbackComponent(onDone, context.abortController.signal, context.messages, initialDescription);
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const initialDescription = args || ''
|
||||
return renderFeedbackComponent(
|
||||
onDone,
|
||||
context.abortController.signal,
|
||||
context.messages,
|
||||
initialDescription,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import * as React from 'react';
|
||||
import { HelpV2 } from '../../components/HelpV2/HelpV2.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
export const call: LocalJSXCommandCall = async (onDone, {
|
||||
options: {
|
||||
commands
|
||||
}
|
||||
}) => {
|
||||
return <HelpV2 commands={commands} onClose={onDone} />;
|
||||
};
|
||||
import * as React from 'react'
|
||||
import { HelpV2 } from '../../components/HelpV2/HelpV2.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (
|
||||
onDone,
|
||||
{ options: { commands } },
|
||||
) => {
|
||||
return <HelpV2 commands={commands} onClose={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import * as React from 'react';
|
||||
import { HooksConfigMenu } from '../../components/hooks/HooksConfigMenu.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { getTools } from '../../tools.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { HooksConfigMenu } from '../../components/hooks/HooksConfigMenu.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { getTools } from '../../tools.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
logEvent('tengu_hooks_command', {});
|
||||
const appState = context.getAppState();
|
||||
const permissionContext = appState.toolPermissionContext;
|
||||
const toolNames = getTools(permissionContext).map(tool => tool.name);
|
||||
return <HooksConfigMenu toolNames={toolNames} onExit={onDone} />;
|
||||
};
|
||||
logEvent('tengu_hooks_command', {})
|
||||
const appState = context.getAppState()
|
||||
const permissionContext = appState.toolPermissionContext
|
||||
const toolNames = getTools(permissionContext).map(tool => tool.name)
|
||||
return <HooksConfigMenu toolNames={toolNames} onExit={onDone} />
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,230 +1,152 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface ApiKeyStepProps {
|
||||
existingApiKey: string | null;
|
||||
useExistingKey: boolean;
|
||||
apiKeyOrOAuthToken: string;
|
||||
onApiKeyChange: (value: string) => void;
|
||||
onToggleUseExistingKey: (useExisting: boolean) => void;
|
||||
onSubmit: () => void;
|
||||
onCreateOAuthToken?: () => void;
|
||||
selectedOption?: 'existing' | 'new' | 'oauth';
|
||||
onSelectOption?: (option: 'existing' | 'new' | 'oauth') => void;
|
||||
existingApiKey: string | null
|
||||
useExistingKey: boolean
|
||||
apiKeyOrOAuthToken: string
|
||||
onApiKeyChange: (value: string) => void
|
||||
onToggleUseExistingKey: (useExisting: boolean) => void
|
||||
onSubmit: () => void
|
||||
onCreateOAuthToken?: () => void
|
||||
selectedOption?: 'existing' | 'new' | 'oauth'
|
||||
onSelectOption?: (option: 'existing' | 'new' | 'oauth') => void
|
||||
}
|
||||
export function ApiKeyStep(t0) {
|
||||
const $ = _c(55);
|
||||
const {
|
||||
existingApiKey,
|
||||
apiKeyOrOAuthToken,
|
||||
onApiKeyChange,
|
||||
onSubmit,
|
||||
onToggleUseExistingKey,
|
||||
|
||||
export function ApiKeyStep({
|
||||
existingApiKey,
|
||||
apiKeyOrOAuthToken,
|
||||
onApiKeyChange,
|
||||
onSubmit,
|
||||
onToggleUseExistingKey,
|
||||
onCreateOAuthToken,
|
||||
selectedOption = existingApiKey
|
||||
? 'existing'
|
||||
: onCreateOAuthToken
|
||||
? 'oauth'
|
||||
: 'new',
|
||||
onSelectOption,
|
||||
}: ApiKeyStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
|
||||
const handlePrevious = useCallback(() => {
|
||||
if (selectedOption === 'new' && onCreateOAuthToken) {
|
||||
// From 'new' go up to 'oauth'
|
||||
onSelectOption?.('oauth')
|
||||
} else if (selectedOption === 'oauth' && existingApiKey) {
|
||||
// From 'oauth' go up to 'existing' (only if it exists)
|
||||
onSelectOption?.('existing')
|
||||
onToggleUseExistingKey(true)
|
||||
}
|
||||
}, [
|
||||
selectedOption,
|
||||
onCreateOAuthToken,
|
||||
selectedOption: t1,
|
||||
onSelectOption
|
||||
} = t0;
|
||||
const selectedOption = t1 === undefined ? existingApiKey ? "existing" : onCreateOAuthToken ? "oauth" : "new" : t1;
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
let t2;
|
||||
if ($[0] !== existingApiKey || $[1] !== onCreateOAuthToken || $[2] !== onSelectOption || $[3] !== onToggleUseExistingKey || $[4] !== selectedOption) {
|
||||
t2 = () => {
|
||||
if (selectedOption === "new" && onCreateOAuthToken) {
|
||||
onSelectOption?.("oauth");
|
||||
} else {
|
||||
if (selectedOption === "oauth" && existingApiKey) {
|
||||
onSelectOption?.("existing");
|
||||
onToggleUseExistingKey(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
$[0] = existingApiKey;
|
||||
$[1] = onCreateOAuthToken;
|
||||
$[2] = onSelectOption;
|
||||
$[3] = onToggleUseExistingKey;
|
||||
$[4] = selectedOption;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const handlePrevious = t2;
|
||||
let t3;
|
||||
if ($[6] !== onCreateOAuthToken || $[7] !== onSelectOption || $[8] !== onToggleUseExistingKey || $[9] !== selectedOption) {
|
||||
t3 = () => {
|
||||
if (selectedOption === "existing") {
|
||||
onSelectOption?.(onCreateOAuthToken ? "oauth" : "new");
|
||||
onToggleUseExistingKey(false);
|
||||
} else {
|
||||
if (selectedOption === "oauth") {
|
||||
onSelectOption?.("new");
|
||||
}
|
||||
}
|
||||
};
|
||||
$[6] = onCreateOAuthToken;
|
||||
$[7] = onSelectOption;
|
||||
$[8] = onToggleUseExistingKey;
|
||||
$[9] = selectedOption;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
const handleNext = t3;
|
||||
let t4;
|
||||
if ($[11] !== onCreateOAuthToken || $[12] !== onSubmit || $[13] !== selectedOption) {
|
||||
t4 = () => {
|
||||
if (selectedOption === "oauth" && onCreateOAuthToken) {
|
||||
onCreateOAuthToken();
|
||||
} else {
|
||||
onSubmit();
|
||||
}
|
||||
};
|
||||
$[11] = onCreateOAuthToken;
|
||||
$[12] = onSubmit;
|
||||
$[13] = selectedOption;
|
||||
$[14] = t4;
|
||||
} else {
|
||||
t4 = $[14];
|
||||
}
|
||||
const handleConfirm = t4;
|
||||
const isTextInputVisible = selectedOption === "new";
|
||||
let t5;
|
||||
if ($[15] !== handleConfirm || $[16] !== handleNext || $[17] !== handlePrevious) {
|
||||
t5 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext,
|
||||
"confirm:yes": handleConfirm
|
||||
};
|
||||
$[15] = handleConfirm;
|
||||
$[16] = handleNext;
|
||||
$[17] = handlePrevious;
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
}
|
||||
const t6 = !isTextInputVisible;
|
||||
let t7;
|
||||
if ($[19] !== t6) {
|
||||
t7 = {
|
||||
context: "Confirmation",
|
||||
isActive: t6
|
||||
};
|
||||
$[19] = t6;
|
||||
$[20] = t7;
|
||||
} else {
|
||||
t7 = $[20];
|
||||
}
|
||||
useKeybindings(t5, t7);
|
||||
let t8;
|
||||
if ($[21] !== handleNext || $[22] !== handlePrevious) {
|
||||
t8 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext
|
||||
};
|
||||
$[21] = handleNext;
|
||||
$[22] = handlePrevious;
|
||||
$[23] = t8;
|
||||
} else {
|
||||
t8 = $[23];
|
||||
}
|
||||
let t9;
|
||||
if ($[24] !== isTextInputVisible) {
|
||||
t9 = {
|
||||
context: "Confirmation",
|
||||
isActive: isTextInputVisible
|
||||
};
|
||||
$[24] = isTextInputVisible;
|
||||
$[25] = t9;
|
||||
} else {
|
||||
t9 = $[25];
|
||||
}
|
||||
useKeybindings(t8, t9);
|
||||
let t10;
|
||||
if ($[26] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Choose API key</Text></Box>;
|
||||
$[26] = t10;
|
||||
} else {
|
||||
t10 = $[26];
|
||||
}
|
||||
let t11;
|
||||
if ($[27] !== existingApiKey || $[28] !== selectedOption || $[29] !== theme) {
|
||||
t11 = existingApiKey && <Box marginBottom={1}><Text>{selectedOption === "existing" ? color("success", theme)("> ") : " "}Use your existing Claude Code API key</Text></Box>;
|
||||
$[27] = existingApiKey;
|
||||
$[28] = selectedOption;
|
||||
$[29] = theme;
|
||||
$[30] = t11;
|
||||
} else {
|
||||
t11 = $[30];
|
||||
}
|
||||
let t12;
|
||||
if ($[31] !== onCreateOAuthToken || $[32] !== selectedOption || $[33] !== theme) {
|
||||
t12 = onCreateOAuthToken && <Box marginBottom={1}><Text>{selectedOption === "oauth" ? color("success", theme)("> ") : " "}Create a long-lived token with your Claude subscription</Text></Box>;
|
||||
$[31] = onCreateOAuthToken;
|
||||
$[32] = selectedOption;
|
||||
$[33] = theme;
|
||||
$[34] = t12;
|
||||
} else {
|
||||
t12 = $[34];
|
||||
}
|
||||
let t13;
|
||||
if ($[35] !== selectedOption || $[36] !== theme) {
|
||||
t13 = selectedOption === "new" ? color("success", theme)("> ") : " ";
|
||||
$[35] = selectedOption;
|
||||
$[36] = theme;
|
||||
$[37] = t13;
|
||||
} else {
|
||||
t13 = $[37];
|
||||
}
|
||||
let t14;
|
||||
if ($[38] !== t13) {
|
||||
t14 = <Box marginBottom={1}><Text>{t13}Enter a new API key</Text></Box>;
|
||||
$[38] = t13;
|
||||
$[39] = t14;
|
||||
} else {
|
||||
t14 = $[39];
|
||||
}
|
||||
let t15;
|
||||
if ($[40] !== apiKeyOrOAuthToken || $[41] !== cursorOffset || $[42] !== onApiKeyChange || $[43] !== onSubmit || $[44] !== selectedOption || $[45] !== terminalSize) {
|
||||
t15 = selectedOption === "new" && <TextInput value={apiKeyOrOAuthToken} onChange={onApiKeyChange} onSubmit={onSubmit} onPaste={onApiKeyChange} focus={true} placeholder={"sk-ant\u2026 (Create a new key at https://platform.claude.com/settings/keys)"} mask="*" columns={terminalSize.columns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} showCursor={true} />;
|
||||
$[40] = apiKeyOrOAuthToken;
|
||||
$[41] = cursorOffset;
|
||||
$[42] = onApiKeyChange;
|
||||
$[43] = onSubmit;
|
||||
$[44] = selectedOption;
|
||||
$[45] = terminalSize;
|
||||
$[46] = t15;
|
||||
} else {
|
||||
t15 = $[46];
|
||||
}
|
||||
let t16;
|
||||
if ($[47] !== t11 || $[48] !== t12 || $[49] !== t14 || $[50] !== t15) {
|
||||
t16 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t10}{t11}{t12}{t14}{t15}</Box>;
|
||||
$[47] = t11;
|
||||
$[48] = t12;
|
||||
$[49] = t14;
|
||||
$[50] = t15;
|
||||
$[51] = t16;
|
||||
} else {
|
||||
t16 = $[51];
|
||||
}
|
||||
let t17;
|
||||
if ($[52] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t17 = <Box marginLeft={3}><Text dimColor={true}>↑/↓ to select · Enter to continue</Text></Box>;
|
||||
$[52] = t17;
|
||||
} else {
|
||||
t17 = $[52];
|
||||
}
|
||||
let t18;
|
||||
if ($[53] !== t16) {
|
||||
t18 = <>{t16}{t17}</>;
|
||||
$[53] = t16;
|
||||
$[54] = t18;
|
||||
} else {
|
||||
t18 = $[54];
|
||||
}
|
||||
return t18;
|
||||
existingApiKey,
|
||||
onSelectOption,
|
||||
onToggleUseExistingKey,
|
||||
])
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
if (selectedOption === 'existing') {
|
||||
// From 'existing' go down to 'oauth' (if available) or 'new'
|
||||
onSelectOption?.(onCreateOAuthToken ? 'oauth' : 'new')
|
||||
onToggleUseExistingKey(false)
|
||||
} else if (selectedOption === 'oauth') {
|
||||
// From 'oauth' go down to 'new'
|
||||
onSelectOption?.('new')
|
||||
}
|
||||
}, [
|
||||
selectedOption,
|
||||
onCreateOAuthToken,
|
||||
onSelectOption,
|
||||
onToggleUseExistingKey,
|
||||
])
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (selectedOption === 'oauth' && onCreateOAuthToken) {
|
||||
onCreateOAuthToken()
|
||||
} else {
|
||||
onSubmit()
|
||||
}
|
||||
}, [selectedOption, onCreateOAuthToken, onSubmit])
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const isTextInputVisible = selectedOption === 'new'
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
'confirm:yes': handleConfirm,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !isTextInputVisible },
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: isTextInputVisible },
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Choose API key</Text>
|
||||
</Box>
|
||||
{existingApiKey && (
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'existing'
|
||||
? color('success', theme)('> ')
|
||||
: ' '}
|
||||
Use your existing Claude Code API key
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{onCreateOAuthToken && (
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'oauth'
|
||||
? color('success', theme)('> ')
|
||||
: ' '}
|
||||
Create a long-lived token with your Claude subscription
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{selectedOption === 'new' ? color('success', theme)('> ') : ' '}
|
||||
Enter a new API key
|
||||
</Text>
|
||||
</Box>
|
||||
{selectedOption === 'new' && (
|
||||
<TextInput
|
||||
value={apiKeyOrOAuthToken}
|
||||
onChange={onApiKeyChange}
|
||||
onSubmit={onSubmit}
|
||||
onPaste={onApiKeyChange}
|
||||
focus={true}
|
||||
placeholder="sk-ant… (Create a new key at https://platform.claude.com/settings/keys)"
|
||||
mask="*"
|
||||
columns={terminalSize.columns}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
showCursor={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>↑/↓ to select · Enter to continue</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,189 +1,106 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface CheckExistingSecretStepProps {
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
onToggleUseExistingSecret: (useExisting: boolean) => void;
|
||||
onSecretNameChange: (value: string) => void;
|
||||
onSubmit: () => void;
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
onToggleUseExistingSecret: (useExisting: boolean) => void
|
||||
onSecretNameChange: (value: string) => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
export function CheckExistingSecretStep(t0) {
|
||||
const $ = _c(42);
|
||||
const {
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
onToggleUseExistingSecret,
|
||||
onSecretNameChange,
|
||||
onSubmit
|
||||
} = t0;
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const terminalSize = useTerminalSize();
|
||||
const [theme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] !== onToggleUseExistingSecret) {
|
||||
t1 = () => onToggleUseExistingSecret(true);
|
||||
$[0] = onToggleUseExistingSecret;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const handlePrevious = t1;
|
||||
let t2;
|
||||
if ($[2] !== onToggleUseExistingSecret) {
|
||||
t2 = () => onToggleUseExistingSecret(false);
|
||||
$[2] = onToggleUseExistingSecret;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const handleNext = t2;
|
||||
let t3;
|
||||
if ($[4] !== handleNext || $[5] !== handlePrevious || $[6] !== onSubmit) {
|
||||
t3 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext,
|
||||
"confirm:yes": onSubmit
|
||||
};
|
||||
$[4] = handleNext;
|
||||
$[5] = handlePrevious;
|
||||
$[6] = onSubmit;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== useExistingSecret) {
|
||||
t4 = {
|
||||
context: "Confirmation",
|
||||
isActive: useExistingSecret
|
||||
};
|
||||
$[8] = useExistingSecret;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
useKeybindings(t3, t4);
|
||||
let t5;
|
||||
if ($[10] !== handleNext || $[11] !== handlePrevious) {
|
||||
t5 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext
|
||||
};
|
||||
$[10] = handleNext;
|
||||
$[11] = handlePrevious;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
const t6 = !useExistingSecret;
|
||||
let t7;
|
||||
if ($[13] !== t6) {
|
||||
t7 = {
|
||||
context: "Confirmation",
|
||||
isActive: t6
|
||||
};
|
||||
$[13] = t6;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
}
|
||||
useKeybindings(t5, t7);
|
||||
let t8;
|
||||
if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Setup API key secret</Text></Box>;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
let t9;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginBottom={1}><Text color="warning">ANTHROPIC_API_KEY already exists in repository secrets!</Text></Box>;
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
let t10;
|
||||
if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Box marginBottom={1}><Text>Would you like to:</Text></Box>;
|
||||
$[17] = t10;
|
||||
} else {
|
||||
t10 = $[17];
|
||||
}
|
||||
let t11;
|
||||
if ($[18] !== theme || $[19] !== useExistingSecret) {
|
||||
t11 = useExistingSecret ? color("success", theme)("> ") : " ";
|
||||
$[18] = theme;
|
||||
$[19] = useExistingSecret;
|
||||
$[20] = t11;
|
||||
} else {
|
||||
t11 = $[20];
|
||||
}
|
||||
let t12;
|
||||
if ($[21] !== t11) {
|
||||
t12 = <Box marginBottom={1}><Text>{t11}Use the existing API key</Text></Box>;
|
||||
$[21] = t11;
|
||||
$[22] = t12;
|
||||
} else {
|
||||
t12 = $[22];
|
||||
}
|
||||
let t13;
|
||||
if ($[23] !== theme || $[24] !== useExistingSecret) {
|
||||
t13 = !useExistingSecret ? color("success", theme)("> ") : " ";
|
||||
$[23] = theme;
|
||||
$[24] = useExistingSecret;
|
||||
$[25] = t13;
|
||||
} else {
|
||||
t13 = $[25];
|
||||
}
|
||||
let t14;
|
||||
if ($[26] !== t13) {
|
||||
t14 = <Box marginBottom={1}><Text>{t13}Create a new secret with a different name</Text></Box>;
|
||||
$[26] = t13;
|
||||
$[27] = t14;
|
||||
} else {
|
||||
t14 = $[27];
|
||||
}
|
||||
let t15;
|
||||
if ($[28] !== cursorOffset || $[29] !== onSecretNameChange || $[30] !== onSubmit || $[31] !== secretName || $[32] !== terminalSize || $[33] !== useExistingSecret) {
|
||||
t15 = !useExistingSecret && <><Box marginBottom={1}><Text>Enter new secret name (alphanumeric with underscores):</Text></Box><TextInput value={secretName} onChange={onSecretNameChange} onSubmit={onSubmit} focus={true} placeholder="e.g., CLAUDE_API_KEY" columns={terminalSize.columns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} showCursor={true} /></>;
|
||||
$[28] = cursorOffset;
|
||||
$[29] = onSecretNameChange;
|
||||
$[30] = onSubmit;
|
||||
$[31] = secretName;
|
||||
$[32] = terminalSize;
|
||||
$[33] = useExistingSecret;
|
||||
$[34] = t15;
|
||||
} else {
|
||||
t15 = $[34];
|
||||
}
|
||||
let t16;
|
||||
if ($[35] !== t12 || $[36] !== t14 || $[37] !== t15) {
|
||||
t16 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t8}{t9}{t10}{t12}{t14}{t15}</Box>;
|
||||
$[35] = t12;
|
||||
$[36] = t14;
|
||||
$[37] = t15;
|
||||
$[38] = t16;
|
||||
} else {
|
||||
t16 = $[38];
|
||||
}
|
||||
let t17;
|
||||
if ($[39] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t17 = <Box marginLeft={3}><Text dimColor={true}>↑/↓ to select · Enter to continue</Text></Box>;
|
||||
$[39] = t17;
|
||||
} else {
|
||||
t17 = $[39];
|
||||
}
|
||||
let t18;
|
||||
if ($[40] !== t16) {
|
||||
t18 = <>{t16}{t17}</>;
|
||||
$[40] = t16;
|
||||
$[41] = t18;
|
||||
} else {
|
||||
t18 = $[41];
|
||||
}
|
||||
return t18;
|
||||
|
||||
export function CheckExistingSecretStep({
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
onToggleUseExistingSecret,
|
||||
onSecretNameChange,
|
||||
onSubmit,
|
||||
}: CheckExistingSecretStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const terminalSize = useTerminalSize()
|
||||
const [theme] = useTheme()
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const handlePrevious = useCallback(
|
||||
() => onToggleUseExistingSecret(true),
|
||||
[onToggleUseExistingSecret],
|
||||
)
|
||||
const handleNext = useCallback(
|
||||
() => onToggleUseExistingSecret(false),
|
||||
[onToggleUseExistingSecret],
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
'confirm:yes': onSubmit,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: useExistingSecret },
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !useExistingSecret },
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Setup API key secret</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text color="warning">
|
||||
ANTHROPIC_API_KEY already exists in repository secrets!
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>Would you like to:</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{useExistingSecret ? color('success', theme)('> ') : ' '}
|
||||
Use the existing API key
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
{!useExistingSecret ? color('success', theme)('> ') : ' '}
|
||||
Create a new secret with a different name
|
||||
</Text>
|
||||
</Box>
|
||||
{!useExistingSecret && (
|
||||
<>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
Enter new secret name (alphanumeric with underscores):
|
||||
</Text>
|
||||
</Box>
|
||||
<TextInput
|
||||
value={secretName}
|
||||
onChange={onSecretNameChange}
|
||||
onSubmit={onSubmit}
|
||||
focus={true}
|
||||
placeholder="e.g., CLAUDE_API_KEY"
|
||||
columns={terminalSize.columns}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
showCursor={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>↑/↓ to select · Enter to continue</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Text } from '../../ink.js'
|
||||
|
||||
export function CheckGitHubStep() {
|
||||
const $ = _c(1);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = <Text>Checking GitHub CLI installation…</Text>;
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
return t0;
|
||||
return <Text>Checking GitHub CLI installation…</Text>
|
||||
}
|
||||
|
||||
@@ -1,210 +1,125 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybindings } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface ChooseRepoStepProps {
|
||||
currentRepo: string | null;
|
||||
useCurrentRepo: boolean;
|
||||
repoUrl: string;
|
||||
onRepoUrlChange: (value: string) => void;
|
||||
onToggleUseCurrentRepo: (useCurrentRepo: boolean) => void;
|
||||
onSubmit: () => void;
|
||||
currentRepo: string | null
|
||||
useCurrentRepo: boolean
|
||||
repoUrl: string
|
||||
onRepoUrlChange: (value: string) => void
|
||||
onToggleUseCurrentRepo: (useCurrentRepo: boolean) => void
|
||||
onSubmit: () => void
|
||||
}
|
||||
export function ChooseRepoStep(t0) {
|
||||
const $ = _c(49);
|
||||
const {
|
||||
currentRepo,
|
||||
useCurrentRepo,
|
||||
repoUrl,
|
||||
onRepoUrlChange,
|
||||
onSubmit,
|
||||
onToggleUseCurrentRepo
|
||||
} = t0;
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const [showEmptyError, setShowEmptyError] = useState(false);
|
||||
const terminalSize = useTerminalSize();
|
||||
const textInputColumns = terminalSize.columns;
|
||||
let t1;
|
||||
if ($[0] !== currentRepo || $[1] !== onSubmit || $[2] !== repoUrl || $[3] !== useCurrentRepo) {
|
||||
t1 = () => {
|
||||
const repoName = useCurrentRepo ? currentRepo : repoUrl;
|
||||
if (!repoName?.trim()) {
|
||||
setShowEmptyError(true);
|
||||
return;
|
||||
}
|
||||
onSubmit();
|
||||
};
|
||||
$[0] = currentRepo;
|
||||
$[1] = onSubmit;
|
||||
$[2] = repoUrl;
|
||||
$[3] = useCurrentRepo;
|
||||
$[4] = t1;
|
||||
} else {
|
||||
t1 = $[4];
|
||||
}
|
||||
const handleSubmit = t1;
|
||||
const isTextInputVisible = !useCurrentRepo || !currentRepo;
|
||||
let t2;
|
||||
if ($[5] !== onToggleUseCurrentRepo) {
|
||||
t2 = () => {
|
||||
onToggleUseCurrentRepo(true);
|
||||
setShowEmptyError(false);
|
||||
};
|
||||
$[5] = onToggleUseCurrentRepo;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t2 = $[6];
|
||||
}
|
||||
const handlePrevious = t2;
|
||||
let t3;
|
||||
if ($[7] !== onToggleUseCurrentRepo) {
|
||||
t3 = () => {
|
||||
onToggleUseCurrentRepo(false);
|
||||
setShowEmptyError(false);
|
||||
};
|
||||
$[7] = onToggleUseCurrentRepo;
|
||||
$[8] = t3;
|
||||
} else {
|
||||
t3 = $[8];
|
||||
}
|
||||
const handleNext = t3;
|
||||
let t4;
|
||||
if ($[9] !== handleNext || $[10] !== handlePrevious || $[11] !== handleSubmit) {
|
||||
t4 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext,
|
||||
"confirm:yes": handleSubmit
|
||||
};
|
||||
$[9] = handleNext;
|
||||
$[10] = handlePrevious;
|
||||
$[11] = handleSubmit;
|
||||
$[12] = t4;
|
||||
} else {
|
||||
t4 = $[12];
|
||||
}
|
||||
const t5 = !isTextInputVisible;
|
||||
let t6;
|
||||
if ($[13] !== t5) {
|
||||
t6 = {
|
||||
context: "Confirmation",
|
||||
isActive: t5
|
||||
};
|
||||
$[13] = t5;
|
||||
$[14] = t6;
|
||||
} else {
|
||||
t6 = $[14];
|
||||
}
|
||||
useKeybindings(t4, t6);
|
||||
let t7;
|
||||
if ($[15] !== handleNext || $[16] !== handlePrevious) {
|
||||
t7 = {
|
||||
"confirm:previous": handlePrevious,
|
||||
"confirm:next": handleNext
|
||||
};
|
||||
$[15] = handleNext;
|
||||
$[16] = handlePrevious;
|
||||
$[17] = t7;
|
||||
} else {
|
||||
t7 = $[17];
|
||||
}
|
||||
let t8;
|
||||
if ($[18] !== isTextInputVisible) {
|
||||
t8 = {
|
||||
context: "Confirmation",
|
||||
isActive: isTextInputVisible
|
||||
};
|
||||
$[18] = isTextInputVisible;
|
||||
$[19] = t8;
|
||||
} else {
|
||||
t8 = $[19];
|
||||
}
|
||||
useKeybindings(t7, t8);
|
||||
let t9;
|
||||
if ($[20] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Select GitHub repository</Text></Box>;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
let t10;
|
||||
if ($[21] !== currentRepo || $[22] !== useCurrentRepo) {
|
||||
t10 = currentRepo && <Box marginBottom={1}><Text bold={useCurrentRepo} color={useCurrentRepo ? "permission" : undefined}>{useCurrentRepo ? "> " : " "}Use current repository: {currentRepo}</Text></Box>;
|
||||
$[21] = currentRepo;
|
||||
$[22] = useCurrentRepo;
|
||||
$[23] = t10;
|
||||
} else {
|
||||
t10 = $[23];
|
||||
}
|
||||
const t11 = !useCurrentRepo || !currentRepo;
|
||||
const t12 = !useCurrentRepo || !currentRepo ? "permission" : undefined;
|
||||
const t13 = !useCurrentRepo || !currentRepo ? "> " : " ";
|
||||
const t14 = currentRepo ? "Enter a different repository" : "Enter repository";
|
||||
let t15;
|
||||
if ($[24] !== t11 || $[25] !== t12 || $[26] !== t13 || $[27] !== t14) {
|
||||
t15 = <Box marginBottom={1}><Text bold={t11} color={t12}>{t13}{t14}</Text></Box>;
|
||||
$[24] = t11;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
$[27] = t14;
|
||||
$[28] = t15;
|
||||
} else {
|
||||
t15 = $[28];
|
||||
}
|
||||
let t16;
|
||||
if ($[29] !== currentRepo || $[30] !== cursorOffset || $[31] !== handleSubmit || $[32] !== onRepoUrlChange || $[33] !== repoUrl || $[34] !== textInputColumns || $[35] !== useCurrentRepo) {
|
||||
t16 = (!useCurrentRepo || !currentRepo) && <Box marginLeft={2} marginBottom={1}><TextInput value={repoUrl} onChange={value => {
|
||||
onRepoUrlChange(value);
|
||||
setShowEmptyError(false);
|
||||
}} onSubmit={handleSubmit} focus={true} placeholder={"Enter a repo as owner/repo or https://github.com/owner/repo\u2026"} columns={textInputColumns} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} showCursor={true} /></Box>;
|
||||
$[29] = currentRepo;
|
||||
$[30] = cursorOffset;
|
||||
$[31] = handleSubmit;
|
||||
$[32] = onRepoUrlChange;
|
||||
$[33] = repoUrl;
|
||||
$[34] = textInputColumns;
|
||||
$[35] = useCurrentRepo;
|
||||
$[36] = t16;
|
||||
} else {
|
||||
t16 = $[36];
|
||||
}
|
||||
let t17;
|
||||
if ($[37] !== t10 || $[38] !== t15 || $[39] !== t16) {
|
||||
t17 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t9}{t10}{t15}{t16}</Box>;
|
||||
$[37] = t10;
|
||||
$[38] = t15;
|
||||
$[39] = t16;
|
||||
$[40] = t17;
|
||||
} else {
|
||||
t17 = $[40];
|
||||
}
|
||||
let t18;
|
||||
if ($[41] !== showEmptyError) {
|
||||
t18 = showEmptyError && <Box marginLeft={3} marginBottom={1}><Text color="error">Please enter a repository name to continue</Text></Box>;
|
||||
$[41] = showEmptyError;
|
||||
$[42] = t18;
|
||||
} else {
|
||||
t18 = $[42];
|
||||
}
|
||||
const t19 = currentRepo ? "\u2191/\u2193 to select \xB7 " : "";
|
||||
let t20;
|
||||
if ($[43] !== t19) {
|
||||
t20 = <Box marginLeft={3}><Text dimColor={true}>{t19}Enter to continue</Text></Box>;
|
||||
$[43] = t19;
|
||||
$[44] = t20;
|
||||
} else {
|
||||
t20 = $[44];
|
||||
}
|
||||
let t21;
|
||||
if ($[45] !== t17 || $[46] !== t18 || $[47] !== t20) {
|
||||
t21 = <>{t17}{t18}{t20}</>;
|
||||
$[45] = t17;
|
||||
$[46] = t18;
|
||||
$[47] = t20;
|
||||
$[48] = t21;
|
||||
} else {
|
||||
t21 = $[48];
|
||||
}
|
||||
return t21;
|
||||
|
||||
export function ChooseRepoStep({
|
||||
currentRepo,
|
||||
useCurrentRepo,
|
||||
repoUrl,
|
||||
onRepoUrlChange,
|
||||
onSubmit,
|
||||
onToggleUseCurrentRepo,
|
||||
}: ChooseRepoStepProps) {
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const [showEmptyError, setShowEmptyError] = useState(false)
|
||||
const terminalSize = useTerminalSize()
|
||||
const textInputColumns = terminalSize.columns
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
const repoName = useCurrentRepo ? currentRepo : repoUrl
|
||||
if (!repoName?.trim()) {
|
||||
setShowEmptyError(true)
|
||||
return
|
||||
}
|
||||
onSubmit()
|
||||
}, [useCurrentRepo, currentRepo, repoUrl, onSubmit])
|
||||
|
||||
// When the text input is visible, omit confirm:yes so bare 'y' passes
|
||||
// through to the input instead of submitting. TextInput's onSubmit handles
|
||||
// Enter. Keep the Confirmation context (not Settings) to avoid j/k bindings.
|
||||
const isTextInputVisible = !useCurrentRepo || !currentRepo
|
||||
const handlePrevious = useCallback(() => {
|
||||
onToggleUseCurrentRepo(true)
|
||||
setShowEmptyError(false)
|
||||
}, [onToggleUseCurrentRepo])
|
||||
const handleNext = useCallback(() => {
|
||||
onToggleUseCurrentRepo(false)
|
||||
setShowEmptyError(false)
|
||||
}, [onToggleUseCurrentRepo])
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
'confirm:yes': handleSubmit,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: !isTextInputVisible },
|
||||
)
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:previous': handlePrevious,
|
||||
'confirm:next': handleNext,
|
||||
},
|
||||
{ context: 'Confirmation', isActive: isTextInputVisible },
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Select GitHub repository</Text>
|
||||
</Box>
|
||||
{currentRepo && (
|
||||
<Box marginBottom={1}>
|
||||
<Text
|
||||
bold={useCurrentRepo}
|
||||
color={useCurrentRepo ? 'permission' : undefined}
|
||||
>
|
||||
{useCurrentRepo ? '> ' : ' '}
|
||||
Use current repository: {currentRepo}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginBottom={1}>
|
||||
<Text
|
||||
bold={!useCurrentRepo || !currentRepo}
|
||||
color={!useCurrentRepo || !currentRepo ? 'permission' : undefined}
|
||||
>
|
||||
{!useCurrentRepo || !currentRepo ? '> ' : ' '}
|
||||
{currentRepo ? 'Enter a different repository' : 'Enter repository'}
|
||||
</Text>
|
||||
</Box>
|
||||
{(!useCurrentRepo || !currentRepo) && (
|
||||
<Box marginLeft={2} marginBottom={1}>
|
||||
<TextInput
|
||||
value={repoUrl}
|
||||
onChange={value => {
|
||||
onRepoUrlChange(value)
|
||||
setShowEmptyError(false)
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
focus={true}
|
||||
placeholder="Enter a repo as owner/repo or https://github.com/owner/repo…"
|
||||
columns={textInputColumns}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
showCursor={true}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
{showEmptyError && (
|
||||
<Box marginLeft={3} marginBottom={1}>
|
||||
<Text color="error">Please enter a repository name to continue</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>
|
||||
{currentRepo ? '↑/↓ to select · ' : ''}Enter to continue
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,64 +1,78 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { Workflow } from './types.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { Workflow } from './types.js'
|
||||
|
||||
interface CreatingStepProps {
|
||||
currentWorkflowInstallStep: number;
|
||||
secretExists: boolean;
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
skipWorkflow?: boolean;
|
||||
selectedWorkflows: Workflow[];
|
||||
currentWorkflowInstallStep: number
|
||||
secretExists: boolean
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
skipWorkflow?: boolean
|
||||
selectedWorkflows: Workflow[]
|
||||
}
|
||||
export function CreatingStep(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
currentWorkflowInstallStep,
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow: t1,
|
||||
selectedWorkflows
|
||||
} = t0;
|
||||
const skipWorkflow = t1 === undefined ? false : t1;
|
||||
let t2;
|
||||
if ($[0] !== secretExists || $[1] !== secretName || $[2] !== selectedWorkflows || $[3] !== skipWorkflow || $[4] !== useExistingSecret) {
|
||||
t2 = skipWorkflow ? ["Getting repository information", secretExists && useExistingSecret ? "Using existing API key secret" : `Setting up ${secretName} secret`] : ["Getting repository information", "Creating branch", selectedWorkflows.length > 1 ? "Creating workflow files" : "Creating workflow file", secretExists && useExistingSecret ? "Using existing API key secret" : `Setting up ${secretName} secret`, "Opening pull request page"];
|
||||
$[0] = secretExists;
|
||||
$[1] = secretName;
|
||||
$[2] = selectedWorkflows;
|
||||
$[3] = skipWorkflow;
|
||||
$[4] = useExistingSecret;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
}
|
||||
const progressSteps = t2;
|
||||
let t3;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Create GitHub Actions workflow</Text></Box>;
|
||||
$[6] = t3;
|
||||
} else {
|
||||
t3 = $[6];
|
||||
}
|
||||
let t4;
|
||||
if ($[7] !== currentWorkflowInstallStep || $[8] !== progressSteps) {
|
||||
t4 = <><Box flexDirection="column" borderStyle="round" paddingX={1}>{t3}{progressSteps.map((stepText, index) => {
|
||||
let status = "pending";
|
||||
|
||||
export function CreatingStep({
|
||||
currentWorkflowInstallStep,
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow = false,
|
||||
selectedWorkflows,
|
||||
}: CreatingStepProps) {
|
||||
const progressSteps = skipWorkflow
|
||||
? [
|
||||
'Getting repository information',
|
||||
secretExists && useExistingSecret
|
||||
? 'Using existing API key secret'
|
||||
: `Setting up ${secretName} secret`,
|
||||
]
|
||||
: [
|
||||
'Getting repository information',
|
||||
'Creating branch',
|
||||
selectedWorkflows.length > 1
|
||||
? 'Creating workflow files'
|
||||
: 'Creating workflow file',
|
||||
secretExists && useExistingSecret
|
||||
? 'Using existing API key secret'
|
||||
: `Setting up ${secretName} secret`,
|
||||
'Opening pull request page',
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Create GitHub Actions workflow</Text>
|
||||
</Box>
|
||||
{progressSteps.map((stepText, index) => {
|
||||
let status: 'completed' | 'in-progress' | 'pending' = 'pending'
|
||||
|
||||
if (index < currentWorkflowInstallStep) {
|
||||
status = "completed";
|
||||
} else {
|
||||
if (index === currentWorkflowInstallStep) {
|
||||
status = "in-progress";
|
||||
}
|
||||
status = 'completed'
|
||||
} else if (index === currentWorkflowInstallStep) {
|
||||
status = 'in-progress'
|
||||
}
|
||||
return <Box key={index}><Text color={status === "completed" ? "success" : status === "in-progress" ? "warning" : undefined}>{status === "completed" ? "\u2713 " : ""}{stepText}{status === "in-progress" ? "\u2026" : ""}</Text></Box>;
|
||||
})}</Box></>;
|
||||
$[7] = currentWorkflowInstallStep;
|
||||
$[8] = progressSteps;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
return t4;
|
||||
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text
|
||||
color={
|
||||
status === 'completed'
|
||||
? 'success'
|
||||
: status === 'in-progress'
|
||||
? 'warning'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{status === 'completed' ? '✓ ' : ''}
|
||||
{stepText}
|
||||
{status === 'in-progress' ? '…' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,84 +1,51 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
interface ErrorStepProps {
|
||||
error: string | undefined;
|
||||
errorReason?: string;
|
||||
errorInstructions?: string[];
|
||||
error: string | undefined
|
||||
errorReason?: string
|
||||
errorInstructions?: string[]
|
||||
}
|
||||
export function ErrorStep(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
error,
|
||||
errorReason,
|
||||
errorInstructions
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== error) {
|
||||
t2 = <Text color="error">Error: {error}</Text>;
|
||||
$[1] = error;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== errorReason) {
|
||||
t3 = errorReason && <Box marginTop={1}><Text dimColor={true}>Reason: {errorReason}</Text></Box>;
|
||||
$[3] = errorReason;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== errorInstructions) {
|
||||
t4 = errorInstructions && errorInstructions.length > 0 && <Box flexDirection="column" marginTop={1}><Text dimColor={true}>How to fix:</Text>{errorInstructions.map(_temp)}</Box>;
|
||||
$[5] = errorInstructions;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box marginTop={1}><Text dimColor={true}>For manual setup instructions, see:{" "}<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text></Text></Box>;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== t2 || $[9] !== t3 || $[10] !== t4) {
|
||||
t6 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t1}{t2}{t3}{t4}{t5}</Box>;
|
||||
$[8] = t2;
|
||||
$[9] = t3;
|
||||
$[10] = t4;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Box marginLeft={3}><Text dimColor={true}>Press any key to exit</Text></Box>;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== t6) {
|
||||
t8 = <>{t6}{t7}</>;
|
||||
$[13] = t6;
|
||||
$[14] = t8;
|
||||
} else {
|
||||
t8 = $[14];
|
||||
}
|
||||
return t8;
|
||||
}
|
||||
function _temp(instruction, index) {
|
||||
return <Box key={index} marginLeft={2}><Text dimColor={true}>• </Text><Text>{instruction}</Text></Box>;
|
||||
|
||||
export function ErrorStep({
|
||||
error,
|
||||
errorReason,
|
||||
errorInstructions,
|
||||
}: ErrorStepProps) {
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
</Box>
|
||||
<Text color="error">Error: {error}</Text>
|
||||
{errorReason && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Reason: {errorReason}</Text>
|
||||
</Box>
|
||||
)}
|
||||
{errorInstructions && errorInstructions.length > 0 && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
<Text dimColor>How to fix:</Text>
|
||||
{errorInstructions.map((instruction, index) => (
|
||||
<Box key={index} marginLeft={2}>
|
||||
<Text dimColor>• </Text>
|
||||
<Text>{instruction}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
For manual setup instructions, see:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>Press any key to exit</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,102 +1,70 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Select } from 'src/components/CustomSelect/index.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Select } from 'src/components/CustomSelect/index.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
interface ExistingWorkflowStepProps {
|
||||
repoName: string;
|
||||
onSelectAction: (action: 'update' | 'skip' | 'exit') => void;
|
||||
repoName: string
|
||||
onSelectAction: (action: 'update' | 'skip' | 'exit') => void
|
||||
}
|
||||
export function ExistingWorkflowStep(t0) {
|
||||
const $ = _c(16);
|
||||
const {
|
||||
repoName,
|
||||
onSelectAction
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = [{
|
||||
label: "Update workflow file with latest version",
|
||||
value: "update"
|
||||
}, {
|
||||
label: "Skip workflow update (configure secrets only)",
|
||||
value: "skip"
|
||||
}, {
|
||||
label: "Exit without making changes",
|
||||
value: "exit"
|
||||
}];
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
|
||||
export function ExistingWorkflowStep({
|
||||
repoName,
|
||||
onSelectAction,
|
||||
}: ExistingWorkflowStepProps) {
|
||||
const options = [
|
||||
{
|
||||
label: 'Update workflow file with latest version',
|
||||
value: 'update',
|
||||
},
|
||||
{
|
||||
label: 'Skip workflow update (configure secrets only)',
|
||||
value: 'skip',
|
||||
},
|
||||
{
|
||||
label: 'Exit without making changes',
|
||||
value: 'exit',
|
||||
},
|
||||
]
|
||||
|
||||
const handleSelect = (value: string) => {
|
||||
onSelectAction(value as 'update' | 'skip' | 'exit')
|
||||
}
|
||||
const options = t1;
|
||||
let t2;
|
||||
if ($[1] !== onSelectAction) {
|
||||
t2 = value => {
|
||||
onSelectAction(value as 'update' | 'skip' | 'exit');
|
||||
};
|
||||
$[1] = onSelectAction;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
|
||||
const handleCancel = () => {
|
||||
onSelectAction('exit')
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[3] !== onSelectAction) {
|
||||
t3 = () => {
|
||||
onSelectAction("exit");
|
||||
};
|
||||
$[3] = onSelectAction;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
const handleCancel = t3;
|
||||
let t4;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Text bold={true}>Existing Workflow Found</Text>;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== repoName) {
|
||||
t5 = <Box flexDirection="column" marginBottom={1}>{t4}<Text dimColor={true}>Repository: {repoName}</Text></Box>;
|
||||
$[6] = repoName;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box flexDirection="column" marginBottom={1}><Text>A Claude workflow file already exists at{" "}<Text color="claude">.github/workflows/claude.yml</Text></Text><Text dimColor={true}>What would you like to do?</Text></Box>;
|
||||
$[8] = t6;
|
||||
} else {
|
||||
t6 = $[8];
|
||||
}
|
||||
let t7;
|
||||
if ($[9] !== handleCancel || $[10] !== handleSelect) {
|
||||
t7 = <Box flexDirection="column"><Select options={options} onChange={handleSelect} onCancel={handleCancel} /></Box>;
|
||||
$[9] = handleCancel;
|
||||
$[10] = handleSelect;
|
||||
$[11] = t7;
|
||||
} else {
|
||||
t7 = $[11];
|
||||
}
|
||||
let t8;
|
||||
if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box marginTop={1}><Text dimColor={true}>View the latest workflow template at:{" "}<Text color="claude">https://github.com/anthropics/claude-code-action/blob/main/examples/claude.yml</Text></Text></Box>;
|
||||
$[12] = t8;
|
||||
} else {
|
||||
t8 = $[12];
|
||||
}
|
||||
let t9;
|
||||
if ($[13] !== t5 || $[14] !== t7) {
|
||||
t9 = <Box flexDirection="column" borderStyle="round" borderDimColor={true} paddingX={1}>{t5}{t6}{t7}{t8}</Box>;
|
||||
$[13] = t5;
|
||||
$[14] = t7;
|
||||
$[15] = t9;
|
||||
} else {
|
||||
t9 = $[15];
|
||||
}
|
||||
return t9;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Existing Workflow Found</Text>
|
||||
<Text dimColor>Repository: {repoName}</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text>
|
||||
A Claude workflow file already exists at{' '}
|
||||
<Text color="claude">.github/workflows/claude.yml</Text>
|
||||
</Text>
|
||||
<Text dimColor>What would you like to do?</Text>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
View the latest workflow template at:{' '}
|
||||
<Text color="claude">
|
||||
https://github.com/anthropics/claude-code-action/blob/main/examples/claude.yml
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,93 +1,53 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
|
||||
interface InstallAppStepProps {
|
||||
repoUrl: string;
|
||||
onSubmit: () => void;
|
||||
repoUrl: string
|
||||
onSubmit: () => void
|
||||
}
|
||||
export function InstallAppStep(t0) {
|
||||
const $ = _c(12);
|
||||
const {
|
||||
repoUrl,
|
||||
onSubmit
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
useKeybinding("confirm:yes", onSubmit, t1);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install the Claude GitHub App</Text></Box>;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box marginBottom={1}><Text>Opening browser to install the Claude GitHub App…</Text></Box>;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
let t4;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginBottom={1}><Text>If your browser doesn't open automatically, visit:</Text></Box>;
|
||||
$[3] = t4;
|
||||
} else {
|
||||
t4 = $[3];
|
||||
}
|
||||
let t5;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box marginBottom={1}><Text underline={true}>https://github.com/apps/claude</Text></Box>;
|
||||
$[4] = t5;
|
||||
} else {
|
||||
t5 = $[4];
|
||||
}
|
||||
let t6;
|
||||
if ($[5] !== repoUrl) {
|
||||
t6 = <Box marginBottom={1}><Text>Please install the app for repository: <Text bold={true}>{repoUrl}</Text></Text></Box>;
|
||||
$[5] = repoUrl;
|
||||
$[6] = t6;
|
||||
} else {
|
||||
t6 = $[6];
|
||||
}
|
||||
let t7;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Box marginBottom={1}><Text dimColor={true}>Important: Make sure to grant access to this specific repository</Text></Box>;
|
||||
$[7] = t7;
|
||||
} else {
|
||||
t7 = $[7];
|
||||
}
|
||||
let t8;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box><Text bold={true} color="permission">Press Enter once you've installed the app{figures.ellipsis}</Text></Box>;
|
||||
$[8] = t8;
|
||||
} else {
|
||||
t8 = $[8];
|
||||
}
|
||||
let t9;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginTop={1}><Text dimColor={true}>Having trouble? See manual setup instructions at:{" "}<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text></Text></Box>;
|
||||
$[9] = t9;
|
||||
} else {
|
||||
t9 = $[9];
|
||||
}
|
||||
let t10;
|
||||
if ($[10] !== t6) {
|
||||
t10 = <Box flexDirection="column" borderStyle="round" borderDimColor={true} paddingX={1}>{t2}{t3}{t4}{t5}{t6}{t7}{t8}{t9}</Box>;
|
||||
$[10] = t6;
|
||||
$[11] = t10;
|
||||
} else {
|
||||
t10 = $[11];
|
||||
}
|
||||
return t10;
|
||||
|
||||
export function InstallAppStep({ repoUrl, onSubmit }: InstallAppStepProps) {
|
||||
// Enter to submit
|
||||
useKeybinding('confirm:yes', onSubmit, { context: 'Confirmation' })
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle="round" borderDimColor paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install the Claude GitHub App</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>Opening browser to install the Claude GitHub App…</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>If your browser doesn't open automatically, visit:</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text underline>https://github.com/apps/claude</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text>
|
||||
Please install the app for repository: <Text bold>{repoUrl}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginBottom={1}>
|
||||
<Text dimColor>
|
||||
Important: Make sure to grant access to this specific repository
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text bold color="permission">
|
||||
Press Enter once you've installed the app{figures.ellipsis}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
Having trouble? See manual setup instructions at:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,275 +1,343 @@
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Link, Text } from '../../ink.js';
|
||||
import { OAuthService } from '../../services/oauth/index.js';
|
||||
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Link, Text } from '../../ink.js'
|
||||
import { OAuthService } from '../../services/oauth/index.js'
|
||||
import { saveOAuthTokensIfNeeded } from '../../utils/auth.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
|
||||
interface OAuthFlowStepProps {
|
||||
onSuccess: (token: string) => void;
|
||||
onCancel: () => void;
|
||||
onSuccess: (token: string) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
type OAuthStatus = {
|
||||
state: 'starting';
|
||||
} | {
|
||||
state: 'waiting_for_login';
|
||||
url: string;
|
||||
} | {
|
||||
state: 'processing';
|
||||
} | {
|
||||
state: 'success';
|
||||
token: string;
|
||||
} | {
|
||||
state: 'error';
|
||||
message: string;
|
||||
toRetry?: OAuthStatus;
|
||||
} | {
|
||||
state: 'about_to_retry';
|
||||
nextState: OAuthStatus;
|
||||
};
|
||||
const PASTE_HERE_MSG = 'Paste code here if prompted > ';
|
||||
|
||||
type OAuthStatus =
|
||||
| { state: 'starting' }
|
||||
| { state: 'waiting_for_login'; url: string }
|
||||
| { state: 'processing' }
|
||||
| { state: 'success'; token: string }
|
||||
| { state: 'error'; message: string; toRetry?: OAuthStatus }
|
||||
| { state: 'about_to_retry'; nextState: OAuthStatus }
|
||||
|
||||
const PASTE_HERE_MSG = 'Paste code here if prompted > '
|
||||
|
||||
export function OAuthFlowStep({
|
||||
onSuccess,
|
||||
onCancel
|
||||
onCancel,
|
||||
}: OAuthFlowStepProps): React.ReactNode {
|
||||
const [oauthStatus, setOAuthStatus] = useState<OAuthStatus>({
|
||||
state: 'starting'
|
||||
});
|
||||
const [oauthService] = useState(() => new OAuthService());
|
||||
const [pastedCode, setPastedCode] = useState('');
|
||||
const [cursorOffset, setCursorOffset] = useState(0);
|
||||
const [showPastePrompt, setShowPastePrompt] = useState(false);
|
||||
const [urlCopied, setUrlCopied] = useState(false);
|
||||
const timersRef = useRef<Set<NodeJS.Timeout>>(new Set());
|
||||
state: 'starting',
|
||||
})
|
||||
const [oauthService] = useState(() => new OAuthService())
|
||||
const [pastedCode, setPastedCode] = useState('')
|
||||
const [cursorOffset, setCursorOffset] = useState(0)
|
||||
const [showPastePrompt, setShowPastePrompt] = useState(false)
|
||||
const [urlCopied, setUrlCopied] = useState(false)
|
||||
const timersRef = useRef<Set<NodeJS.Timeout>>(new Set())
|
||||
// Separate ref so startOAuth's timer clear doesn't cancel the urlCopied reset
|
||||
const urlCopiedTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
||||
const terminalSize = useTerminalSize();
|
||||
const textInputColumns = Math.max(50, terminalSize.columns - PASTE_HERE_MSG.length - 4);
|
||||
const urlCopiedTimerRef = useRef<NodeJS.Timeout | undefined>(undefined)
|
||||
|
||||
const terminalSize = useTerminalSize()
|
||||
const textInputColumns = Math.max(
|
||||
50,
|
||||
terminalSize.columns - PASTE_HERE_MSG.length - 4,
|
||||
)
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (oauthStatus.state !== 'error') return;
|
||||
e.preventDefault();
|
||||
if (oauthStatus.state !== 'error') return
|
||||
e.preventDefault()
|
||||
if (e.key === 'return' && oauthStatus.toRetry) {
|
||||
setPastedCode('');
|
||||
setCursorOffset(0);
|
||||
setPastedCode('')
|
||||
setCursorOffset(0)
|
||||
setOAuthStatus({
|
||||
state: 'about_to_retry',
|
||||
nextState: oauthStatus.toRetry
|
||||
});
|
||||
nextState: oauthStatus.toRetry,
|
||||
})
|
||||
} else {
|
||||
onCancel();
|
||||
onCancel()
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmitCode(value: string, url: string) {
|
||||
try {
|
||||
// Expecting format "authorizationCode#state" from the authorization callback URL
|
||||
const [authorizationCode, state] = value.split('#');
|
||||
const [authorizationCode, state] = value.split('#')
|
||||
|
||||
if (!authorizationCode || !state) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: 'Invalid code. Please make sure the full code was copied',
|
||||
toRetry: {
|
||||
state: 'waiting_for_login',
|
||||
url
|
||||
}
|
||||
});
|
||||
return;
|
||||
toRetry: { state: 'waiting_for_login', url },
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Track which path the user is taking (manual code entry)
|
||||
logEvent('tengu_oauth_manual_entry', {});
|
||||
logEvent('tengu_oauth_manual_entry', {})
|
||||
oauthService.handleManualAuthCodeInput({
|
||||
authorizationCode,
|
||||
state
|
||||
});
|
||||
state,
|
||||
})
|
||||
} catch (err: unknown) {
|
||||
logError(err);
|
||||
logError(err)
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: (err as Error).message,
|
||||
toRetry: {
|
||||
state: 'waiting_for_login',
|
||||
url
|
||||
}
|
||||
});
|
||||
toRetry: { state: 'waiting_for_login', url },
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const startOAuth = useCallback(async () => {
|
||||
// Clear any existing timers when starting new OAuth flow
|
||||
timersRef.current.forEach(timer => clearTimeout(timer));
|
||||
timersRef.current.clear();
|
||||
timersRef.current.forEach(timer => clearTimeout(timer))
|
||||
timersRef.current.clear()
|
||||
|
||||
try {
|
||||
const result = await oauthService.startOAuthFlow(async url_0 => {
|
||||
setOAuthStatus({
|
||||
state: 'waiting_for_login',
|
||||
url: url_0
|
||||
});
|
||||
const timer_0 = setTimeout(setShowPastePrompt, 3000, true);
|
||||
timersRef.current.add(timer_0);
|
||||
}, {
|
||||
loginWithClaudeAi: true,
|
||||
// Always use Claude AI for subscription tokens
|
||||
inferenceOnly: true,
|
||||
expiresIn: 365 * 24 * 60 * 60 // 1 year
|
||||
});
|
||||
const result = await oauthService.startOAuthFlow(
|
||||
async url => {
|
||||
setOAuthStatus({ state: 'waiting_for_login', url })
|
||||
const timer = setTimeout(setShowPastePrompt, 3000, true)
|
||||
timersRef.current.add(timer)
|
||||
},
|
||||
{
|
||||
loginWithClaudeAi: true, // Always use Claude AI for subscription tokens
|
||||
inferenceOnly: true,
|
||||
expiresIn: 365 * 24 * 60 * 60, // 1 year
|
||||
},
|
||||
)
|
||||
|
||||
// Show processing state
|
||||
setOAuthStatus({
|
||||
state: 'processing'
|
||||
});
|
||||
setOAuthStatus({ state: 'processing' })
|
||||
|
||||
// OAuthFlowStep creates inference-only tokens for GitHub Actions, not a
|
||||
// replacement login. Use saveOAuthTokensIfNeeded directly to avoid
|
||||
// performLogout which would destroy the user's existing auth session.
|
||||
saveOAuthTokensIfNeeded(result);
|
||||
saveOAuthTokensIfNeeded(result)
|
||||
|
||||
// For OAuth flow, the access token can be used as an API key
|
||||
const timer1 = setTimeout((setOAuthStatus_0, accessToken, onSuccess_0, timersRef_0) => {
|
||||
setOAuthStatus_0({
|
||||
state: 'success',
|
||||
token: accessToken
|
||||
});
|
||||
// Auto-continue after brief delay to show success
|
||||
const timer2 = setTimeout(onSuccess_0, 1000, accessToken);
|
||||
timersRef_0.current.add(timer2 as ReturnType<typeof setTimeout>);
|
||||
}, 100, setOAuthStatus, result.accessToken, onSuccess, timersRef);
|
||||
timersRef.current.add(timer1);
|
||||
} catch (err_0) {
|
||||
const errorMessage = (err_0 as Error).message;
|
||||
const timer1 = setTimeout(
|
||||
(setOAuthStatus, accessToken, onSuccess, timersRef) => {
|
||||
setOAuthStatus({ state: 'success', token: accessToken })
|
||||
// Auto-continue after brief delay to show success
|
||||
const timer2 = setTimeout(onSuccess, 1000, accessToken)
|
||||
timersRef.current.add(timer2)
|
||||
},
|
||||
100,
|
||||
setOAuthStatus,
|
||||
result.accessToken,
|
||||
onSuccess,
|
||||
timersRef,
|
||||
)
|
||||
timersRef.current.add(timer1)
|
||||
} catch (err) {
|
||||
const errorMessage = (err as Error).message
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: errorMessage,
|
||||
toRetry: {
|
||||
state: 'starting'
|
||||
} // Allow retry by starting fresh OAuth flow
|
||||
});
|
||||
logError(err_0);
|
||||
toRetry: { state: 'starting' }, // Allow retry by starting fresh OAuth flow
|
||||
})
|
||||
logError(err)
|
||||
logEvent('tengu_oauth_error', {
|
||||
error: errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
error:
|
||||
errorMessage as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
}, [oauthService, onSuccess]);
|
||||
}, [oauthService, onSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (oauthStatus.state === 'starting') {
|
||||
void startOAuth();
|
||||
void startOAuth()
|
||||
}
|
||||
}, [oauthStatus.state, startOAuth]);
|
||||
}, [oauthStatus.state, startOAuth])
|
||||
|
||||
// Retry logic
|
||||
useEffect(() => {
|
||||
if (oauthStatus.state === 'about_to_retry') {
|
||||
const timer_1 = setTimeout((nextState, setShowPastePrompt_0, setOAuthStatus_1) => {
|
||||
// Only show paste prompt when retrying to waiting_for_login
|
||||
setShowPastePrompt_0(nextState.state === 'waiting_for_login');
|
||||
setOAuthStatus_1(nextState);
|
||||
}, 500, oauthStatus.nextState, setShowPastePrompt, setOAuthStatus);
|
||||
timersRef.current.add(timer_1);
|
||||
const timer = setTimeout(
|
||||
(nextState, setShowPastePrompt, setOAuthStatus) => {
|
||||
// Only show paste prompt when retrying to waiting_for_login
|
||||
setShowPastePrompt(nextState.state === 'waiting_for_login')
|
||||
setOAuthStatus(nextState)
|
||||
},
|
||||
500,
|
||||
oauthStatus.nextState,
|
||||
setShowPastePrompt,
|
||||
setOAuthStatus,
|
||||
)
|
||||
timersRef.current.add(timer)
|
||||
}
|
||||
}, [oauthStatus]);
|
||||
}, [oauthStatus])
|
||||
|
||||
useEffect(() => {
|
||||
if (pastedCode === 'c' && oauthStatus.state === 'waiting_for_login' && showPastePrompt && !urlCopied) {
|
||||
if (
|
||||
pastedCode === 'c' &&
|
||||
oauthStatus.state === 'waiting_for_login' &&
|
||||
showPastePrompt &&
|
||||
!urlCopied
|
||||
) {
|
||||
void setClipboard(oauthStatus.url).then(raw => {
|
||||
if (raw) process.stdout.write(raw);
|
||||
setUrlCopied(true);
|
||||
clearTimeout(urlCopiedTimerRef.current);
|
||||
urlCopiedTimerRef.current = setTimeout(setUrlCopied, 2000, false);
|
||||
});
|
||||
setPastedCode('');
|
||||
if (raw) process.stdout.write(raw)
|
||||
setUrlCopied(true)
|
||||
clearTimeout(urlCopiedTimerRef.current)
|
||||
urlCopiedTimerRef.current = setTimeout(setUrlCopied, 2000, false)
|
||||
})
|
||||
setPastedCode('')
|
||||
}
|
||||
}, [pastedCode, oauthStatus, showPastePrompt, urlCopied]);
|
||||
}, [pastedCode, oauthStatus, showPastePrompt, urlCopied])
|
||||
|
||||
// Cleanup OAuth service and timers when component unmounts
|
||||
useEffect(() => {
|
||||
const timers = timersRef.current;
|
||||
const timers = timersRef.current
|
||||
return () => {
|
||||
oauthService.cleanup();
|
||||
oauthService.cleanup()
|
||||
// Clear all timers
|
||||
timers.forEach(timer_2 => clearTimeout(timer_2));
|
||||
timers.clear();
|
||||
clearTimeout(urlCopiedTimerRef.current);
|
||||
};
|
||||
}, [oauthService]);
|
||||
timers.forEach(timer => clearTimeout(timer))
|
||||
timers.clear()
|
||||
clearTimeout(urlCopiedTimerRef.current)
|
||||
}
|
||||
}, [oauthService])
|
||||
|
||||
// Helper function to render the appropriate status message
|
||||
function renderStatusMessage(): React.ReactNode {
|
||||
switch (oauthStatus.state) {
|
||||
case 'starting':
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Starting authentication…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'waiting_for_login':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
{!showPastePrompt && <Box>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{!showPastePrompt && (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>
|
||||
Opening browser to sign in with your Claude account…
|
||||
</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showPastePrompt && <Box>
|
||||
{showPastePrompt && (
|
||||
<Box>
|
||||
<Text>{PASTE_HERE_MSG}</Text>
|
||||
<TextInput value={pastedCode} onChange={setPastedCode} onSubmit={(value_0: string) => handleSubmitCode(value_0, oauthStatus.url)} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} columns={textInputColumns} />
|
||||
</Box>}
|
||||
</Box>;
|
||||
<TextInput
|
||||
value={pastedCode}
|
||||
onChange={setPastedCode}
|
||||
onSubmit={(value: string) =>
|
||||
handleSubmitCode(value, oauthStatus.url)
|
||||
}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
columns={textInputColumns}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'processing':
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Processing authentication…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'success':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="success">
|
||||
✓ Authentication token created successfully!
|
||||
</Text>
|
||||
<Text dimColor>Using token for GitHub Actions setup…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'error':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="error">OAuth error: {oauthStatus.message}</Text>
|
||||
{oauthStatus.toRetry ? <Text dimColor>
|
||||
{oauthStatus.toRetry ? (
|
||||
<Text dimColor>
|
||||
Press Enter to try again, or any other key to cancel
|
||||
</Text> : <Text dimColor>Press any key to return to API key selection</Text>}
|
||||
</Box>;
|
||||
</Text>
|
||||
) : (
|
||||
<Text dimColor>Press any key to return to API key selection</Text>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'about_to_retry':
|
||||
return <Box flexDirection="column" gap={1}>
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text color="permission">Retrying…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
|
||||
default:
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
}
|
||||
return <Box flexDirection="column" gap={1} tabIndex={0} autoFocus onKeyDown={handleKeyDown}>
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
gap={1}
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{/* Show header inline only for initial starting state */}
|
||||
{oauthStatus.state === 'starting' && <Box flexDirection="column" gap={1} paddingBottom={1}>
|
||||
{oauthStatus.state === 'starting' && (
|
||||
<Box flexDirection="column" gap={1} paddingBottom={1}>
|
||||
<Text bold>Create Authentication Token</Text>
|
||||
<Text dimColor>Creating a long-lived token for GitHub Actions</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
{/* Show header for non-starting states (to avoid duplicate with inline header)*/}
|
||||
{oauthStatus.state !== 'success' && oauthStatus.state !== 'starting' && oauthStatus.state !== 'processing' && <Box key="header" flexDirection="column" gap={1} paddingBottom={1}>
|
||||
{oauthStatus.state !== 'success' &&
|
||||
oauthStatus.state !== 'starting' &&
|
||||
oauthStatus.state !== 'processing' && (
|
||||
<Box key="header" flexDirection="column" gap={1} paddingBottom={1}>
|
||||
<Text bold>Create Authentication Token</Text>
|
||||
<Text dimColor>Creating a long-lived token for GitHub Actions</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
{/* Show URL when paste prompt is visible */}
|
||||
{oauthStatus.state === 'waiting_for_login' && showPastePrompt && <Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}>
|
||||
{oauthStatus.state === 'waiting_for_login' && showPastePrompt && (
|
||||
<Box flexDirection="column" key="urlToCopy" gap={1} paddingBottom={1}>
|
||||
<Box paddingX={1}>
|
||||
<Text dimColor>
|
||||
Browser didn't open? Use the url below to sign in{' '}
|
||||
</Text>
|
||||
{urlCopied ? <Text color="success">(Copied!)</Text> : <Text dimColor>
|
||||
{urlCopied ? (
|
||||
<Text color="success">(Copied!)</Text>
|
||||
) : (
|
||||
<Text dimColor>
|
||||
<KeyboardShortcutHint shortcut="c" action="copy" parens />
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Link url={oauthStatus.url}>
|
||||
<Text dimColor>{oauthStatus.url}</Text>
|
||||
</Link>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
<Box paddingLeft={1} flexDirection="column" gap={1}>
|
||||
{renderStatusMessage()}
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,95 +1,65 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type SuccessStepProps = {
|
||||
secretExists: boolean;
|
||||
useExistingSecret: boolean;
|
||||
secretName: string;
|
||||
skipWorkflow?: boolean;
|
||||
};
|
||||
export function SuccessStep(t0) {
|
||||
const $ = _c(21);
|
||||
const {
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow: t1
|
||||
} = t0;
|
||||
const skipWorkflow = t1 === undefined ? false : t1;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>Install GitHub App</Text><Text dimColor={true}>Success</Text></Box>;
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== skipWorkflow) {
|
||||
t3 = !skipWorkflow && <Text color="success">✓ GitHub Actions workflow created!</Text>;
|
||||
$[1] = skipWorkflow;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t3 = $[2];
|
||||
}
|
||||
let t4;
|
||||
if ($[3] !== secretExists || $[4] !== useExistingSecret) {
|
||||
t4 = secretExists && useExistingSecret && <Box marginTop={1}><Text color="success">✓ Using existing ANTHROPIC_API_KEY secret</Text></Box>;
|
||||
$[3] = secretExists;
|
||||
$[4] = useExistingSecret;
|
||||
$[5] = t4;
|
||||
} else {
|
||||
t4 = $[5];
|
||||
}
|
||||
let t5;
|
||||
if ($[6] !== secretExists || $[7] !== secretName || $[8] !== useExistingSecret) {
|
||||
t5 = (!secretExists || !useExistingSecret) && <Box marginTop={1}><Text color="success">✓ API key saved as {secretName} secret</Text></Box>;
|
||||
$[6] = secretExists;
|
||||
$[7] = secretName;
|
||||
$[8] = useExistingSecret;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Box marginTop={1}><Text>Next steps:</Text></Box>;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
}
|
||||
let t7;
|
||||
if ($[11] !== skipWorkflow) {
|
||||
t7 = skipWorkflow ? <><Text>1. Install the Claude GitHub App if you haven't already</Text><Text>2. Your workflow file was kept unchanged</Text><Text>3. API key is configured and ready to use</Text></> : <><Text>1. A pre-filled PR page has been created</Text><Text>2. Install the Claude GitHub App if you haven't already</Text><Text>3. Merge the PR to enable Claude PR assistance</Text></>;
|
||||
$[11] = skipWorkflow;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] !== t3 || $[14] !== t4 || $[15] !== t5 || $[16] !== t7) {
|
||||
t8 = <Box flexDirection="column" borderStyle="round" paddingX={1}>{t2}{t3}{t4}{t5}{t6}{t7}</Box>;
|
||||
$[13] = t3;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
$[16] = t7;
|
||||
$[17] = t8;
|
||||
} else {
|
||||
t8 = $[17];
|
||||
}
|
||||
let t9;
|
||||
if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Box marginLeft={3}><Text dimColor={true}>Press any key to exit</Text></Box>;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
let t10;
|
||||
if ($[19] !== t8) {
|
||||
t10 = <>{t8}{t9}</>;
|
||||
$[19] = t8;
|
||||
$[20] = t10;
|
||||
} else {
|
||||
t10 = $[20];
|
||||
}
|
||||
return t10;
|
||||
secretExists: boolean
|
||||
useExistingSecret: boolean
|
||||
secretName: string
|
||||
skipWorkflow?: boolean
|
||||
}
|
||||
|
||||
export function SuccessStep({
|
||||
secretExists,
|
||||
useExistingSecret,
|
||||
secretName,
|
||||
skipWorkflow = false,
|
||||
}: SuccessStepProps): React.ReactNode {
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>Install GitHub App</Text>
|
||||
<Text dimColor>Success</Text>
|
||||
</Box>
|
||||
{!skipWorkflow && (
|
||||
<Text color="success">✓ GitHub Actions workflow created!</Text>
|
||||
)}
|
||||
{secretExists && useExistingSecret && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="success">
|
||||
✓ Using existing ANTHROPIC_API_KEY secret
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{(!secretExists || !useExistingSecret) && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="success">✓ API key saved as {secretName} secret</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Box marginTop={1}>
|
||||
<Text>Next steps:</Text>
|
||||
</Box>
|
||||
{skipWorkflow ? (
|
||||
<>
|
||||
<Text>
|
||||
1. Install the Claude GitHub App if you haven't already
|
||||
</Text>
|
||||
<Text>2. Your workflow file was kept unchanged</Text>
|
||||
<Text>3. API key is configured and ready to use</Text>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text>1. A pre-filled PR page has been created</Text>
|
||||
<Text>
|
||||
2. Install the Claude GitHub App if you haven't already
|
||||
</Text>
|
||||
<Text>3. Merge the PR to enable Claude PR assistance</Text>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor>Press any key to exit</Text>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,72 +1,59 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React from 'react';
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { Warning } from './types.js';
|
||||
import figures from 'figures'
|
||||
import React from 'react'
|
||||
import { GITHUB_ACTION_SETUP_DOCS_URL } from '../../constants/github-app.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { Warning } from './types.js'
|
||||
|
||||
interface WarningsStepProps {
|
||||
warnings: Warning[];
|
||||
onContinue: () => void;
|
||||
warnings: Warning[]
|
||||
onContinue: () => void
|
||||
}
|
||||
export function WarningsStep(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
warnings,
|
||||
onContinue
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
useKeybinding("confirm:yes", onContinue, t1);
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box flexDirection="column" marginBottom={1}><Text bold={true}>{figures.warning} Setup Warnings</Text><Text dimColor={true}>We found some potential issues, but you can continue anyway</Text></Box>;
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
let t3;
|
||||
if ($[2] !== warnings) {
|
||||
t3 = warnings.map(_temp2);
|
||||
$[2] = warnings;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginTop={1}><Text bold={true} color="permission">Press Enter to continue anyway, or Ctrl+C to exit and fix issues</Text></Box>;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Box marginTop={1}><Text dimColor={true}>You can also try the manual setup steps if needed:{" "}<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text></Text></Box>;
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
let t6;
|
||||
if ($[6] !== t3) {
|
||||
t6 = <><Box flexDirection="column" borderStyle="round" paddingX={1}>{t2}{t3}{t4}{t5}</Box></>;
|
||||
$[6] = t3;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
return t6;
|
||||
}
|
||||
function _temp2(warning, index) {
|
||||
return <Box key={index} flexDirection="column" marginBottom={1}><Text color="warning" bold={true}>{warning.title}</Text><Text>{warning.message}</Text>{warning.instructions.length > 0 && <Box flexDirection="column" marginLeft={2} marginTop={1}>{warning.instructions.map(_temp)}</Box>}</Box>;
|
||||
}
|
||||
function _temp(instruction, i) {
|
||||
return <Text key={i} dimColor={true}>• {instruction}</Text>;
|
||||
|
||||
export function WarningsStep({ warnings, onContinue }: WarningsStepProps) {
|
||||
// Enter to continue
|
||||
useKeybinding('confirm:yes', onContinue, { context: 'Confirmation' })
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection="column" borderStyle="round" paddingX={1}>
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>{figures.warning} Setup Warnings</Text>
|
||||
<Text dimColor>
|
||||
We found some potential issues, but you can continue anyway
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{warnings.map((warning, index) => (
|
||||
<Box key={index} flexDirection="column" marginBottom={1}>
|
||||
<Text color="warning" bold>
|
||||
{warning.title}
|
||||
</Text>
|
||||
<Text>{warning.message}</Text>
|
||||
{warning.instructions.length > 0 && (
|
||||
<Box flexDirection="column" marginLeft={2} marginTop={1}>
|
||||
{warning.instructions.map((instruction, i) => (
|
||||
<Text key={i} dimColor>
|
||||
• {instruction}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
))}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text bold color="permission">
|
||||
Press Enter to continue anyway, or Ctrl+C to exit and fix issues
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>
|
||||
You can also try the manual setup steps if needed:{' '}
|
||||
<Text color="claude">{GITHUB_ACTION_SETUP_DOCS_URL}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,239 +1,252 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import type { CommandResultDisplay } from 'src/commands.js';
|
||||
import { logEvent } from 'src/services/analytics/index.js';
|
||||
import { StatusIcon } from '../components/design-system/StatusIcon.js';
|
||||
import { Box, render, Text } from '../ink.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { env } from '../utils/env.js';
|
||||
import { errorMessage } from '../utils/errors.js';
|
||||
import { checkInstall, cleanupNpmInstallations, cleanupShellAliases, installLatest } from '../utils/nativeInstaller/index.js';
|
||||
import { getInitialSettings, updateSettingsForSource } from '../utils/settings/settings.js';
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import type { CommandResultDisplay } from 'src/commands.js'
|
||||
import { logEvent } from 'src/services/analytics/index.js'
|
||||
import { StatusIcon } from '../components/design-system/StatusIcon.js'
|
||||
import { Box, render, Text } from '../ink.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { env } from '../utils/env.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import {
|
||||
checkInstall,
|
||||
cleanupNpmInstallations,
|
||||
cleanupShellAliases,
|
||||
installLatest,
|
||||
} from '../utils/nativeInstaller/index.js'
|
||||
import {
|
||||
getInitialSettings,
|
||||
updateSettingsForSource,
|
||||
} from '../utils/settings/settings.js'
|
||||
|
||||
interface InstallProps {
|
||||
onDone: (result: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
force?: boolean;
|
||||
target?: string; // 'latest', 'stable', or version like '1.0.34'
|
||||
onDone: (result: string, options?: { display?: CommandResultDisplay }) => void
|
||||
force?: boolean
|
||||
target?: string // 'latest', 'stable', or version like '1.0.34'
|
||||
}
|
||||
type InstallState = {
|
||||
type: 'checking';
|
||||
} | {
|
||||
type: 'cleaning-npm';
|
||||
} | {
|
||||
type: 'installing';
|
||||
version: string;
|
||||
} | {
|
||||
type: 'setting-up';
|
||||
} | {
|
||||
type: 'set-up';
|
||||
messages: string[];
|
||||
} | {
|
||||
type: 'success';
|
||||
version: string;
|
||||
setupMessages?: string[];
|
||||
} | {
|
||||
type: 'error';
|
||||
message: string;
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
type InstallState =
|
||||
| { type: 'checking' }
|
||||
| { type: 'cleaning-npm' }
|
||||
| { type: 'installing'; version: string }
|
||||
| { type: 'setting-up' }
|
||||
| { type: 'set-up'; messages: string[] }
|
||||
| { type: 'success'; version: string; setupMessages?: string[] }
|
||||
| { type: 'error'; message: string; warnings?: string[] }
|
||||
|
||||
function getInstallationPath(): string {
|
||||
const isWindows = env.platform === 'win32';
|
||||
const homeDir = homedir();
|
||||
const isWindows = env.platform === 'win32'
|
||||
const homeDir = homedir()
|
||||
|
||||
if (isWindows) {
|
||||
// Convert to Windows-style path
|
||||
const windowsPath = join(homeDir, '.local', 'bin', 'claude.exe');
|
||||
const windowsPath = join(homeDir, '.local', 'bin', 'claude.exe')
|
||||
// Replace forward slashes with backslashes for Windows display
|
||||
return windowsPath.replace(/\//g, '\\');
|
||||
return windowsPath.replace(/\//g, '\\')
|
||||
}
|
||||
return '~/.local/bin/claude';
|
||||
|
||||
return '~/.local/bin/claude'
|
||||
}
|
||||
function SetupNotes(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
messages
|
||||
} = t0;
|
||||
if (messages.length === 0) {
|
||||
return null;
|
||||
}
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Box><Text color="warning"><StatusIcon status="warning" withSpace={true} />Setup notes:</Text></Box>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== messages) {
|
||||
t2 = messages.map(_temp);
|
||||
$[1] = messages;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== t2) {
|
||||
t3 = <Box flexDirection="column" gap={0} marginBottom={1}>{t1}{t2}</Box>;
|
||||
$[3] = t2;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
|
||||
function SetupNotes({ messages }: { messages: string[] }): React.ReactNode {
|
||||
if (messages.length === 0) return null
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={0} marginBottom={1}>
|
||||
<Box>
|
||||
<Text color="warning">
|
||||
<StatusIcon status="warning" withSpace />
|
||||
Setup notes:
|
||||
</Text>
|
||||
</Box>
|
||||
{messages.map((message, index) => (
|
||||
<Box key={index} marginLeft={2}>
|
||||
<Text dimColor>• {message}</Text>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
function _temp(message, index) {
|
||||
return <Box key={index} marginLeft={2}><Text dimColor={true}>• {message}</Text></Box>;
|
||||
}
|
||||
function Install({
|
||||
onDone,
|
||||
force,
|
||||
target
|
||||
}: InstallProps): React.ReactNode {
|
||||
const [state, setState] = useState<InstallState>({
|
||||
type: 'checking'
|
||||
});
|
||||
|
||||
function Install({ onDone, force, target }: InstallProps): React.ReactNode {
|
||||
const [state, setState] = useState<InstallState>({ type: 'checking' })
|
||||
|
||||
useEffect(() => {
|
||||
async function run() {
|
||||
try {
|
||||
logForDebugging(`Install: Starting installation process (force=${force}, target=${target})`);
|
||||
logForDebugging(
|
||||
`Install: Starting installation process (force=${force}, target=${target})`,
|
||||
)
|
||||
|
||||
// Install native build first
|
||||
const channelOrVersion = target || getInitialSettings()?.autoUpdatesChannel || 'latest';
|
||||
setState({
|
||||
type: 'installing',
|
||||
version: channelOrVersion
|
||||
});
|
||||
const channelOrVersion =
|
||||
target || getInitialSettings()?.autoUpdatesChannel || 'latest'
|
||||
setState({ type: 'installing', version: channelOrVersion })
|
||||
|
||||
// Pass force flag to trigger reinstall even if up to date
|
||||
logForDebugging(`Install: Calling installLatest(channelOrVersion=${channelOrVersion}, forceReinstall=${force})`);
|
||||
const result = await installLatest(channelOrVersion, force);
|
||||
logForDebugging(`Install: installLatest returned version=${result.latestVersion}, wasUpdated=${result.wasUpdated}, lockFailed=${result.lockFailed}`);
|
||||
logForDebugging(
|
||||
`Install: Calling installLatest(channelOrVersion=${channelOrVersion}, forceReinstall=${force})`,
|
||||
)
|
||||
const result = await installLatest(channelOrVersion, force)
|
||||
logForDebugging(
|
||||
`Install: installLatest returned version=${result.latestVersion}, wasUpdated=${result.wasUpdated}, lockFailed=${result.lockFailed}`,
|
||||
)
|
||||
|
||||
// Check specifically for lock failure
|
||||
if (result.lockFailed) {
|
||||
throw new Error('Could not install - another process is currently installing Claude. Please try again in a moment.');
|
||||
throw new Error(
|
||||
'Could not install - another process is currently installing Claude. Please try again in a moment.',
|
||||
)
|
||||
}
|
||||
|
||||
// If we couldn't get the version, there might be an issue
|
||||
if (!result.latestVersion) {
|
||||
logForDebugging('Install: Failed to retrieve version information during install', {
|
||||
level: 'error'
|
||||
});
|
||||
logForDebugging(
|
||||
'Install: Failed to retrieve version information during install',
|
||||
{ level: 'error' },
|
||||
)
|
||||
}
|
||||
|
||||
if (!result.wasUpdated) {
|
||||
logForDebugging('Install: Already up to date');
|
||||
logForDebugging('Install: Already up to date')
|
||||
}
|
||||
|
||||
// Set up launcher and shell integration
|
||||
setState({
|
||||
type: 'setting-up'
|
||||
});
|
||||
const setupMessages = await checkInstall(true);
|
||||
logForDebugging(`Install: Setup launcher completed with ${setupMessages.length} messages`);
|
||||
setState({ type: 'setting-up' })
|
||||
const setupMessages = await checkInstall(true)
|
||||
|
||||
logForDebugging(
|
||||
`Install: Setup launcher completed with ${setupMessages.length} messages`,
|
||||
)
|
||||
if (setupMessages.length > 0) {
|
||||
setupMessages.forEach(msg => logForDebugging(`Install: Setup message: ${msg.message}`));
|
||||
setupMessages.forEach(msg =>
|
||||
logForDebugging(`Install: Setup message: ${msg.message}`),
|
||||
)
|
||||
}
|
||||
|
||||
// Now that native installation succeeded, clean up old npm installations
|
||||
logForDebugging('Install: Cleaning up npm installations after successful install');
|
||||
const {
|
||||
removed,
|
||||
errors,
|
||||
warnings
|
||||
} = await cleanupNpmInstallations();
|
||||
logForDebugging(
|
||||
'Install: Cleaning up npm installations after successful install',
|
||||
)
|
||||
const { removed, errors, warnings } = await cleanupNpmInstallations()
|
||||
|
||||
if (removed > 0) {
|
||||
logForDebugging(`Cleaned up ${removed} npm installation(s)`);
|
||||
logForDebugging(`Cleaned up ${removed} npm installation(s)`)
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
logForDebugging(`Cleanup errors: ${errors.join(', ')}`);
|
||||
logForDebugging(`Cleanup errors: ${errors.join(', ')}`)
|
||||
// Continue despite cleanup errors - native install already succeeded
|
||||
}
|
||||
|
||||
// Clean up old shell aliases
|
||||
const aliasMessages = await cleanupShellAliases();
|
||||
const aliasMessages = await cleanupShellAliases()
|
||||
if (aliasMessages.length > 0) {
|
||||
logForDebugging(`Shell alias cleanup: ${aliasMessages.map(m => m.message).join('; ')}`);
|
||||
logForDebugging(
|
||||
`Shell alias cleanup: ${aliasMessages.map(m => m.message).join('; ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
// Log success event
|
||||
logEvent('tengu_claude_install_command', {
|
||||
has_version: result.latestVersion ? 1 : 0,
|
||||
forced: force ? 1 : 0
|
||||
});
|
||||
forced: force ? 1 : 0,
|
||||
})
|
||||
|
||||
// If user explicitly specified a channel, save it to settings
|
||||
if (target === 'latest' || target === 'stable') {
|
||||
updateSettingsForSource('userSettings', {
|
||||
autoUpdatesChannel: target
|
||||
});
|
||||
logForDebugging(`Install: Saved autoUpdatesChannel=${target} to user settings`);
|
||||
autoUpdatesChannel: target,
|
||||
})
|
||||
logForDebugging(
|
||||
`Install: Saved autoUpdatesChannel=${target} to user settings`,
|
||||
)
|
||||
}
|
||||
|
||||
// Combine all warning/info messages (convert SetupMessage to string)
|
||||
const allWarnings = [...warnings, ...aliasMessages.map(m_0 => m_0.message)];
|
||||
const allWarnings = [...warnings, ...aliasMessages.map(m => m.message)]
|
||||
|
||||
// Check if there were any setup errors or notes
|
||||
if (setupMessages.length > 0) {
|
||||
setState({
|
||||
type: 'set-up',
|
||||
messages: setupMessages.map(m_1 => m_1.message)
|
||||
});
|
||||
messages: setupMessages.map(m => m.message),
|
||||
})
|
||||
// Still mark as success but show both setup messages and cleanup warnings
|
||||
setTimeout(setState, 2000, {
|
||||
type: 'success' as const,
|
||||
version: result.latestVersion || 'current',
|
||||
setupMessages: [...setupMessages.map(m_2 => m_2.message), ...allWarnings]
|
||||
});
|
||||
setupMessages: [
|
||||
...setupMessages.map(m => m.message),
|
||||
...allWarnings,
|
||||
],
|
||||
})
|
||||
} else {
|
||||
// No setup messages, go straight to success (but still show cleanup warnings if any)
|
||||
logForDebugging('Install: Shell PATH already configured');
|
||||
logForDebugging('Install: Shell PATH already configured')
|
||||
setState({
|
||||
type: 'success',
|
||||
version: result.latestVersion || 'current',
|
||||
setupMessages: allWarnings.length > 0 ? allWarnings : undefined
|
||||
});
|
||||
setupMessages: allWarnings.length > 0 ? allWarnings : undefined,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
logForDebugging(`Install command failed: ${error}`, {
|
||||
level: 'error'
|
||||
});
|
||||
level: 'error',
|
||||
})
|
||||
setState({
|
||||
type: 'error',
|
||||
message: errorMessage(error)
|
||||
});
|
||||
message: errorMessage(error),
|
||||
})
|
||||
}
|
||||
}
|
||||
void run();
|
||||
}, [force, target]);
|
||||
|
||||
void run()
|
||||
}, [force, target])
|
||||
|
||||
useEffect(() => {
|
||||
if (state.type === 'success') {
|
||||
// Give success message time to render before exiting
|
||||
setTimeout(onDone, 2000, 'Claude Code installation completed successfully', {
|
||||
display: 'system' as const
|
||||
});
|
||||
setTimeout(
|
||||
onDone,
|
||||
2000,
|
||||
'Claude Code installation completed successfully',
|
||||
{
|
||||
display: 'system' as const,
|
||||
},
|
||||
)
|
||||
} else if (state.type === 'error') {
|
||||
// Give error message time to render before exiting
|
||||
setTimeout(onDone, 3000, 'Claude Code installation failed', {
|
||||
display: 'system' as const
|
||||
});
|
||||
display: 'system' as const,
|
||||
})
|
||||
}
|
||||
}, [state, onDone]);
|
||||
return <Box flexDirection="column" marginTop={1}>
|
||||
{state.type === 'checking' && <Text color="claude">Checking installation status...</Text>}
|
||||
}, [state, onDone])
|
||||
|
||||
{state.type === 'cleaning-npm' && <Text color="warning">Cleaning up old npm installations...</Text>}
|
||||
return (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
{state.type === 'checking' && (
|
||||
<Text color="claude">Checking installation status...</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'installing' && <Text color="claude">
|
||||
{state.type === 'cleaning-npm' && (
|
||||
<Text color="warning">Cleaning up old npm installations...</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'installing' && (
|
||||
<Text color="claude">
|
||||
Installing Claude Code native build {state.version}...
|
||||
</Text>}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'setting-up' && <Text color="claude">Setting up launcher and shell integration...</Text>}
|
||||
{state.type === 'setting-up' && (
|
||||
<Text color="claude">Setting up launcher and shell integration...</Text>
|
||||
)}
|
||||
|
||||
{state.type === 'set-up' && <SetupNotes messages={state.messages} />}
|
||||
|
||||
{state.type === 'success' && <Box flexDirection="column" gap={1}>
|
||||
{state.type === 'success' && (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<StatusIcon status="success" withSpace />
|
||||
<Text color="success" bold>
|
||||
@@ -241,10 +254,12 @@ function Install({
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginLeft={2} flexDirection="column" gap={1}>
|
||||
{state.version !== 'current' && <Box>
|
||||
{state.version !== 'current' && (
|
||||
<Box>
|
||||
<Text dimColor>Version: </Text>
|
||||
<Text color="claude">{state.version}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
<Box>
|
||||
<Text dimColor>Location: </Text>
|
||||
<Text color="text">{getInstallationPath()}</Text>
|
||||
@@ -260,9 +275,11 @@ function Install({
|
||||
</Box>
|
||||
</Box>
|
||||
{state.setupMessages && <SetupNotes messages={state.setupMessages} />}
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{state.type === 'error' && <Box flexDirection="column" gap={1}>
|
||||
{state.type === 'error' && (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Box>
|
||||
<StatusIcon status="error" withSpace />
|
||||
<Text color="error">Installation failed</Text>
|
||||
@@ -271,8 +288,10 @@ function Install({
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Try running with --force to override checks</Text>
|
||||
</Box>
|
||||
</Box>}
|
||||
</Box>;
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// This is only used from cli.tsx, not as a slash command
|
||||
@@ -281,19 +300,28 @@ export const install = {
|
||||
name: 'install',
|
||||
description: 'Install Claude Code native build',
|
||||
argumentHint: '[options]',
|
||||
async call(onDone: (result: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void, _context: unknown, args: string[]) {
|
||||
async call(
|
||||
onDone: (
|
||||
result: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void,
|
||||
_context: unknown,
|
||||
args: string[],
|
||||
) {
|
||||
// Parse arguments
|
||||
const force = args.includes('--force');
|
||||
const nonFlagArgs = args.filter(arg => !arg.startsWith('--'));
|
||||
const target = nonFlagArgs[0]; // 'latest', 'stable', or version like '1.0.34'
|
||||
const force = args.includes('--force')
|
||||
const nonFlagArgs = args.filter(arg => !arg.startsWith('--'))
|
||||
const target = nonFlagArgs[0] // 'latest', 'stable', or version like '1.0.34'
|
||||
|
||||
const {
|
||||
unmount
|
||||
} = await render(<Install onDone={(result, options) => {
|
||||
unmount();
|
||||
onDone(result, options);
|
||||
}} force={force} target={target} />);
|
||||
}
|
||||
};
|
||||
const { unmount } = await render(
|
||||
<Install
|
||||
onDone={(result, options) => {
|
||||
unmount()
|
||||
onDone(result, options)
|
||||
}}
|
||||
force={force}
|
||||
target={target}
|
||||
/>,
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,103 +1,113 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { feature } from 'bun:bundle';
|
||||
import * as React from 'react';
|
||||
import { resetCostState } from '../../bootstrap/state.js';
|
||||
import { clearTrustedDeviceToken, enrollTrustedDevice } from '../../bridge/trustedDevice.js';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js';
|
||||
import { Text } from '../../ink.js';
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js';
|
||||
import { refreshPolicyLimits } from '../../services/policyLimits/index.js';
|
||||
import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { stripSignatureBlocks } from '../../utils/messages.js';
|
||||
import { checkAndDisableAutoModeIfNeeded, checkAndDisableBypassPermissionsIfNeeded, resetAutoModeGateCheck, resetBypassPermissionsCheck } from '../../utils/permissions/bypassPermissionsKillswitch.js';
|
||||
import { resetUserCache } from '../../utils/user.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <Login onDone={async success => {
|
||||
context.onChangeAPIKey();
|
||||
// Signature-bearing blocks (thinking, connector_text) are bound to the API key —
|
||||
// strip them so the new key doesn't reject stale signatures.
|
||||
context.setMessages(stripSignatureBlocks);
|
||||
if (success) {
|
||||
// Post-login refresh logic. Keep in sync with onboarding in src/interactiveHelpers.tsx
|
||||
// Reset cost state when switching accounts
|
||||
resetCostState();
|
||||
// Refresh remotely managed settings after login (non-blocking)
|
||||
void refreshRemoteManagedSettings();
|
||||
// Refresh policy limits after login (non-blocking)
|
||||
void refreshPolicyLimits();
|
||||
// Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials
|
||||
resetUserCache();
|
||||
// Refresh GrowthBook after login to get updated feature flags (e.g., for claude.ai MCPs)
|
||||
refreshGrowthBookAfterAuthChange();
|
||||
// Clear any stale trusted device token from a previous account before
|
||||
// re-enrolling — prevents sending the old token on bridge calls while
|
||||
// the async enrollTrustedDevice() is in-flight.
|
||||
clearTrustedDeviceToken();
|
||||
// Enroll as a trusted device for Remote Control (10-min fresh-session window)
|
||||
void enrollTrustedDevice();
|
||||
// Reset killswitch gate checks and re-run with new org
|
||||
resetBypassPermissionsCheck();
|
||||
const appState = context.getAppState();
|
||||
void checkAndDisableBypassPermissionsIfNeeded(appState.toolPermissionContext, context.setAppState);
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
resetAutoModeGateCheck();
|
||||
void checkAndDisableAutoModeIfNeeded(appState.toolPermissionContext, context.setAppState, appState.fastMode);
|
||||
import { feature } from 'bun:bundle'
|
||||
import * as React from 'react'
|
||||
import { resetCostState } from '../../bootstrap/state.js'
|
||||
import {
|
||||
clearTrustedDeviceToken,
|
||||
enrollTrustedDevice,
|
||||
} from '../../bridge/trustedDevice.js'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||
import { ConsoleOAuthFlow } from '../../components/ConsoleOAuthFlow.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { useMainLoopModel } from '../../hooks/useMainLoopModel.js'
|
||||
import { Text } from '../../ink.js'
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
||||
import { refreshPolicyLimits } from '../../services/policyLimits/index.js'
|
||||
import { refreshRemoteManagedSettings } from '../../services/remoteManagedSettings/index.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { stripSignatureBlocks } from '../../utils/messages.js'
|
||||
import {
|
||||
checkAndDisableAutoModeIfNeeded,
|
||||
checkAndDisableBypassPermissionsIfNeeded,
|
||||
resetAutoModeGateCheck,
|
||||
resetBypassPermissionsCheck,
|
||||
} from '../../utils/permissions/bypassPermissionsKillswitch.js'
|
||||
import { resetUserCache } from '../../utils/user.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return (
|
||||
<Login
|
||||
onDone={async success => {
|
||||
context.onChangeAPIKey()
|
||||
// Signature-bearing blocks (thinking, connector_text) are bound to the API key —
|
||||
// strip them so the new key doesn't reject stale signatures.
|
||||
context.setMessages(stripSignatureBlocks)
|
||||
if (success) {
|
||||
// Post-login refresh logic. Keep in sync with onboarding in src/interactiveHelpers.tsx
|
||||
// Reset cost state when switching accounts
|
||||
resetCostState()
|
||||
// Refresh remotely managed settings after login (non-blocking)
|
||||
void refreshRemoteManagedSettings()
|
||||
// Refresh policy limits after login (non-blocking)
|
||||
void refreshPolicyLimits()
|
||||
// Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials
|
||||
resetUserCache()
|
||||
// Refresh GrowthBook after login to get updated feature flags (e.g., for claude.ai MCPs)
|
||||
refreshGrowthBookAfterAuthChange()
|
||||
// Clear any stale trusted device token from a previous account before
|
||||
// re-enrolling — prevents sending the old token on bridge calls while
|
||||
// the async enrollTrustedDevice() is in-flight.
|
||||
clearTrustedDeviceToken()
|
||||
// Enroll as a trusted device for Remote Control (10-min fresh-session window)
|
||||
void enrollTrustedDevice()
|
||||
// Reset killswitch gate checks and re-run with new org
|
||||
resetBypassPermissionsCheck()
|
||||
const appState = context.getAppState()
|
||||
void checkAndDisableBypassPermissionsIfNeeded(
|
||||
appState.toolPermissionContext,
|
||||
context.setAppState,
|
||||
)
|
||||
if (feature('TRANSCRIPT_CLASSIFIER')) {
|
||||
resetAutoModeGateCheck()
|
||||
void checkAndDisableAutoModeIfNeeded(
|
||||
appState.toolPermissionContext,
|
||||
context.setAppState,
|
||||
appState.fastMode,
|
||||
)
|
||||
}
|
||||
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
authVersion: prev.authVersion + 1,
|
||||
}))
|
||||
}
|
||||
onDone(success ? 'Login successful' : 'Login interrupted')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function Login(props: {
|
||||
onDone: (success: boolean, mainLoopModel: string) => void
|
||||
startingMessage?: string
|
||||
}): React.ReactNode {
|
||||
const mainLoopModel = useMainLoopModel()
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Login"
|
||||
onCancel={() => props.onDone(false, mainLoopModel)}
|
||||
color="permission"
|
||||
inputGuide={exitState =>
|
||||
exitState.pending ? (
|
||||
<Text>Press {exitState.keyName} again to exit</Text>
|
||||
) : (
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
)
|
||||
}
|
||||
// Increment authVersion to trigger re-fetching of auth-dependent data in hooks (e.g., MCP servers)
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
authVersion: prev.authVersion + 1
|
||||
}));
|
||||
}
|
||||
onDone(success ? 'Login successful' : 'Login interrupted');
|
||||
}} />;
|
||||
}
|
||||
export function Login(props) {
|
||||
const $ = _c(12);
|
||||
const mainLoopModel = useMainLoopModel();
|
||||
let t0;
|
||||
if ($[0] !== mainLoopModel || $[1] !== props) {
|
||||
t0 = () => props.onDone(false, mainLoopModel);
|
||||
$[0] = mainLoopModel;
|
||||
$[1] = props;
|
||||
$[2] = t0;
|
||||
} else {
|
||||
t0 = $[2];
|
||||
}
|
||||
let t1;
|
||||
if ($[3] !== mainLoopModel || $[4] !== props) {
|
||||
t1 = () => props.onDone(true, mainLoopModel);
|
||||
$[3] = mainLoopModel;
|
||||
$[4] = props;
|
||||
$[5] = t1;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
}
|
||||
let t2;
|
||||
if ($[6] !== props.startingMessage || $[7] !== t1) {
|
||||
t2 = <ConsoleOAuthFlow onDone={t1} startingMessage={props.startingMessage} />;
|
||||
$[6] = props.startingMessage;
|
||||
$[7] = t1;
|
||||
$[8] = t2;
|
||||
} else {
|
||||
t2 = $[8];
|
||||
}
|
||||
let t3;
|
||||
if ($[9] !== t0 || $[10] !== t2) {
|
||||
t3 = <Dialog title="Login" onCancel={t0} color="permission" inputGuide={_temp}>{t2}</Dialog>;
|
||||
$[9] = t0;
|
||||
$[10] = t2;
|
||||
$[11] = t3;
|
||||
} else {
|
||||
t3 = $[11];
|
||||
}
|
||||
return t3;
|
||||
}
|
||||
function _temp(exitState) {
|
||||
return exitState.pending ? <Text>Press {exitState.keyName} again to exit</Text> : <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="cancel" />;
|
||||
>
|
||||
<ConsoleOAuthFlow
|
||||
onDone={() => props.onDone(true, mainLoopModel)}
|
||||
startingMessage={props.startingMessage}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +1,89 @@
|
||||
import * as React from 'react';
|
||||
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js';
|
||||
import { Text } from '../../ink.js';
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js';
|
||||
import { getGroveNoticeConfig, getGroveSettings } from '../../services/api/grove.js';
|
||||
import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js';
|
||||
import * as React from 'react'
|
||||
import { clearTrustedDeviceTokenCache } from '../../bridge/trustedDevice.js'
|
||||
import { Text } from '../../ink.js'
|
||||
import { refreshGrowthBookAfterAuthChange } from '../../services/analytics/growthbook.js'
|
||||
import {
|
||||
getGroveNoticeConfig,
|
||||
getGroveSettings,
|
||||
} from '../../services/api/grove.js'
|
||||
import { clearPolicyLimitsCache } from '../../services/policyLimits/index.js'
|
||||
// flushTelemetry is loaded lazily to avoid pulling in ~1.1MB of OpenTelemetry at startup
|
||||
import { clearRemoteManagedSettingsCache } from '../../services/remoteManagedSettings/index.js';
|
||||
import { getClaudeAIOAuthTokens, removeApiKey } from '../../utils/auth.js';
|
||||
import { clearBetasCaches } from '../../utils/betas.js';
|
||||
import { saveGlobalConfig } from '../../utils/config.js';
|
||||
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js';
|
||||
import { getSecureStorage } from '../../utils/secureStorage/index.js';
|
||||
import { clearToolSchemaCache } from '../../utils/toolSchemaCache.js';
|
||||
import { resetUserCache } from '../../utils/user.js';
|
||||
import { clearRemoteManagedSettingsCache } from '../../services/remoteManagedSettings/index.js'
|
||||
import { getClaudeAIOAuthTokens, removeApiKey } from '../../utils/auth.js'
|
||||
import { clearBetasCaches } from '../../utils/betas.js'
|
||||
import { saveGlobalConfig } from '../../utils/config.js'
|
||||
import { gracefulShutdownSync } from '../../utils/gracefulShutdown.js'
|
||||
import { getSecureStorage } from '../../utils/secureStorage/index.js'
|
||||
import { clearToolSchemaCache } from '../../utils/toolSchemaCache.js'
|
||||
import { resetUserCache } from '../../utils/user.js'
|
||||
|
||||
export async function performLogout({
|
||||
clearOnboarding = false
|
||||
clearOnboarding = false,
|
||||
}): Promise<void> {
|
||||
// Flush telemetry BEFORE clearing credentials to prevent org data leakage
|
||||
const {
|
||||
flushTelemetry
|
||||
} = await import('../../utils/telemetry/instrumentation.js');
|
||||
await flushTelemetry();
|
||||
await removeApiKey();
|
||||
const { flushTelemetry } = await import(
|
||||
'../../utils/telemetry/instrumentation.js'
|
||||
)
|
||||
await flushTelemetry()
|
||||
|
||||
await removeApiKey()
|
||||
|
||||
// Wipe all secure storage data on logout
|
||||
const secureStorage = getSecureStorage();
|
||||
secureStorage.delete();
|
||||
await clearAuthRelatedCaches();
|
||||
const secureStorage = getSecureStorage()
|
||||
secureStorage.delete()
|
||||
|
||||
await clearAuthRelatedCaches()
|
||||
saveGlobalConfig(current => {
|
||||
const updated = {
|
||||
...current
|
||||
};
|
||||
const updated = { ...current }
|
||||
if (clearOnboarding) {
|
||||
updated.hasCompletedOnboarding = false;
|
||||
updated.subscriptionNoticeCount = 0;
|
||||
updated.hasAvailableSubscription = false;
|
||||
updated.hasCompletedOnboarding = false
|
||||
updated.subscriptionNoticeCount = 0
|
||||
updated.hasAvailableSubscription = false
|
||||
if (updated.customApiKeyResponses?.approved) {
|
||||
updated.customApiKeyResponses = {
|
||||
...updated.customApiKeyResponses,
|
||||
approved: []
|
||||
};
|
||||
approved: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
updated.oauthAccount = undefined;
|
||||
return updated;
|
||||
});
|
||||
updated.oauthAccount = undefined
|
||||
return updated
|
||||
})
|
||||
}
|
||||
|
||||
// clearing anything memoized that must be invalidated when user/session/auth changes
|
||||
export async function clearAuthRelatedCaches(): Promise<void> {
|
||||
// Clear the OAuth token cache
|
||||
getClaudeAIOAuthTokens.cache?.clear?.();
|
||||
clearTrustedDeviceTokenCache();
|
||||
clearBetasCaches();
|
||||
clearToolSchemaCache();
|
||||
getClaudeAIOAuthTokens.cache?.clear?.()
|
||||
clearTrustedDeviceTokenCache()
|
||||
clearBetasCaches()
|
||||
clearToolSchemaCache()
|
||||
|
||||
// Clear user data cache BEFORE GrowthBook refresh so it picks up fresh credentials
|
||||
resetUserCache();
|
||||
refreshGrowthBookAfterAuthChange();
|
||||
resetUserCache()
|
||||
refreshGrowthBookAfterAuthChange()
|
||||
|
||||
// Clear Grove config cache
|
||||
getGroveNoticeConfig.cache?.clear?.();
|
||||
getGroveSettings.cache?.clear?.();
|
||||
getGroveNoticeConfig.cache?.clear?.()
|
||||
getGroveSettings.cache?.clear?.()
|
||||
|
||||
// Clear remotely managed settings cache
|
||||
await clearRemoteManagedSettingsCache();
|
||||
await clearRemoteManagedSettingsCache()
|
||||
|
||||
// Clear policy limits cache
|
||||
await clearPolicyLimitsCache();
|
||||
await clearPolicyLimitsCache()
|
||||
}
|
||||
|
||||
export async function call(): Promise<React.ReactNode> {
|
||||
await performLogout({
|
||||
clearOnboarding: true
|
||||
});
|
||||
const message = <Text>Successfully logged out from your Anthropic account.</Text>;
|
||||
await performLogout({ clearOnboarding: true })
|
||||
|
||||
const message = (
|
||||
<Text>Successfully logged out from your Anthropic account.</Text>
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
gracefulShutdownSync(0, 'logout');
|
||||
}, 200);
|
||||
return message;
|
||||
gracefulShutdownSync(0, 'logout')
|
||||
}, 200)
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
@@ -1,84 +1,105 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { MCPSettings } from '../../components/mcp/index.js';
|
||||
import { MCPReconnect } from '../../components/mcp/MCPReconnect.js';
|
||||
import { useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { PluginSettings } from '../plugin/PluginSettings.js';
|
||||
import React, { useEffect, useRef } from 'react'
|
||||
import { MCPSettings } from '../../components/mcp/index.js'
|
||||
import { MCPReconnect } from '../../components/mcp/MCPReconnect.js'
|
||||
import { useMcpToggleEnabled } from '../../services/mcp/MCPConnectionManager.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { PluginSettings } from '../plugin/PluginSettings.js'
|
||||
|
||||
// TODO: This is a hack to get the context value from toggleMcpServer (useContext only works in a component)
|
||||
// Ideally, all MCP state and functions would be in global state.
|
||||
function MCPToggle(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
action,
|
||||
target,
|
||||
onComplete
|
||||
} = t0;
|
||||
const mcpClients = useAppState(_temp);
|
||||
const toggleMcpServer = useMcpToggleEnabled();
|
||||
const didRun = useRef(false);
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== action || $[1] !== mcpClients || $[2] !== onComplete || $[3] !== target || $[4] !== toggleMcpServer) {
|
||||
t1 = () => {
|
||||
if (didRun.current) {
|
||||
return;
|
||||
}
|
||||
didRun.current = true;
|
||||
const isEnabling = action === "enable";
|
||||
const clients = mcpClients.filter(_temp2);
|
||||
const toToggle = target === "all" ? clients.filter(c_0 => isEnabling ? c_0.type === "disabled" : c_0.type !== "disabled") : clients.filter(c_1 => c_1.name === target);
|
||||
if (toToggle.length === 0) {
|
||||
onComplete(target === "all" ? `All MCP servers are already ${isEnabling ? "enabled" : "disabled"}` : `MCP server "${target}" not found`);
|
||||
return;
|
||||
}
|
||||
for (const s_0 of toToggle) {
|
||||
toggleMcpServer(s_0.name);
|
||||
}
|
||||
onComplete(target === "all" ? `${isEnabling ? "Enabled" : "Disabled"} ${toToggle.length} MCP server(s)` : `MCP server "${target}" ${isEnabling ? "enabled" : "disabled"}`);
|
||||
};
|
||||
t2 = [action, target, mcpClients, toggleMcpServer, onComplete];
|
||||
$[0] = action;
|
||||
$[1] = mcpClients;
|
||||
$[2] = onComplete;
|
||||
$[3] = target;
|
||||
$[4] = toggleMcpServer;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
} else {
|
||||
t1 = $[5];
|
||||
t2 = $[6];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
return null;
|
||||
function MCPToggle({
|
||||
action,
|
||||
target,
|
||||
onComplete,
|
||||
}: {
|
||||
action: 'enable' | 'disable'
|
||||
target: string
|
||||
onComplete: (result: string) => void
|
||||
}): null {
|
||||
const mcpClients = useAppState(s => s.mcp.clients)
|
||||
const toggleMcpServer = useMcpToggleEnabled()
|
||||
const didRun = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (didRun.current) return
|
||||
didRun.current = true
|
||||
|
||||
const isEnabling = action === 'enable'
|
||||
const clients = mcpClients.filter(c => c.name !== 'ide')
|
||||
const toToggle =
|
||||
target === 'all'
|
||||
? clients.filter(c =>
|
||||
isEnabling ? c.type === 'disabled' : c.type !== 'disabled',
|
||||
)
|
||||
: clients.filter(c => c.name === target)
|
||||
|
||||
if (toToggle.length === 0) {
|
||||
onComplete(
|
||||
target === 'all'
|
||||
? `All MCP servers are already ${isEnabling ? 'enabled' : 'disabled'}`
|
||||
: `MCP server "${target}" not found`,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
for (const s of toToggle) {
|
||||
void toggleMcpServer(s.name)
|
||||
}
|
||||
|
||||
onComplete(
|
||||
target === 'all'
|
||||
? `${isEnabling ? 'Enabled' : 'Disabled'} ${toToggle.length} MCP server(s)`
|
||||
: `MCP server "${target}" ${isEnabling ? 'enabled' : 'disabled'}`,
|
||||
)
|
||||
}, [action, target, mcpClients, toggleMcpServer, onComplete])
|
||||
|
||||
return null
|
||||
}
|
||||
function _temp2(c) {
|
||||
return c.name !== "ide";
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mcp.clients;
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
if (args) {
|
||||
const parts = args.trim().split(/\s+/);
|
||||
const parts = args.trim().split(/\s+/)
|
||||
|
||||
// Allow /mcp no-redirect to bypass the redirect for testing
|
||||
if (parts[0] === 'no-redirect') {
|
||||
return <MCPSettings onComplete={onDone} />;
|
||||
return <MCPSettings onComplete={onDone} />
|
||||
}
|
||||
|
||||
if (parts[0] === 'reconnect' && parts[1]) {
|
||||
return <MCPReconnect serverName={parts.slice(1).join(' ')} onComplete={onDone} />;
|
||||
return (
|
||||
<MCPReconnect
|
||||
serverName={parts.slice(1).join(' ')}
|
||||
onComplete={onDone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (parts[0] === 'enable' || parts[0] === 'disable') {
|
||||
return <MCPToggle action={parts[0]} target={parts.length > 1 ? parts.slice(1).join(' ') : 'all'} onComplete={onDone} />;
|
||||
return (
|
||||
<MCPToggle
|
||||
action={parts[0]}
|
||||
target={parts.length > 1 ? parts.slice(1).join(' ') : 'all'}
|
||||
onComplete={onDone}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Redirect base /mcp command to /plugins installed tab for ant users
|
||||
if ((process.env.USER_TYPE) === 'ant') {
|
||||
return <PluginSettings onComplete={onDone} args="manage" showMcpRedirectMessage />;
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
return (
|
||||
<PluginSettings
|
||||
onComplete={onDone}
|
||||
args="manage"
|
||||
showMcpRedirectMessage
|
||||
/>
|
||||
)
|
||||
}
|
||||
return <MCPSettings onComplete={onDone} />;
|
||||
|
||||
return <MCPSettings onComplete={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,75 +1,86 @@
|
||||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { MemoryFileSelector } from '../../components/memory/MemoryFileSelector.js';
|
||||
import { getRelativeMemoryPath } from '../../components/memory/MemoryUpdateNotification.js';
|
||||
import { Box, Link, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js';
|
||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js';
|
||||
import { getErrnoCode } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js';
|
||||
import { mkdir, writeFile } from 'fs/promises'
|
||||
import * as React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { MemoryFileSelector } from '../../components/memory/MemoryFileSelector.js'
|
||||
import { getRelativeMemoryPath } from '../../components/memory/MemoryUpdateNotification.js'
|
||||
import { Box, Link, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import { clearMemoryFileCaches, getMemoryFiles } from '../../utils/claudemd.js'
|
||||
import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
||||
import { getErrnoCode } from '../../utils/errors.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js'
|
||||
|
||||
function MemoryCommand({
|
||||
onDone
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const handleSelectMemoryFile = async (memoryPath: string) => {
|
||||
try {
|
||||
// Create claude directory if it doesn't exist (idempotent with recursive)
|
||||
if (memoryPath.includes(getClaudeConfigHomeDir())) {
|
||||
await mkdir(getClaudeConfigHomeDir(), {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(getClaudeConfigHomeDir(), { recursive: true })
|
||||
}
|
||||
|
||||
// Create file if it doesn't exist (wx flag fails if file exists,
|
||||
// which we catch to preserve existing content)
|
||||
try {
|
||||
await writeFile(memoryPath, '', {
|
||||
encoding: 'utf8',
|
||||
flag: 'wx'
|
||||
});
|
||||
await writeFile(memoryPath, '', { encoding: 'utf8', flag: 'wx' })
|
||||
} catch (e: unknown) {
|
||||
if (getErrnoCode(e) !== 'EEXIST') {
|
||||
throw e;
|
||||
throw e
|
||||
}
|
||||
}
|
||||
await editFileInEditor(memoryPath);
|
||||
|
||||
await editFileInEditor(memoryPath)
|
||||
|
||||
// Determine which environment variable controls the editor
|
||||
let editorSource = 'default';
|
||||
let editorValue = '';
|
||||
let editorSource = 'default'
|
||||
let editorValue = ''
|
||||
if (process.env.VISUAL) {
|
||||
editorSource = '$VISUAL';
|
||||
editorValue = process.env.VISUAL;
|
||||
editorSource = '$VISUAL'
|
||||
editorValue = process.env.VISUAL
|
||||
} else if (process.env.EDITOR) {
|
||||
editorSource = '$EDITOR';
|
||||
editorValue = process.env.EDITOR;
|
||||
editorSource = '$EDITOR'
|
||||
editorValue = process.env.EDITOR
|
||||
}
|
||||
const editorInfo = editorSource !== 'default' ? `Using ${editorSource}="${editorValue}".` : '';
|
||||
const editorHint = editorInfo ? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.` : `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`;
|
||||
onDone(`Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`, {
|
||||
display: 'system'
|
||||
});
|
||||
|
||||
const editorInfo =
|
||||
editorSource !== 'default'
|
||||
? `Using ${editorSource}="${editorValue}".`
|
||||
: ''
|
||||
|
||||
const editorHint = editorInfo
|
||||
? `> ${editorInfo} To change editor, set $EDITOR or $VISUAL environment variable.`
|
||||
: `> To use a different editor, set the $EDITOR or $VISUAL environment variable.`
|
||||
|
||||
onDone(
|
||||
`Opened memory file at ${getRelativeMemoryPath(memoryPath)}\n\n${editorHint}`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
onDone(`Error opening memory file: ${error}`);
|
||||
logError(error)
|
||||
onDone(`Error opening memory file: ${error}`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
onDone('Cancelled memory editing', {
|
||||
display: 'system'
|
||||
});
|
||||
};
|
||||
return <Dialog title="Memory" onCancel={handleCancel} color="remember">
|
||||
onDone('Cancelled memory editing', { display: 'system' })
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog title="Memory" onCancel={handleCancel} color="remember">
|
||||
<Box flexDirection="column">
|
||||
<React.Suspense fallback={null}>
|
||||
<MemoryFileSelector onSelect={handleSelectMemoryFile} onCancel={handleCancel} />
|
||||
<MemoryFileSelector
|
||||
onSelect={handleSelectMemoryFile}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</React.Suspense>
|
||||
|
||||
<Box marginTop={1}>
|
||||
@@ -78,12 +89,14 @@ function MemoryCommand({
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
</Dialog>;
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async onDone => {
|
||||
// Clear + prime before rendering — Suspense handles the unprimed case,
|
||||
// but awaiting here avoids a fallback flash on initial open.
|
||||
clearMemoryFileCaches();
|
||||
await getMemoryFiles();
|
||||
return <MemoryCommand onDone={onDone} />;
|
||||
};
|
||||
clearMemoryFileCaches()
|
||||
await getMemoryFiles()
|
||||
return <MemoryCommand onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,273 +1,117 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { toString as qrToString } from 'qrcode';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
type Platform = 'ios' | 'android';
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import type { KeyboardEvent } from '../../ink/events/keyboard-event.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
type Platform = 'ios' | 'android'
|
||||
|
||||
type Props = {
|
||||
onDone: () => void;
|
||||
};
|
||||
const PLATFORMS: Record<Platform, {
|
||||
url: string;
|
||||
}> = {
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const PLATFORMS: Record<Platform, { url: string }> = {
|
||||
ios: {
|
||||
url: 'https://apps.apple.com/app/claude-by-anthropic/id6473753684'
|
||||
url: 'https://apps.apple.com/app/claude-by-anthropic/id6473753684',
|
||||
},
|
||||
android: {
|
||||
url: 'https://play.google.com/store/apps/details?id=com.anthropic.claude'
|
||||
}
|
||||
};
|
||||
function MobileQRCode(t0) {
|
||||
const $ = _c(52);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const [platform, setPlatform] = useState("ios");
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = {
|
||||
ios: "",
|
||||
android: ""
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const [qrCodes, setQrCodes] = useState(t1);
|
||||
const {
|
||||
url
|
||||
} = PLATFORMS[platform];
|
||||
const qrCode = qrCodes[platform];
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = () => {
|
||||
const generateQRCodes = async function generateQRCodes() {
|
||||
const [ios, android] = await Promise.all([qrToString(PLATFORMS.ios.url, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L"
|
||||
}), qrToString(PLATFORMS.android.url, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L"
|
||||
})]);
|
||||
setQrCodes({
|
||||
ios,
|
||||
android
|
||||
});
|
||||
};
|
||||
generateQRCodes().catch(_temp);
|
||||
};
|
||||
t3 = [];
|
||||
$[1] = t2;
|
||||
$[2] = t3;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
t3 = $[2];
|
||||
}
|
||||
useEffect(t2, t3);
|
||||
let t4;
|
||||
if ($[3] !== onDone) {
|
||||
t4 = () => {
|
||||
onDone();
|
||||
};
|
||||
$[3] = onDone;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
const handleClose = t4;
|
||||
let t5;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[5] = t5;
|
||||
} else {
|
||||
t5 = $[5];
|
||||
}
|
||||
useKeybinding("confirm:no", handleClose, t5);
|
||||
let t6;
|
||||
if ($[6] !== onDone) {
|
||||
t6 = function handleKeyDown(e) {
|
||||
if (e.key === "q" || e.ctrl && e.key === "c") {
|
||||
e.preventDefault();
|
||||
onDone();
|
||||
return;
|
||||
}
|
||||
if (e.key === "tab" || e.key === "left" || e.key === "right") {
|
||||
e.preventDefault();
|
||||
setPlatform(_temp2);
|
||||
}
|
||||
};
|
||||
$[6] = onDone;
|
||||
$[7] = t6;
|
||||
} else {
|
||||
t6 = $[7];
|
||||
}
|
||||
const handleKeyDown = t6;
|
||||
let T0;
|
||||
let T1;
|
||||
let t10;
|
||||
let t11;
|
||||
let t12;
|
||||
let t13;
|
||||
let t7;
|
||||
let t8;
|
||||
let t9;
|
||||
if ($[8] !== handleKeyDown || $[9] !== qrCode) {
|
||||
const lines = qrCode.split("\n").filter(_temp3);
|
||||
T1 = Pane;
|
||||
T0 = Box;
|
||||
t7 = "column";
|
||||
t8 = 0;
|
||||
t9 = true;
|
||||
t10 = handleKeyDown;
|
||||
if ($[19] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t11 = <Text> </Text>;
|
||||
t12 = <Text> </Text>;
|
||||
$[19] = t11;
|
||||
$[20] = t12;
|
||||
} else {
|
||||
t11 = $[19];
|
||||
t12 = $[20];
|
||||
url: 'https://play.google.com/store/apps/details?id=com.anthropic.claude',
|
||||
},
|
||||
}
|
||||
|
||||
function MobileQRCode({ onDone }: Props): React.ReactNode {
|
||||
const [platform, setPlatform] = useState<Platform>('ios')
|
||||
const [qrCodes, setQrCodes] = useState<Record<Platform, string>>({
|
||||
ios: '',
|
||||
android: '',
|
||||
})
|
||||
|
||||
const { url } = PLATFORMS[platform]
|
||||
const qrCode = qrCodes[platform]
|
||||
|
||||
// Generate both QR codes upfront to avoid flicker when switching
|
||||
useEffect(() => {
|
||||
async function generateQRCodes(): Promise<void> {
|
||||
const [ios, android] = await Promise.all([
|
||||
qrToString(PLATFORMS.ios.url, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
}),
|
||||
qrToString(PLATFORMS.android.url, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
}),
|
||||
])
|
||||
setQrCodes({ ios, android })
|
||||
}
|
||||
generateQRCodes().catch(() => {
|
||||
// QR generation failed, leave empty
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
onDone()
|
||||
}, [onDone])
|
||||
|
||||
useKeybinding('confirm:no', handleClose, { context: 'Confirmation' })
|
||||
|
||||
function handleKeyDown(e: KeyboardEvent): void {
|
||||
if (e.key === 'q' || (e.ctrl && e.key === 'c')) {
|
||||
e.preventDefault()
|
||||
onDone()
|
||||
return
|
||||
}
|
||||
if (e.key === 'tab' || e.key === 'left' || e.key === 'right') {
|
||||
e.preventDefault()
|
||||
setPlatform(prev => (prev === 'ios' ? 'android' : 'ios'))
|
||||
}
|
||||
t13 = lines.map(_temp4);
|
||||
$[8] = handleKeyDown;
|
||||
$[9] = qrCode;
|
||||
$[10] = T0;
|
||||
$[11] = T1;
|
||||
$[12] = t10;
|
||||
$[13] = t11;
|
||||
$[14] = t12;
|
||||
$[15] = t13;
|
||||
$[16] = t7;
|
||||
$[17] = t8;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
T0 = $[10];
|
||||
T1 = $[11];
|
||||
t10 = $[12];
|
||||
t11 = $[13];
|
||||
t12 = $[14];
|
||||
t13 = $[15];
|
||||
t7 = $[16];
|
||||
t8 = $[17];
|
||||
t9 = $[18];
|
||||
}
|
||||
let t14;
|
||||
let t15;
|
||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t14 = <Text> </Text>;
|
||||
t15 = <Text> </Text>;
|
||||
$[21] = t14;
|
||||
$[22] = t15;
|
||||
} else {
|
||||
t14 = $[21];
|
||||
t15 = $[22];
|
||||
}
|
||||
const t16 = platform === "ios";
|
||||
const t17 = platform === "ios";
|
||||
let t18;
|
||||
if ($[23] !== t16 || $[24] !== t17) {
|
||||
t18 = <Text bold={t16} underline={t17}>iOS</Text>;
|
||||
$[23] = t16;
|
||||
$[24] = t17;
|
||||
$[25] = t18;
|
||||
} else {
|
||||
t18 = $[25];
|
||||
}
|
||||
let t19;
|
||||
if ($[26] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t19 = <Text dimColor={true}>{" / "}</Text>;
|
||||
$[26] = t19;
|
||||
} else {
|
||||
t19 = $[26];
|
||||
}
|
||||
const t20 = platform === "android";
|
||||
const t21 = platform === "android";
|
||||
let t22;
|
||||
if ($[27] !== t20 || $[28] !== t21) {
|
||||
t22 = <Text bold={t20} underline={t21}>Android</Text>;
|
||||
$[27] = t20;
|
||||
$[28] = t21;
|
||||
$[29] = t22;
|
||||
} else {
|
||||
t22 = $[29];
|
||||
}
|
||||
let t23;
|
||||
if ($[30] !== t18 || $[31] !== t22) {
|
||||
t23 = <Text>{t18}{t19}{t22}</Text>;
|
||||
$[30] = t18;
|
||||
$[31] = t22;
|
||||
$[32] = t23;
|
||||
} else {
|
||||
t23 = $[32];
|
||||
}
|
||||
let t24;
|
||||
if ($[33] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t24 = <Text dimColor={true}>(tab to switch, esc to close)</Text>;
|
||||
$[33] = t24;
|
||||
} else {
|
||||
t24 = $[33];
|
||||
}
|
||||
let t25;
|
||||
if ($[34] !== t23) {
|
||||
t25 = <Box flexDirection="row" gap={2}>{t23}{t24}</Box>;
|
||||
$[34] = t23;
|
||||
$[35] = t25;
|
||||
} else {
|
||||
t25 = $[35];
|
||||
}
|
||||
let t26;
|
||||
if ($[36] !== url) {
|
||||
t26 = <Text dimColor={true}>{url}</Text>;
|
||||
$[36] = url;
|
||||
$[37] = t26;
|
||||
} else {
|
||||
t26 = $[37];
|
||||
}
|
||||
let t27;
|
||||
if ($[38] !== T0 || $[39] !== t10 || $[40] !== t11 || $[41] !== t12 || $[42] !== t13 || $[43] !== t25 || $[44] !== t26 || $[45] !== t7 || $[46] !== t8 || $[47] !== t9) {
|
||||
t27 = <T0 flexDirection={t7} tabIndex={t8} autoFocus={t9} onKeyDown={t10}>{t11}{t12}{t13}{t14}{t15}{t25}{t26}</T0>;
|
||||
$[38] = T0;
|
||||
$[39] = t10;
|
||||
$[40] = t11;
|
||||
$[41] = t12;
|
||||
$[42] = t13;
|
||||
$[43] = t25;
|
||||
$[44] = t26;
|
||||
$[45] = t7;
|
||||
$[46] = t8;
|
||||
$[47] = t9;
|
||||
$[48] = t27;
|
||||
} else {
|
||||
t27 = $[48];
|
||||
}
|
||||
let t28;
|
||||
if ($[49] !== T1 || $[50] !== t27) {
|
||||
t28 = <T1>{t27}</T1>;
|
||||
$[49] = T1;
|
||||
$[50] = t27;
|
||||
$[51] = t28;
|
||||
} else {
|
||||
t28 = $[51];
|
||||
}
|
||||
return t28;
|
||||
|
||||
const lines = qrCode.split('\n').filter(line => line.length > 0)
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
tabIndex={0}
|
||||
autoFocus
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<Text> </Text>
|
||||
<Text> </Text>
|
||||
{lines.map((line, i) => (
|
||||
<Text key={i}>{line}</Text>
|
||||
))}
|
||||
<Text> </Text>
|
||||
<Text> </Text>
|
||||
|
||||
{/* Controls */}
|
||||
<Box flexDirection="row" gap={2}>
|
||||
<Text>
|
||||
<Text bold={platform === 'ios'} underline={platform === 'ios'}>
|
||||
iOS
|
||||
</Text>
|
||||
<Text dimColor>{' / '}</Text>
|
||||
<Text
|
||||
bold={platform === 'android'}
|
||||
underline={platform === 'android'}
|
||||
>
|
||||
Android
|
||||
</Text>
|
||||
</Text>
|
||||
<Text dimColor>(tab to switch, esc to close)</Text>
|
||||
</Box>
|
||||
<Text dimColor>{url}</Text>
|
||||
</Box>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
function _temp4(line_0, i) {
|
||||
return <Text key={i}>{line_0}</Text>;
|
||||
}
|
||||
function _temp3(line) {
|
||||
return line.length > 0;
|
||||
}
|
||||
function _temp2(prev) {
|
||||
return prev === "ios" ? "android" : "ios";
|
||||
}
|
||||
function _temp() {}
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
return <MobileQRCode onDone={onDone} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
return <MobileQRCode onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,296 +1,337 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { ModelPicker } from '../../components/ModelPicker.js';
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { EffortLevel } from '../../utils/effort.js';
|
||||
import { isBilledAsExtraUsage } from '../../utils/extraUsage.js';
|
||||
import { clearFastModeCooldown, isFastModeAvailable, isFastModeEnabled, isFastModeSupportedByModel } from '../../utils/fastMode.js';
|
||||
import { MODEL_ALIASES } from '../../utils/model/aliases.js';
|
||||
import { checkOpus1mAccess, checkSonnet1mAccess } from '../../utils/model/check1mAccess.js';
|
||||
import { getDefaultMainLoopModelSetting, isOpus1mMergeEnabled, renderDefaultModelSetting } from '../../utils/model/model.js';
|
||||
import { isModelAllowed } from '../../utils/model/modelAllowlist.js';
|
||||
import { validateModel } from '../../utils/model/validateModel.js';
|
||||
function ModelPickerWrapper(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const mainLoopModel = useAppState(_temp);
|
||||
const mainLoopModelForSession = useAppState(_temp2);
|
||||
const isFastMode = useAppState(_temp3);
|
||||
const setAppState = useSetAppState();
|
||||
let t1;
|
||||
if ($[0] !== mainLoopModel || $[1] !== onDone) {
|
||||
t1 = function handleCancel() {
|
||||
logEvent("tengu_model_command_menu", {
|
||||
action: "cancel" as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
const displayModel = renderModelLabel(mainLoopModel);
|
||||
onDone(`Kept model as ${chalk.bold(displayModel)}`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[0] = mainLoopModel;
|
||||
$[1] = onDone;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
import chalk from 'chalk'
|
||||
import * as React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { ModelPicker } from '../../components/ModelPicker.js'
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import { useAppState, useSetAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { EffortLevel } from '../../utils/effort.js'
|
||||
import { isBilledAsExtraUsage } from '../../utils/extraUsage.js'
|
||||
import {
|
||||
clearFastModeCooldown,
|
||||
isFastModeAvailable,
|
||||
isFastModeEnabled,
|
||||
isFastModeSupportedByModel,
|
||||
} from '../../utils/fastMode.js'
|
||||
import { MODEL_ALIASES } from '../../utils/model/aliases.js'
|
||||
import {
|
||||
checkOpus1mAccess,
|
||||
checkSonnet1mAccess,
|
||||
} from '../../utils/model/check1mAccess.js'
|
||||
import {
|
||||
getDefaultMainLoopModelSetting,
|
||||
isOpus1mMergeEnabled,
|
||||
renderDefaultModelSetting,
|
||||
} from '../../utils/model/model.js'
|
||||
import { isModelAllowed } from '../../utils/model/modelAllowlist.js'
|
||||
import { validateModel } from '../../utils/model/validateModel.js'
|
||||
|
||||
function ModelPickerWrapper({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const mainLoopModel = useAppState(s => s.mainLoopModel)
|
||||
const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession)
|
||||
const isFastMode = useAppState(s => s.fastMode)
|
||||
const setAppState = useSetAppState()
|
||||
|
||||
function handleCancel(): void {
|
||||
logEvent('tengu_model_command_menu', {
|
||||
action:
|
||||
'cancel' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
const displayModel = renderModelLabel(mainLoopModel)
|
||||
onDone(`Kept model as ${chalk.bold(displayModel)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
}
|
||||
const handleCancel = t1;
|
||||
let t2;
|
||||
if ($[3] !== isFastMode || $[4] !== mainLoopModel || $[5] !== onDone || $[6] !== setAppState) {
|
||||
t2 = function handleSelect(model, effort) {
|
||||
logEvent("tengu_model_command_menu", {
|
||||
action: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
from_model: mainLoopModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
to_model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
mainLoopModel: model,
|
||||
mainLoopModelForSession: null
|
||||
}));
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(model))}`;
|
||||
if (effort !== undefined) {
|
||||
message = message + ` with ${chalk.bold(effort)} effort`;
|
||||
|
||||
function handleSelect(
|
||||
model: string | null,
|
||||
effort: EffortLevel | undefined,
|
||||
): void {
|
||||
logEvent('tengu_model_command_menu', {
|
||||
action:
|
||||
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
from_model:
|
||||
mainLoopModel as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
to_model:
|
||||
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
mainLoopModel: model,
|
||||
mainLoopModelForSession: null,
|
||||
}))
|
||||
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(model))}`
|
||||
if (effort !== undefined) {
|
||||
message += ` with ${chalk.bold(effort)} effort`
|
||||
}
|
||||
|
||||
// Turn off fast mode if switching to unsupported model
|
||||
let wasFastModeToggledOn = undefined
|
||||
if (isFastModeEnabled()) {
|
||||
clearFastModeCooldown()
|
||||
if (!isFastModeSupportedByModel(model) && isFastMode) {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
fastMode: false,
|
||||
}))
|
||||
wasFastModeToggledOn = false
|
||||
// Do not update fast mode in settings since this is an automatic downgrade
|
||||
} else if (
|
||||
isFastModeSupportedByModel(model) &&
|
||||
isFastModeAvailable() &&
|
||||
isFastMode
|
||||
) {
|
||||
message += ` · Fast mode ON`
|
||||
wasFastModeToggledOn = true
|
||||
}
|
||||
let wasFastModeToggledOn = undefined;
|
||||
if (isFastModeEnabled()) {
|
||||
clearFastModeCooldown();
|
||||
if (!isFastModeSupportedByModel(model) && isFastMode) {
|
||||
setAppState(_temp4);
|
||||
wasFastModeToggledOn = false;
|
||||
} else {
|
||||
if (isFastModeSupportedByModel(model) && isFastModeAvailable() && isFastMode) {
|
||||
message = message + " \xB7 Fast mode ON";
|
||||
wasFastModeToggledOn = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isBilledAsExtraUsage(model, wasFastModeToggledOn === true, isOpus1mMergeEnabled())) {
|
||||
message = message + " \xB7 Billed as extra usage";
|
||||
}
|
||||
if (wasFastModeToggledOn === false) {
|
||||
message = message + " \xB7 Fast mode OFF";
|
||||
}
|
||||
onDone(message);
|
||||
};
|
||||
$[3] = isFastMode;
|
||||
$[4] = mainLoopModel;
|
||||
$[5] = onDone;
|
||||
$[6] = setAppState;
|
||||
$[7] = t2;
|
||||
} else {
|
||||
t2 = $[7];
|
||||
}
|
||||
|
||||
if (
|
||||
isBilledAsExtraUsage(
|
||||
model,
|
||||
wasFastModeToggledOn === true,
|
||||
isOpus1mMergeEnabled(),
|
||||
)
|
||||
) {
|
||||
message += ` · Billed as extra usage`
|
||||
}
|
||||
|
||||
if (wasFastModeToggledOn === false) {
|
||||
// Fast mode was toggled off, show suffix after extra usage billing
|
||||
message += ` · Fast mode OFF`
|
||||
}
|
||||
|
||||
onDone(message)
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[8] !== isFastMode || $[9] !== mainLoopModel) {
|
||||
t3 = isFastModeEnabled() && isFastMode && isFastModeSupportedByModel(mainLoopModel) && isFastModeAvailable();
|
||||
$[8] = isFastMode;
|
||||
$[9] = mainLoopModel;
|
||||
$[10] = t3;
|
||||
} else {
|
||||
t3 = $[10];
|
||||
}
|
||||
let t4;
|
||||
if ($[11] !== handleCancel || $[12] !== handleSelect || $[13] !== mainLoopModel || $[14] !== mainLoopModelForSession || $[15] !== t3) {
|
||||
t4 = <ModelPicker initial={mainLoopModel} sessionModel={mainLoopModelForSession} onSelect={handleSelect} onCancel={handleCancel} isStandaloneCommand={true} showFastModeNotice={t3} />;
|
||||
$[11] = handleCancel;
|
||||
$[12] = handleSelect;
|
||||
$[13] = mainLoopModel;
|
||||
$[14] = mainLoopModelForSession;
|
||||
$[15] = t3;
|
||||
$[16] = t4;
|
||||
} else {
|
||||
t4 = $[16];
|
||||
}
|
||||
return t4;
|
||||
}
|
||||
function _temp4(prev_0) {
|
||||
return {
|
||||
...prev_0,
|
||||
fastMode: false
|
||||
};
|
||||
}
|
||||
function _temp3(s_1) {
|
||||
return s_1.fastMode;
|
||||
}
|
||||
function _temp2(s_0) {
|
||||
return s_0.mainLoopModelForSession;
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.mainLoopModel;
|
||||
|
||||
return (
|
||||
<ModelPicker
|
||||
initial={mainLoopModel}
|
||||
sessionModel={mainLoopModelForSession}
|
||||
onSelect={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
isStandaloneCommand
|
||||
showFastModeNotice={
|
||||
isFastModeEnabled() &&
|
||||
isFastMode &&
|
||||
isFastModeSupportedByModel(mainLoopModel) &&
|
||||
isFastModeAvailable()
|
||||
}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SetModelAndClose({
|
||||
args,
|
||||
onDone
|
||||
onDone,
|
||||
}: {
|
||||
args: string;
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
args: string
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const isFastMode = useAppState(s => s.fastMode);
|
||||
const setAppState = useSetAppState();
|
||||
const model = args === 'default' ? null : args;
|
||||
const isFastMode = useAppState(s => s.fastMode)
|
||||
const setAppState = useSetAppState()
|
||||
const model = args === 'default' ? null : args
|
||||
|
||||
React.useEffect(() => {
|
||||
async function handleModelChange(): Promise<void> {
|
||||
if (model && !isModelAllowed(model)) {
|
||||
onDone(`Model '${model}' is not available. Your organization restricts model selection.`, {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
`Model '${model}' is not available. Your organization restricts model selection.`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// @[MODEL LAUNCH]: Update check for 1M access.
|
||||
if (model && isOpus1mUnavailable(model)) {
|
||||
onDone(`Opus 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`, {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
`Opus 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (model && isSonnet1mUnavailable(model)) {
|
||||
onDone(`Sonnet 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`, {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
`Sonnet 4.6 with 1M context is not available for your account. Learn more: https://code.claude.com/docs/en/model-config#extended-context-with-1m`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip validation for default model
|
||||
if (!model) {
|
||||
setModel(null);
|
||||
return;
|
||||
setModel(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Skip validation for known aliases - they're predefined and should work
|
||||
if (isKnownAlias(model)) {
|
||||
setModel(model);
|
||||
return;
|
||||
setModel(model)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate and set custom model
|
||||
try {
|
||||
// Don't use parseUserSpecifiedModel for non-aliases since it lowercases the input
|
||||
// and model names are case-sensitive
|
||||
const {
|
||||
valid,
|
||||
error: error_0
|
||||
} = await validateModel(model);
|
||||
const { valid, error } = await validateModel(model)
|
||||
|
||||
if (valid) {
|
||||
setModel(model);
|
||||
setModel(model)
|
||||
} else {
|
||||
onDone(error_0 || `Model '${model}' not found`, {
|
||||
display: 'system'
|
||||
});
|
||||
onDone(error || `Model '${model}' not found`, {
|
||||
display: 'system',
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
onDone(`Failed to validate model: ${(error as Error).message}`, {
|
||||
display: 'system'
|
||||
});
|
||||
display: 'system',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function setModel(modelValue: string | null): void {
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
mainLoopModel: modelValue,
|
||||
mainLoopModelForSession: null
|
||||
}));
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(modelValue))}`;
|
||||
let wasFastModeToggledOn = undefined;
|
||||
mainLoopModelForSession: null,
|
||||
}))
|
||||
let message = `Set model to ${chalk.bold(renderModelLabel(modelValue))}`
|
||||
|
||||
let wasFastModeToggledOn = undefined
|
||||
if (isFastModeEnabled()) {
|
||||
clearFastModeCooldown();
|
||||
clearFastModeCooldown()
|
||||
if (!isFastModeSupportedByModel(modelValue) && isFastMode) {
|
||||
setAppState(prev_0 => ({
|
||||
...prev_0,
|
||||
fastMode: false
|
||||
}));
|
||||
wasFastModeToggledOn = false;
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
fastMode: false,
|
||||
}))
|
||||
wasFastModeToggledOn = false
|
||||
// Do not update fast mode in settings since this is an automatic downgrade
|
||||
} else if (isFastModeSupportedByModel(modelValue) && isFastMode) {
|
||||
message += ` · Fast mode ON`;
|
||||
wasFastModeToggledOn = true;
|
||||
message += ` · Fast mode ON`
|
||||
wasFastModeToggledOn = true
|
||||
}
|
||||
}
|
||||
if (isBilledAsExtraUsage(modelValue, wasFastModeToggledOn === true, isOpus1mMergeEnabled())) {
|
||||
message += ` · Billed as extra usage`;
|
||||
|
||||
if (
|
||||
isBilledAsExtraUsage(
|
||||
modelValue,
|
||||
wasFastModeToggledOn === true,
|
||||
isOpus1mMergeEnabled(),
|
||||
)
|
||||
) {
|
||||
message += ` · Billed as extra usage`
|
||||
}
|
||||
|
||||
if (wasFastModeToggledOn === false) {
|
||||
// Fast mode was toggled off, show suffix after extra usage billing
|
||||
message += ` · Fast mode OFF`;
|
||||
message += ` · Fast mode OFF`
|
||||
}
|
||||
onDone(message);
|
||||
|
||||
onDone(message)
|
||||
}
|
||||
void handleModelChange();
|
||||
}, [model, onDone, setAppState]);
|
||||
return null;
|
||||
|
||||
void handleModelChange()
|
||||
}, [model, onDone, setAppState])
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function isKnownAlias(model: string): boolean {
|
||||
return (MODEL_ALIASES as readonly string[]).includes(model.toLowerCase().trim());
|
||||
return (MODEL_ALIASES as readonly string[]).includes(
|
||||
model.toLowerCase().trim(),
|
||||
)
|
||||
}
|
||||
|
||||
function isOpus1mUnavailable(model: string): boolean {
|
||||
const m = model.toLowerCase();
|
||||
return !checkOpus1mAccess() && !isOpus1mMergeEnabled() && m.includes('opus') && m.includes('[1m]');
|
||||
const m = model.toLowerCase()
|
||||
return (
|
||||
!checkOpus1mAccess() &&
|
||||
!isOpus1mMergeEnabled() &&
|
||||
m.includes('opus') &&
|
||||
m.includes('[1m]')
|
||||
)
|
||||
}
|
||||
|
||||
function isSonnet1mUnavailable(model: string): boolean {
|
||||
const m = model.toLowerCase();
|
||||
const m = model.toLowerCase()
|
||||
// Warn about Sonnet and Sonnet 4.6, but not Sonnet 4.5 since that had
|
||||
// a different access criteria.
|
||||
return !checkSonnet1mAccess() && (m.includes('sonnet[1m]') || m.includes('sonnet-4-6[1m]'));
|
||||
return (
|
||||
!checkSonnet1mAccess() &&
|
||||
(m.includes('sonnet[1m]') || m.includes('sonnet-4-6[1m]'))
|
||||
)
|
||||
}
|
||||
function ShowModelAndClose(t0) {
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const mainLoopModel = useAppState(_temp7);
|
||||
const mainLoopModelForSession = useAppState(_temp8);
|
||||
const effortValue = useAppState(_temp9);
|
||||
const displayModel = renderModelLabel(mainLoopModel);
|
||||
const effortInfo = effortValue !== undefined ? ` (effort: ${effortValue})` : "";
|
||||
|
||||
function ShowModelAndClose({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (result?: string) => void
|
||||
}): React.ReactNode {
|
||||
const mainLoopModel = useAppState(s => s.mainLoopModel)
|
||||
const mainLoopModelForSession = useAppState(s => s.mainLoopModelForSession)
|
||||
const effortValue = useAppState(s => s.effortValue)
|
||||
const displayModel = renderModelLabel(mainLoopModel)
|
||||
const effortInfo =
|
||||
effortValue !== undefined ? ` (effort: ${effortValue})` : ''
|
||||
|
||||
if (mainLoopModelForSession) {
|
||||
onDone(`Current model: ${chalk.bold(renderModelLabel(mainLoopModelForSession))} (session override from plan mode)\nBase model: ${displayModel}${effortInfo}`);
|
||||
onDone(
|
||||
`Current model: ${chalk.bold(renderModelLabel(mainLoopModelForSession))} (session override from plan mode)\nBase model: ${displayModel}${effortInfo}`,
|
||||
)
|
||||
} else {
|
||||
onDone(`Current model: ${displayModel}${effortInfo}`);
|
||||
onDone(`Current model: ${displayModel}${effortInfo}`)
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function _temp9(s_1) {
|
||||
return s_1.effortValue;
|
||||
}
|
||||
function _temp8(s_0) {
|
||||
return s_0.mainLoopModelForSession;
|
||||
}
|
||||
function _temp7(s) {
|
||||
return s.mainLoopModel;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, _context, args) => {
|
||||
args = args?.trim() || '';
|
||||
args = args?.trim() || ''
|
||||
if (COMMON_INFO_ARGS.includes(args)) {
|
||||
logEvent('tengu_model_command_inline_help', {
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return <ShowModelAndClose onDone={onDone} />;
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return <ShowModelAndClose onDone={onDone} />
|
||||
}
|
||||
if (COMMON_HELP_ARGS.includes(args)) {
|
||||
onDone('Run /model to open the model selection menu, or /model [modelName] to set the model.', {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
onDone(
|
||||
'Run /model to open the model selection menu, or /model [modelName] to set the model.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
if (args) {
|
||||
logEvent('tengu_model_command_inline', {
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return <SetModelAndClose args={args} onDone={onDone} />;
|
||||
args: args as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return <SetModelAndClose args={args} onDone={onDone} />
|
||||
}
|
||||
return <ModelPickerWrapper onDone={onDone} />;
|
||||
};
|
||||
function renderModelLabel(model: string | null): string {
|
||||
const rendered = renderDefaultModelSetting(model ?? getDefaultMainLoopModelSetting());
|
||||
return model === null ? `${rendered} (default)` : rendered;
|
||||
|
||||
return <ModelPickerWrapper onDone={onDone} />
|
||||
}
|
||||
|
||||
function renderModelLabel(model: string | null): string {
|
||||
const rendered = renderDefaultModelSetting(
|
||||
model ?? getDefaultMainLoopModelSetting(),
|
||||
)
|
||||
return model === null ? `${rendered} (default)` : rendered
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<undefined> {
|
||||
onDone('/output-style has been deprecated. Use /config to change your output style, or set it in your settings file. Changes take effect on the next session.', {
|
||||
display: 'system'
|
||||
});
|
||||
onDone(
|
||||
'/output-style has been deprecated. Use /config to change your output style, or set it in your settings file. Changes take effect on the next session.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,23 +1,24 @@
|
||||
import * as React from 'react';
|
||||
import { Passes } from '../../components/Passes/Passes.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { getCachedRemainingPasses } from '../../services/api/referral.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
import * as React from 'react'
|
||||
import { Passes } from '../../components/Passes/Passes.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { getCachedRemainingPasses } from '../../services/api/referral.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
// Mark that user has visited /passes so we stop showing the upsell
|
||||
const config = getGlobalConfig();
|
||||
const isFirstVisit = !config.hasVisitedPasses;
|
||||
const config = getGlobalConfig()
|
||||
const isFirstVisit = !config.hasVisitedPasses
|
||||
if (isFirstVisit) {
|
||||
const remaining = getCachedRemainingPasses();
|
||||
const remaining = getCachedRemainingPasses()
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
hasVisitedPasses: true,
|
||||
passesLastSeenRemaining: remaining ?? current.passesLastSeenRemaining
|
||||
}));
|
||||
passesLastSeenRemaining: remaining ?? current.passesLastSeenRemaining,
|
||||
}))
|
||||
}
|
||||
logEvent('tengu_guest_passes_visited', {
|
||||
is_first_visit: isFirstVisit
|
||||
});
|
||||
return <Passes onDone={onDone} />;
|
||||
logEvent('tengu_guest_passes_visited', { is_first_visit: isFirstVisit })
|
||||
return <Passes onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
import * as React from 'react';
|
||||
import { PermissionRuleList } from '../../components/permissions/rules/PermissionRuleList.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import { createPermissionRetryMessage } from '../../utils/messages.js';
|
||||
import * as React from 'react'
|
||||
import { PermissionRuleList } from '../../components/permissions/rules/PermissionRuleList.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import { createPermissionRetryMessage } from '../../utils/messages.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
return <PermissionRuleList onExit={onDone} onRetryDenials={commands => {
|
||||
context.setMessages(prev => [...prev, createPermissionRetryMessage(commands)]);
|
||||
}} />;
|
||||
};
|
||||
return (
|
||||
<PermissionRuleList
|
||||
onExit={onDone}
|
||||
onRetryDenials={commands => {
|
||||
context.setMessages(prev => [
|
||||
...prev,
|
||||
createPermissionRetryMessage(commands),
|
||||
])
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,121 +1,107 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import { handlePlanModeTransition } from '../../bootstrap/state.js';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getExternalEditor } from '../../utils/editor.js';
|
||||
import { toIDEDisplayName } from '../../utils/ide.js';
|
||||
import { applyPermissionUpdate } from '../../utils/permissions/PermissionUpdate.js';
|
||||
import { prepareContextForPlanMode } from '../../utils/permissions/permissionSetup.js';
|
||||
import { getPlan, getPlanFilePath } from '../../utils/plans.js';
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js';
|
||||
import { renderToString } from '../../utils/staticRender.js';
|
||||
function PlanDisplay(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
planContent,
|
||||
planPath,
|
||||
editorName
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text bold={true}>Current Plan</Text>;
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
let t2;
|
||||
if ($[1] !== planPath) {
|
||||
t2 = <Text dimColor={true}>{planPath}</Text>;
|
||||
$[1] = planPath;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
let t3;
|
||||
if ($[3] !== planContent) {
|
||||
t3 = <Box marginTop={1}><Text>{planContent}</Text></Box>;
|
||||
$[3] = planContent;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== editorName) {
|
||||
t4 = editorName && <Box marginTop={1}><Text dimColor={true}>"/plan open"</Text><Text dimColor={true}> to edit this plan in </Text><Text bold={true} dimColor={true}>{editorName}</Text></Box>;
|
||||
$[5] = editorName;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t2 || $[8] !== t3 || $[9] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t1}{t2}{t3}{t4}</Box>;
|
||||
$[7] = t2;
|
||||
$[8] = t3;
|
||||
$[9] = t4;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
return t5;
|
||||
import * as React from 'react'
|
||||
import { handlePlanModeTransition } from '../../bootstrap/state.js'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { getExternalEditor } from '../../utils/editor.js'
|
||||
import { toIDEDisplayName } from '../../utils/ide.js'
|
||||
import { applyPermissionUpdate } from '../../utils/permissions/PermissionUpdate.js'
|
||||
import { prepareContextForPlanMode } from '../../utils/permissions/permissionSetup.js'
|
||||
import { getPlan, getPlanFilePath } from '../../utils/plans.js'
|
||||
import { editFileInEditor } from '../../utils/promptEditor.js'
|
||||
import { renderToString } from '../../utils/staticRender.js'
|
||||
|
||||
function PlanDisplay({
|
||||
planContent,
|
||||
planPath,
|
||||
editorName,
|
||||
}: {
|
||||
planContent: string
|
||||
planPath: string
|
||||
editorName: string | undefined
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold>Current Plan</Text>
|
||||
<Text dimColor>{planPath}</Text>
|
||||
<Box marginTop={1}>
|
||||
<Text>{planContent}</Text>
|
||||
</Box>
|
||||
{editorName && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>"/plan open"</Text>
|
||||
<Text dimColor> to edit this plan in </Text>
|
||||
<Text bold dimColor>
|
||||
{editorName}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext, args: string): Promise<React.ReactNode> {
|
||||
const {
|
||||
getAppState,
|
||||
setAppState
|
||||
} = context;
|
||||
const appState = getAppState();
|
||||
const currentMode = appState.toolPermissionContext.mode;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
args: string,
|
||||
): Promise<React.ReactNode> {
|
||||
const { getAppState, setAppState } = context
|
||||
const appState = getAppState()
|
||||
const currentMode = appState.toolPermissionContext.mode
|
||||
|
||||
// If not in plan mode, enable it
|
||||
if (currentMode !== 'plan') {
|
||||
handlePlanModeTransition(currentMode, 'plan');
|
||||
handlePlanModeTransition(currentMode, 'plan')
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
toolPermissionContext: applyPermissionUpdate(prepareContextForPlanMode(prev.toolPermissionContext), {
|
||||
type: 'setMode',
|
||||
mode: 'plan',
|
||||
destination: 'session'
|
||||
})
|
||||
}));
|
||||
const description = args.trim();
|
||||
toolPermissionContext: applyPermissionUpdate(
|
||||
prepareContextForPlanMode(prev.toolPermissionContext),
|
||||
{ type: 'setMode', mode: 'plan', destination: 'session' },
|
||||
),
|
||||
}))
|
||||
const description = args.trim()
|
||||
if (description && description !== 'open') {
|
||||
onDone('Enabled plan mode', {
|
||||
shouldQuery: true
|
||||
});
|
||||
onDone('Enabled plan mode', { shouldQuery: true })
|
||||
} else {
|
||||
onDone('Enabled plan mode');
|
||||
onDone('Enabled plan mode')
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
// Already in plan mode - show the current plan
|
||||
const planContent = getPlan();
|
||||
const planPath = getPlanFilePath();
|
||||
const planContent = getPlan()
|
||||
const planPath = getPlanFilePath()
|
||||
|
||||
if (!planContent) {
|
||||
onDone('Already in plan mode. No plan written yet.');
|
||||
return null;
|
||||
onDone('Already in plan mode. No plan written yet.')
|
||||
return null
|
||||
}
|
||||
|
||||
// If user typed "/plan open", open in editor
|
||||
const argList = args.trim().split(/\s+/);
|
||||
const argList = args.trim().split(/\s+/)
|
||||
if (argList[0] === 'open') {
|
||||
const result = await editFileInEditor(planPath);
|
||||
const result = await editFileInEditor(planPath)
|
||||
if (result.error) {
|
||||
onDone(`Failed to open plan in editor: ${result.error}`);
|
||||
onDone(`Failed to open plan in editor: ${result.error}`)
|
||||
} else {
|
||||
onDone(`Opened plan in editor: ${planPath}`);
|
||||
onDone(`Opened plan in editor: ${planPath}`)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const editor = getExternalEditor();
|
||||
const editorName = editor ? toIDEDisplayName(editor) : undefined;
|
||||
const display = <PlanDisplay planContent={planContent} planPath={planPath} editorName={editorName} />;
|
||||
|
||||
const editor = getExternalEditor()
|
||||
const editorName = editor ? toIDEDisplayName(editor) : undefined
|
||||
|
||||
const display = (
|
||||
<PlanDisplay
|
||||
planContent={planContent}
|
||||
planPath={planPath}
|
||||
editorName={editorName}
|
||||
/>
|
||||
)
|
||||
|
||||
// Render to string and pass to onDone like local commands do
|
||||
const output = await renderToString(display);
|
||||
onDone(output);
|
||||
return null;
|
||||
const output = await renderToString(display)
|
||||
onDone(output)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,31 +1,39 @@
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from 'src/services/analytics/index.js';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import TextInput from '../../components/TextInput.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { toError } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { addMarketplaceSource, saveMarketplaceToSettings } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { parseMarketplaceInput } from '../../utils/plugins/parseMarketplaceInput.js';
|
||||
import type { ViewState } from './types.js';
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from 'src/services/analytics/index.js'
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../components/design-system/Byline.js'
|
||||
import { KeyboardShortcutHint } from '../../components/design-system/KeyboardShortcutHint.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import TextInput from '../../components/TextInput.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { toError } from '../../utils/errors.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'
|
||||
import {
|
||||
addMarketplaceSource,
|
||||
saveMarketplaceToSettings,
|
||||
} from '../../utils/plugins/marketplaceManager.js'
|
||||
import { parseMarketplaceInput } from '../../utils/plugins/parseMarketplaceInput.js'
|
||||
import type { ViewState } from './types.js'
|
||||
|
||||
type Props = {
|
||||
inputValue: string;
|
||||
setInputValue: (value: string) => void;
|
||||
cursorOffset: number;
|
||||
setCursorOffset: (offset: number) => void;
|
||||
error: string | null;
|
||||
setError: (error: string | null) => void;
|
||||
result: string | null;
|
||||
setResult: (result: string | null) => void;
|
||||
setViewState: (state: ViewState) => void;
|
||||
onAddComplete?: () => void | Promise<void>;
|
||||
cliMode?: boolean;
|
||||
};
|
||||
inputValue: string
|
||||
setInputValue: (value: string) => void
|
||||
cursorOffset: number
|
||||
setCursorOffset: (offset: number) => void
|
||||
error: string | null
|
||||
setError: (error: string | null) => void
|
||||
result: string | null
|
||||
setResult: (result: string | null) => void
|
||||
setViewState: (state: ViewState) => void
|
||||
onAddComplete?: () => void | Promise<void>
|
||||
cliMode?: boolean
|
||||
}
|
||||
|
||||
export function AddMarketplace({
|
||||
inputValue,
|
||||
setInputValue,
|
||||
@@ -37,90 +45,100 @@ export function AddMarketplace({
|
||||
setResult,
|
||||
setViewState,
|
||||
onAddComplete,
|
||||
cliMode = false
|
||||
cliMode = false,
|
||||
}: Props): React.ReactNode {
|
||||
const hasAttemptedAutoAdd = useRef(false);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
const [progressMessage, setProgressMessage] = useState<string>('');
|
||||
const hasAttemptedAutoAdd = useRef(false)
|
||||
const [isLoading, setLoading] = useState(false)
|
||||
const [progressMessage, setProgressMessage] = useState<string>('')
|
||||
|
||||
const handleAdd = async () => {
|
||||
const input = inputValue.trim();
|
||||
const input = inputValue.trim()
|
||||
if (!input) {
|
||||
setError('Please enter a marketplace source');
|
||||
return;
|
||||
setError('Please enter a marketplace source')
|
||||
return
|
||||
}
|
||||
const parsed = await parseMarketplaceInput(input);
|
||||
|
||||
const parsed = await parseMarketplaceInput(input)
|
||||
if (!parsed) {
|
||||
setError('Invalid marketplace source format. Try: owner/repo, https://..., or ./path');
|
||||
return;
|
||||
setError(
|
||||
'Invalid marketplace source format. Try: owner/repo, https://..., or ./path',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Check if parseMarketplaceInput returned an error
|
||||
if ('error' in parsed) {
|
||||
setError(parsed.error);
|
||||
return;
|
||||
setError(parsed.error)
|
||||
return
|
||||
}
|
||||
setError(null);
|
||||
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
setProgressMessage('');
|
||||
const {
|
||||
name,
|
||||
resolvedSource
|
||||
} = await addMarketplaceSource(parsed, message => {
|
||||
setProgressMessage(message);
|
||||
});
|
||||
saveMarketplaceToSettings(name, {
|
||||
source: resolvedSource
|
||||
});
|
||||
clearAllCaches();
|
||||
let sourceType = parsed.source;
|
||||
setLoading(true)
|
||||
setProgressMessage('')
|
||||
const { name, resolvedSource } = await addMarketplaceSource(
|
||||
parsed,
|
||||
message => {
|
||||
setProgressMessage(message)
|
||||
},
|
||||
)
|
||||
saveMarketplaceToSettings(name, { source: resolvedSource })
|
||||
clearAllCaches()
|
||||
|
||||
let sourceType = parsed.source
|
||||
if (parsed.source === 'github') {
|
||||
sourceType = parsed.repo as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS;
|
||||
sourceType =
|
||||
parsed.repo as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
}
|
||||
|
||||
logEvent('tengu_marketplace_added', {
|
||||
source_type: sourceType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
source_type:
|
||||
sourceType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
|
||||
if (onAddComplete) {
|
||||
await onAddComplete();
|
||||
await onAddComplete()
|
||||
}
|
||||
setProgressMessage('');
|
||||
setLoading(false);
|
||||
|
||||
setProgressMessage('')
|
||||
setLoading(false)
|
||||
|
||||
if (cliMode) {
|
||||
// In CLI mode, set result to trigger completion
|
||||
setResult(`Successfully added marketplace: ${name}`);
|
||||
setResult(`Successfully added marketplace: ${name}`)
|
||||
} else {
|
||||
// In interactive mode, switch to browse view
|
||||
setViewState({
|
||||
type: 'browse-marketplace',
|
||||
targetMarketplace: name
|
||||
});
|
||||
setViewState({ type: 'browse-marketplace', targetMarketplace: name })
|
||||
}
|
||||
} catch (err) {
|
||||
const error = toError(err);
|
||||
logError(error);
|
||||
setError(error.message);
|
||||
setProgressMessage('');
|
||||
setLoading(false);
|
||||
const error = toError(err)
|
||||
logError(error)
|
||||
setError(error.message)
|
||||
setProgressMessage('')
|
||||
setLoading(false)
|
||||
|
||||
if (cliMode) {
|
||||
// In CLI mode, set result with error to trigger completion
|
||||
setResult(`Error: ${error.message}`);
|
||||
setResult(`Error: ${error.message}`)
|
||||
} else {
|
||||
setResult(null);
|
||||
setResult(null)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Auto-add if inputValue is provided
|
||||
useEffect(() => {
|
||||
if (inputValue && !hasAttemptedAutoAdd.current && !error && !result) {
|
||||
hasAttemptedAutoAdd.current = true;
|
||||
void handleAdd();
|
||||
hasAttemptedAutoAdd.current = true
|
||||
void handleAdd()
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: intentional
|
||||
}, []); // Only run once on mount
|
||||
}, []) // Only run once on mount
|
||||
|
||||
return <Box flexDirection="column">
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box flexDirection="column" paddingX={1} borderStyle="round">
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Add Marketplace</Text>
|
||||
@@ -133,29 +151,50 @@ export function AddMarketplace({
|
||||
<Text dimColor> · https://example.com/marketplace.json</Text>
|
||||
<Text dimColor> · ./path/to/marketplace</Text>
|
||||
<Box marginTop={1}>
|
||||
<TextInput value={inputValue} onChange={setInputValue} onSubmit={handleAdd} columns={80} cursorOffset={cursorOffset} onChangeCursorOffset={setCursorOffset} focus showCursor />
|
||||
<TextInput
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
onSubmit={handleAdd}
|
||||
columns={80}
|
||||
cursorOffset={cursorOffset}
|
||||
onChangeCursorOffset={setCursorOffset}
|
||||
focus
|
||||
showCursor
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{isLoading && <Box marginTop={1}>
|
||||
{isLoading && (
|
||||
<Box marginTop={1}>
|
||||
<Spinner />
|
||||
<Text>
|
||||
{progressMessage || 'Adding marketplace to configuration…'}
|
||||
</Text>
|
||||
</Box>}
|
||||
{error && <Box marginTop={1}>
|
||||
</Box>
|
||||
)}
|
||||
{error && (
|
||||
<Box marginTop={1}>
|
||||
<Text color="error">{error}</Text>
|
||||
</Box>}
|
||||
{result && <Box marginTop={1}>
|
||||
</Box>
|
||||
)}
|
||||
{result && (
|
||||
<Box marginTop={1}>
|
||||
<Text>{result}</Text>
|
||||
</Box>}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box marginLeft={3}>
|
||||
<Text dimColor italic>
|
||||
<Byline>
|
||||
<KeyboardShortcutHint shortcut="Enter" action="add" />
|
||||
<ConfigurableShortcutHint action="confirm:no" context="Settings" fallback="Esc" description="cancel" />
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Settings"
|
||||
fallback="Esc"
|
||||
description="cancel"
|
||||
/>
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,123 +1,140 @@
|
||||
import { getPluginErrorMessage, type PluginError } from '../../types/plugin.js';
|
||||
import { getPluginErrorMessage, type PluginError } from '../../types/plugin.js'
|
||||
|
||||
export function formatErrorMessage(error: PluginError): string {
|
||||
switch (error.type) {
|
||||
case 'path-not-found':
|
||||
return `${error.component} path not found: ${error.path}`;
|
||||
return `${error.component} path not found: ${error.path}`
|
||||
case 'git-auth-failed':
|
||||
return `Git ${error.authType.toUpperCase()} authentication failed for ${error.gitUrl}`;
|
||||
return `Git ${error.authType.toUpperCase()} authentication failed for ${error.gitUrl}`
|
||||
case 'git-timeout':
|
||||
return `Git ${error.operation} timed out for ${error.gitUrl}`;
|
||||
return `Git ${error.operation} timed out for ${error.gitUrl}`
|
||||
case 'network-error':
|
||||
return `Network error accessing ${error.url}${error.details ? `: ${error.details}` : ''}`;
|
||||
return `Network error accessing ${error.url}${error.details ? `: ${error.details}` : ''}`
|
||||
case 'manifest-parse-error':
|
||||
return `Failed to parse manifest at ${error.manifestPath}: ${error.parseError}`;
|
||||
return `Failed to parse manifest at ${error.manifestPath}: ${error.parseError}`
|
||||
case 'manifest-validation-error':
|
||||
return `Invalid manifest at ${error.manifestPath}: ${error.validationErrors.join(', ')}`;
|
||||
return `Invalid manifest at ${error.manifestPath}: ${error.validationErrors.join(', ')}`
|
||||
case 'plugin-not-found':
|
||||
return `Plugin "${error.pluginId}" not found in marketplace "${error.marketplace}"`;
|
||||
return `Plugin "${error.pluginId}" not found in marketplace "${error.marketplace}"`
|
||||
case 'marketplace-not-found':
|
||||
return `Marketplace "${error.marketplace}" not found`;
|
||||
return `Marketplace "${error.marketplace}" not found`
|
||||
case 'marketplace-load-failed':
|
||||
return `Failed to load marketplace "${error.marketplace}": ${error.reason}`;
|
||||
return `Failed to load marketplace "${error.marketplace}": ${error.reason}`
|
||||
case 'mcp-config-invalid':
|
||||
return `Invalid MCP server config for "${error.serverName}": ${error.validationError}`;
|
||||
case 'mcp-server-suppressed-duplicate':
|
||||
{
|
||||
const dup = error.duplicateOf.startsWith('plugin:') ? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"` : `already-configured "${error.duplicateOf}"`;
|
||||
return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`;
|
||||
}
|
||||
return `Invalid MCP server config for "${error.serverName}": ${error.validationError}`
|
||||
case 'mcp-server-suppressed-duplicate': {
|
||||
const dup = error.duplicateOf.startsWith('plugin:')
|
||||
? `server provided by plugin "${error.duplicateOf.split(':')[1] ?? '?'}"`
|
||||
: `already-configured "${error.duplicateOf}"`
|
||||
return `MCP server "${error.serverName}" skipped — same command/URL as ${dup}`
|
||||
}
|
||||
case 'hook-load-failed':
|
||||
return `Failed to load hooks from ${error.hookPath}: ${error.reason}`;
|
||||
return `Failed to load hooks from ${error.hookPath}: ${error.reason}`
|
||||
case 'component-load-failed':
|
||||
return `Failed to load ${error.component} from ${error.path}: ${error.reason}`;
|
||||
return `Failed to load ${error.component} from ${error.path}: ${error.reason}`
|
||||
case 'mcpb-download-failed':
|
||||
return `Failed to download MCPB from ${error.url}: ${error.reason}`;
|
||||
return `Failed to download MCPB from ${error.url}: ${error.reason}`
|
||||
case 'mcpb-extract-failed':
|
||||
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`;
|
||||
return `Failed to extract MCPB ${error.mcpbPath}: ${error.reason}`
|
||||
case 'mcpb-invalid-manifest':
|
||||
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`;
|
||||
return `MCPB manifest invalid at ${error.mcpbPath}: ${error.validationError}`
|
||||
case 'marketplace-blocked-by-policy':
|
||||
return error.blockedByBlocklist ? `Marketplace "${error.marketplace}" is blocked by enterprise policy` : `Marketplace "${error.marketplace}" is not in the allowed marketplace list`;
|
||||
return error.blockedByBlocklist
|
||||
? `Marketplace "${error.marketplace}" is blocked by enterprise policy`
|
||||
: `Marketplace "${error.marketplace}" is not in the allowed marketplace list`
|
||||
case 'dependency-unsatisfied':
|
||||
return error.reason === 'not-enabled' ? `Dependency "${error.dependency}" is disabled` : `Dependency "${error.dependency}" is not installed`;
|
||||
return error.reason === 'not-enabled'
|
||||
? `Dependency "${error.dependency}" is disabled`
|
||||
: `Dependency "${error.dependency}" is not installed`
|
||||
case 'lsp-config-invalid':
|
||||
return `Invalid LSP server config for "${error.serverName}": ${error.validationError}`;
|
||||
return `Invalid LSP server config for "${error.serverName}": ${error.validationError}`
|
||||
case 'lsp-server-start-failed':
|
||||
return `LSP server "${error.serverName}" failed to start: ${error.reason}`;
|
||||
return `LSP server "${error.serverName}" failed to start: ${error.reason}`
|
||||
case 'lsp-server-crashed':
|
||||
return error.signal ? `LSP server "${error.serverName}" crashed with signal ${error.signal}` : `LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`;
|
||||
return error.signal
|
||||
? `LSP server "${error.serverName}" crashed with signal ${error.signal}`
|
||||
: `LSP server "${error.serverName}" crashed with exit code ${error.exitCode ?? 'unknown'}`
|
||||
case 'lsp-request-timeout':
|
||||
return `LSP server "${error.serverName}" timed out on ${error.method} after ${error.timeoutMs}ms`;
|
||||
return `LSP server "${error.serverName}" timed out on ${error.method} after ${error.timeoutMs}ms`
|
||||
case 'lsp-request-failed':
|
||||
return `LSP server "${error.serverName}" ${error.method} failed: ${error.error}`;
|
||||
return `LSP server "${error.serverName}" ${error.method} failed: ${error.error}`
|
||||
case 'plugin-cache-miss':
|
||||
return `Plugin "${error.plugin}" not cached at ${error.installPath}`;
|
||||
return `Plugin "${error.plugin}" not cached at ${error.installPath}`
|
||||
case 'generic-error':
|
||||
return error.error;
|
||||
return error.error
|
||||
}
|
||||
const _exhaustive: never = error;
|
||||
return getPluginErrorMessage(_exhaustive);
|
||||
const _exhaustive: never = error
|
||||
return getPluginErrorMessage(_exhaustive)
|
||||
}
|
||||
|
||||
export function getErrorGuidance(error: PluginError): string | null {
|
||||
switch (error.type) {
|
||||
case 'path-not-found':
|
||||
return 'Check that the path in your manifest or marketplace config is correct';
|
||||
return 'Check that the path in your manifest or marketplace config is correct'
|
||||
case 'git-auth-failed':
|
||||
return error.authType === 'ssh' ? 'Configure SSH keys or use HTTPS URL instead' : 'Configure credentials or use SSH URL instead';
|
||||
return error.authType === 'ssh'
|
||||
? 'Configure SSH keys or use HTTPS URL instead'
|
||||
: 'Configure credentials or use SSH URL instead'
|
||||
case 'git-timeout':
|
||||
case 'network-error':
|
||||
return 'Check your internet connection and try again';
|
||||
return 'Check your internet connection and try again'
|
||||
case 'manifest-parse-error':
|
||||
return 'Check manifest file syntax in the plugin directory';
|
||||
return 'Check manifest file syntax in the plugin directory'
|
||||
case 'manifest-validation-error':
|
||||
return 'Check manifest file follows the required schema';
|
||||
return 'Check manifest file follows the required schema'
|
||||
case 'plugin-not-found':
|
||||
return `Plugin may not exist in marketplace "${error.marketplace}"`;
|
||||
return `Plugin may not exist in marketplace "${error.marketplace}"`
|
||||
case 'marketplace-not-found':
|
||||
return error.availableMarketplaces.length > 0 ? `Available marketplaces: ${error.availableMarketplaces.join(', ')}` : 'Add the marketplace first using /plugin marketplace add';
|
||||
return error.availableMarketplaces.length > 0
|
||||
? `Available marketplaces: ${error.availableMarketplaces.join(', ')}`
|
||||
: 'Add the marketplace first using /plugin marketplace add'
|
||||
case 'mcp-config-invalid':
|
||||
return 'Check MCP server configuration in .mcp.json or manifest';
|
||||
case 'mcp-server-suppressed-duplicate':
|
||||
{
|
||||
// duplicateOf is "plugin:name:srv" when another plugin won dedup —
|
||||
// users can't remove plugin-provided servers from their MCP config,
|
||||
// so point them at the winning plugin instead.
|
||||
if (error.duplicateOf.startsWith('plugin:')) {
|
||||
const winningPlugin = error.duplicateOf.split(':')[1] ?? 'the other plugin';
|
||||
return `Disable plugin "${winningPlugin}" if you want this plugin's version instead`;
|
||||
}
|
||||
return `Remove "${error.duplicateOf}" from your MCP config if you want the plugin's version instead`;
|
||||
return 'Check MCP server configuration in .mcp.json or manifest'
|
||||
case 'mcp-server-suppressed-duplicate': {
|
||||
// duplicateOf is "plugin:name:srv" when another plugin won dedup —
|
||||
// users can't remove plugin-provided servers from their MCP config,
|
||||
// so point them at the winning plugin instead.
|
||||
if (error.duplicateOf.startsWith('plugin:')) {
|
||||
const winningPlugin =
|
||||
error.duplicateOf.split(':')[1] ?? 'the other plugin'
|
||||
return `Disable plugin "${winningPlugin}" if you want this plugin's version instead`
|
||||
}
|
||||
return `Remove "${error.duplicateOf}" from your MCP config if you want the plugin's version instead`
|
||||
}
|
||||
case 'hook-load-failed':
|
||||
return 'Check hooks.json file syntax and structure';
|
||||
return 'Check hooks.json file syntax and structure'
|
||||
case 'component-load-failed':
|
||||
return `Check ${error.component} directory structure and file permissions`;
|
||||
return `Check ${error.component} directory structure and file permissions`
|
||||
case 'mcpb-download-failed':
|
||||
return 'Check your internet connection and URL accessibility';
|
||||
return 'Check your internet connection and URL accessibility'
|
||||
case 'mcpb-extract-failed':
|
||||
return 'Verify the MCPB file is valid and not corrupted';
|
||||
return 'Verify the MCPB file is valid and not corrupted'
|
||||
case 'mcpb-invalid-manifest':
|
||||
return 'Contact the plugin author about the invalid manifest';
|
||||
return 'Contact the plugin author about the invalid manifest'
|
||||
case 'marketplace-blocked-by-policy':
|
||||
if (error.blockedByBlocklist) {
|
||||
return 'This marketplace source is explicitly blocked by your administrator';
|
||||
return 'This marketplace source is explicitly blocked by your administrator'
|
||||
}
|
||||
return error.allowedSources.length > 0 ? `Allowed sources: ${error.allowedSources.join(', ')}` : 'Contact your administrator to configure allowed marketplace sources';
|
||||
return error.allowedSources.length > 0
|
||||
? `Allowed sources: ${error.allowedSources.join(', ')}`
|
||||
: 'Contact your administrator to configure allowed marketplace sources'
|
||||
case 'dependency-unsatisfied':
|
||||
return error.reason === 'not-enabled' ? `Enable "${error.dependency}" or uninstall "${error.plugin}"` : `Install "${error.dependency}" or uninstall "${error.plugin}"`;
|
||||
return error.reason === 'not-enabled'
|
||||
? `Enable "${error.dependency}" or uninstall "${error.plugin}"`
|
||||
: `Install "${error.dependency}" or uninstall "${error.plugin}"`
|
||||
case 'lsp-config-invalid':
|
||||
return 'Check LSP server configuration in the plugin manifest';
|
||||
return 'Check LSP server configuration in the plugin manifest'
|
||||
case 'lsp-server-start-failed':
|
||||
case 'lsp-server-crashed':
|
||||
case 'lsp-request-timeout':
|
||||
case 'lsp-request-failed':
|
||||
return 'Check LSP server logs with --debug for details';
|
||||
return 'Check LSP server logs with --debug for details'
|
||||
case 'plugin-cache-miss':
|
||||
return 'Run /plugins to refresh the plugin cache';
|
||||
return 'Run /plugins to refresh the plugin cache'
|
||||
case 'marketplace-load-failed':
|
||||
case 'generic-error':
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const _exhaustive: never = error;
|
||||
return null;
|
||||
const _exhaustive: never = error
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { stringWidth } from '../../ink/stringWidth.js';
|
||||
import figures from 'figures'
|
||||
import React, { useCallback, useState } from 'react'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { stringWidth } from '../../ink/stringWidth.js'
|
||||
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw text input for config dialog
|
||||
import { Box, Text, useInput } from '../../ink.js';
|
||||
import { useKeybinding, useKeybindings } from '../../keybindings/useKeybinding.js';
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js';
|
||||
import type { PluginOptionSchema, PluginOptionValues } from '../../utils/plugins/pluginOptionsStorage.js';
|
||||
import { Box, Text, useInput } from '../../ink.js'
|
||||
import {
|
||||
useKeybinding,
|
||||
useKeybindings,
|
||||
} from '../../keybindings/useKeybinding.js'
|
||||
import { isEnvTruthy } from '../../utils/envUtils.js'
|
||||
import type {
|
||||
PluginOptionSchema,
|
||||
PluginOptionValues,
|
||||
} from '../../utils/plugins/pluginOptionsStorage.js'
|
||||
|
||||
/**
|
||||
* Build the onSave payload from collected string inputs.
|
||||
@@ -21,336 +26,189 @@ import type { PluginOptionSchema, PluginOptionValues } from '../../utils/plugins
|
||||
*
|
||||
* Exported for unit testing.
|
||||
*/
|
||||
export function buildFinalValues(fields: string[], collected: Record<string, string>, configSchema: PluginOptionSchema, initialValues: PluginOptionValues | undefined): PluginOptionValues {
|
||||
const finalValues: PluginOptionValues = {};
|
||||
export function buildFinalValues(
|
||||
fields: string[],
|
||||
collected: Record<string, string>,
|
||||
configSchema: PluginOptionSchema,
|
||||
initialValues: PluginOptionValues | undefined,
|
||||
): PluginOptionValues {
|
||||
const finalValues: PluginOptionValues = {}
|
||||
for (const fieldKey of fields) {
|
||||
const schema = configSchema[fieldKey];
|
||||
const value = collected[fieldKey] ?? '';
|
||||
if (schema?.sensitive === true && value === '' && initialValues?.[fieldKey] !== undefined) {
|
||||
continue;
|
||||
const schema = configSchema[fieldKey]
|
||||
const value = collected[fieldKey] ?? ''
|
||||
|
||||
if (
|
||||
schema?.sensitive === true &&
|
||||
value === '' &&
|
||||
initialValues?.[fieldKey] !== undefined
|
||||
) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (schema?.type === 'number') {
|
||||
// Number('') returns 0, not NaN — omit blank number inputs so
|
||||
// validateUserConfig's required check actually catches them.
|
||||
if (value.trim() === '') continue;
|
||||
const num = Number(value);
|
||||
finalValues[fieldKey] = Number.isNaN(num) ? value : num;
|
||||
if (value.trim() === '') continue
|
||||
const num = Number(value)
|
||||
finalValues[fieldKey] = Number.isNaN(num) ? value : num
|
||||
} else if (schema?.type === 'boolean') {
|
||||
finalValues[fieldKey] = isEnvTruthy(value);
|
||||
finalValues[fieldKey] = isEnvTruthy(value)
|
||||
} else {
|
||||
finalValues[fieldKey] = value;
|
||||
finalValues[fieldKey] = value
|
||||
}
|
||||
}
|
||||
return finalValues;
|
||||
return finalValues
|
||||
}
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
configSchema: PluginOptionSchema;
|
||||
title: string
|
||||
subtitle: string
|
||||
configSchema: PluginOptionSchema
|
||||
/** Pre-fill fields when reconfiguring. Sensitive fields are not prepopulated. */
|
||||
initialValues?: PluginOptionValues;
|
||||
onSave: (config: PluginOptionValues) => void;
|
||||
onCancel: () => void;
|
||||
};
|
||||
export function PluginOptionsDialog(t0) {
|
||||
const $ = _c(70);
|
||||
const {
|
||||
title,
|
||||
subtitle,
|
||||
initialValues?: PluginOptionValues
|
||||
onSave: (config: PluginOptionValues) => void
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function PluginOptionsDialog({
|
||||
title,
|
||||
subtitle,
|
||||
configSchema,
|
||||
initialValues,
|
||||
onSave,
|
||||
onCancel,
|
||||
}: Props): React.ReactNode {
|
||||
const fields = Object.keys(configSchema)
|
||||
|
||||
// Prepopulate from initialValues but skip sensitive fields — we don't
|
||||
// want to echo secrets back into the text buffer.
|
||||
const initialFor = useCallback(
|
||||
(key: string): string => {
|
||||
if (configSchema[key]?.sensitive === true) return ''
|
||||
const v = initialValues?.[key]
|
||||
return v === undefined ? '' : String(v)
|
||||
},
|
||||
[configSchema, initialValues],
|
||||
)
|
||||
|
||||
const [currentFieldIndex, setCurrentFieldIndex] = useState(0)
|
||||
const [values, setValues] = useState<Record<string, string>>({})
|
||||
const [currentInput, setCurrentInput] = useState(() =>
|
||||
fields[0] ? initialFor(fields[0]) : '',
|
||||
)
|
||||
|
||||
const currentField = fields[currentFieldIndex]
|
||||
const fieldSchema = currentField ? configSchema[currentField] : null
|
||||
|
||||
// Use Settings context so 'n' key doesn't cancel (allows typing 'n' in input).
|
||||
// isCancelActive={false} on Dialog keeps its own confirm:no out of the way.
|
||||
useKeybinding('confirm:no', onCancel, { context: 'Settings' })
|
||||
|
||||
// Tab to next field
|
||||
const handleNextField = useCallback(() => {
|
||||
if (currentFieldIndex < fields.length - 1 && currentField) {
|
||||
setValues(prev => ({ ...prev, [currentField]: currentInput }))
|
||||
setCurrentFieldIndex(prev => prev + 1)
|
||||
const nextKey = fields[currentFieldIndex + 1]
|
||||
setCurrentInput(nextKey ? initialFor(nextKey) : '')
|
||||
}
|
||||
}, [currentFieldIndex, fields, currentField, currentInput, initialFor])
|
||||
|
||||
// Enter to save current field and move to next, or save all if last
|
||||
const handleConfirm = useCallback(() => {
|
||||
if (!currentField) return
|
||||
|
||||
const newValues = { ...values, [currentField]: currentInput }
|
||||
|
||||
if (currentFieldIndex === fields.length - 1) {
|
||||
onSave(buildFinalValues(fields, newValues, configSchema, initialValues))
|
||||
} else {
|
||||
// Move to next field
|
||||
setValues(newValues)
|
||||
setCurrentFieldIndex(prev => prev + 1)
|
||||
const nextKey = fields[currentFieldIndex + 1]
|
||||
setCurrentInput(nextKey ? initialFor(nextKey) : '')
|
||||
}
|
||||
}, [
|
||||
currentField,
|
||||
values,
|
||||
currentInput,
|
||||
currentFieldIndex,
|
||||
fields,
|
||||
configSchema,
|
||||
initialValues,
|
||||
onSave,
|
||||
onCancel
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== configSchema) {
|
||||
t1 = Object.keys(configSchema);
|
||||
$[0] = configSchema;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const fields = t1;
|
||||
let t2;
|
||||
if ($[2] !== configSchema || $[3] !== initialValues) {
|
||||
t2 = key => {
|
||||
if (configSchema[key]?.sensitive === true) {
|
||||
return "";
|
||||
}
|
||||
const v = initialValues?.[key];
|
||||
return v === undefined ? "" : String(v);
|
||||
};
|
||||
$[2] = configSchema;
|
||||
$[3] = initialValues;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
const initialFor = t2;
|
||||
const [currentFieldIndex, setCurrentFieldIndex] = useState(0);
|
||||
let t3;
|
||||
if ($[5] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {};
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const [values, setValues] = useState(t3);
|
||||
let t4;
|
||||
if ($[6] !== fields[0] || $[7] !== initialFor) {
|
||||
t4 = () => fields[0] ? initialFor(fields[0]) : "";
|
||||
$[6] = fields[0];
|
||||
$[7] = initialFor;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
}
|
||||
const [currentInput, setCurrentInput] = useState(t4);
|
||||
const currentField = fields[currentFieldIndex];
|
||||
const fieldSchema = currentField ? configSchema[currentField] : null;
|
||||
let t5;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = {
|
||||
context: "Settings"
|
||||
};
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
useKeybinding("confirm:no", onCancel, t5);
|
||||
let t6;
|
||||
if ($[10] !== currentField || $[11] !== currentFieldIndex || $[12] !== currentInput || $[13] !== fields || $[14] !== initialFor) {
|
||||
t6 = () => {
|
||||
if (currentFieldIndex < fields.length - 1 && currentField) {
|
||||
setValues(prev => ({
|
||||
...prev,
|
||||
[currentField]: currentInput
|
||||
}));
|
||||
setCurrentFieldIndex(_temp);
|
||||
const nextKey = fields[currentFieldIndex + 1];
|
||||
setCurrentInput(nextKey ? initialFor(nextKey) : "");
|
||||
}
|
||||
};
|
||||
$[10] = currentField;
|
||||
$[11] = currentFieldIndex;
|
||||
$[12] = currentInput;
|
||||
$[13] = fields;
|
||||
$[14] = initialFor;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
const handleNextField = t6;
|
||||
let t7;
|
||||
if ($[16] !== configSchema || $[17] !== currentField || $[18] !== currentFieldIndex || $[19] !== currentInput || $[20] !== fields || $[21] !== initialFor || $[22] !== initialValues || $[23] !== onSave || $[24] !== values) {
|
||||
t7 = () => {
|
||||
if (!currentField) {
|
||||
return;
|
||||
}
|
||||
const newValues = {
|
||||
...values,
|
||||
[currentField]: currentInput
|
||||
};
|
||||
if (currentFieldIndex === fields.length - 1) {
|
||||
onSave(buildFinalValues(fields, newValues, configSchema, initialValues));
|
||||
} else {
|
||||
setValues(newValues);
|
||||
setCurrentFieldIndex(_temp2);
|
||||
const nextKey_0 = fields[currentFieldIndex + 1];
|
||||
setCurrentInput(nextKey_0 ? initialFor(nextKey_0) : "");
|
||||
}
|
||||
};
|
||||
$[16] = configSchema;
|
||||
$[17] = currentField;
|
||||
$[18] = currentFieldIndex;
|
||||
$[19] = currentInput;
|
||||
$[20] = fields;
|
||||
$[21] = initialFor;
|
||||
$[22] = initialValues;
|
||||
$[23] = onSave;
|
||||
$[24] = values;
|
||||
$[25] = t7;
|
||||
} else {
|
||||
t7 = $[25];
|
||||
}
|
||||
const handleConfirm = t7;
|
||||
let t8;
|
||||
if ($[26] !== handleConfirm || $[27] !== handleNextField) {
|
||||
t8 = {
|
||||
"confirm:nextField": handleNextField,
|
||||
"confirm:yes": handleConfirm
|
||||
};
|
||||
$[26] = handleConfirm;
|
||||
$[27] = handleNextField;
|
||||
$[28] = t8;
|
||||
} else {
|
||||
t8 = $[28];
|
||||
}
|
||||
let t9;
|
||||
if ($[29] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[29] = t9;
|
||||
} else {
|
||||
t9 = $[29];
|
||||
}
|
||||
useKeybindings(t8, t9);
|
||||
let t10;
|
||||
if ($[30] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = (char, key_0) => {
|
||||
if (key_0.backspace || key_0.delete) {
|
||||
setCurrentInput(_temp3);
|
||||
return;
|
||||
}
|
||||
if (char && !key_0.ctrl && !key_0.meta && !key_0.tab && !key_0.return) {
|
||||
setCurrentInput(prev_3 => prev_3 + char);
|
||||
}
|
||||
};
|
||||
$[30] = t10;
|
||||
} else {
|
||||
t10 = $[30];
|
||||
}
|
||||
useInput(t10);
|
||||
initialFor,
|
||||
initialValues,
|
||||
])
|
||||
|
||||
useKeybindings(
|
||||
{
|
||||
'confirm:nextField': handleNextField,
|
||||
'confirm:yes': handleConfirm,
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
// Character input handling (backspace, typing)
|
||||
useInput((char, key) => {
|
||||
// Backspace
|
||||
if (key.backspace || key.delete) {
|
||||
setCurrentInput(prev => prev.slice(0, -1))
|
||||
return
|
||||
}
|
||||
|
||||
// Regular character input
|
||||
if (char && !key.ctrl && !key.meta && !key.tab && !key.return) {
|
||||
setCurrentInput(prev => prev + char)
|
||||
}
|
||||
})
|
||||
|
||||
if (!fieldSchema || !currentField) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const isSensitive = fieldSchema.sensitive === true;
|
||||
const isRequired = fieldSchema.required === true;
|
||||
let t11;
|
||||
if ($[31] !== currentInput || $[32] !== isSensitive) {
|
||||
t11 = isSensitive ? "*".repeat(stringWidth(currentInput)) : currentInput;
|
||||
$[31] = currentInput;
|
||||
$[32] = isSensitive;
|
||||
$[33] = t11;
|
||||
} else {
|
||||
t11 = $[33];
|
||||
}
|
||||
const displayValue = t11;
|
||||
const t12 = fieldSchema.title || currentField;
|
||||
let t13;
|
||||
if ($[34] !== isRequired) {
|
||||
t13 = isRequired && <Text color="error"> *</Text>;
|
||||
$[34] = isRequired;
|
||||
$[35] = t13;
|
||||
} else {
|
||||
t13 = $[35];
|
||||
}
|
||||
let t14;
|
||||
if ($[36] !== t12 || $[37] !== t13) {
|
||||
t14 = <Text bold={true}>{t12}{t13}</Text>;
|
||||
$[36] = t12;
|
||||
$[37] = t13;
|
||||
$[38] = t14;
|
||||
} else {
|
||||
t14 = $[38];
|
||||
}
|
||||
let t15;
|
||||
if ($[39] !== fieldSchema.description) {
|
||||
t15 = fieldSchema.description && <Text dimColor={true}>{fieldSchema.description}</Text>;
|
||||
$[39] = fieldSchema.description;
|
||||
$[40] = t15;
|
||||
} else {
|
||||
t15 = $[40];
|
||||
}
|
||||
let t16;
|
||||
if ($[41] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t16 = <Text>{figures.pointerSmall} </Text>;
|
||||
$[41] = t16;
|
||||
} else {
|
||||
t16 = $[41];
|
||||
}
|
||||
let t17;
|
||||
if ($[42] !== displayValue) {
|
||||
t17 = <Text>{displayValue}</Text>;
|
||||
$[42] = displayValue;
|
||||
$[43] = t17;
|
||||
} else {
|
||||
t17 = $[43];
|
||||
}
|
||||
let t18;
|
||||
if ($[44] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t18 = <Text>█</Text>;
|
||||
$[44] = t18;
|
||||
} else {
|
||||
t18 = $[44];
|
||||
}
|
||||
let t19;
|
||||
if ($[45] !== t17) {
|
||||
t19 = <Box marginTop={1}>{t16}{t17}{t18}</Box>;
|
||||
$[45] = t17;
|
||||
$[46] = t19;
|
||||
} else {
|
||||
t19 = $[46];
|
||||
}
|
||||
let t20;
|
||||
if ($[47] !== t14 || $[48] !== t15 || $[49] !== t19) {
|
||||
t20 = <Box flexDirection="column">{t14}{t15}{t19}</Box>;
|
||||
$[47] = t14;
|
||||
$[48] = t15;
|
||||
$[49] = t19;
|
||||
$[50] = t20;
|
||||
} else {
|
||||
t20 = $[50];
|
||||
}
|
||||
const t21 = currentFieldIndex + 1;
|
||||
let t22;
|
||||
if ($[51] !== fields.length || $[52] !== t21) {
|
||||
t22 = <Text dimColor={true}>Field {t21} of {fields.length}</Text>;
|
||||
$[51] = fields.length;
|
||||
$[52] = t21;
|
||||
$[53] = t22;
|
||||
} else {
|
||||
t22 = $[53];
|
||||
}
|
||||
let t23;
|
||||
if ($[54] !== currentFieldIndex || $[55] !== fields.length) {
|
||||
t23 = currentFieldIndex < fields.length - 1 && <Text dimColor={true}>Tab: Next field · Enter: Save and continue</Text>;
|
||||
$[54] = currentFieldIndex;
|
||||
$[55] = fields.length;
|
||||
$[56] = t23;
|
||||
} else {
|
||||
t23 = $[56];
|
||||
}
|
||||
let t24;
|
||||
if ($[57] !== currentFieldIndex || $[58] !== fields.length) {
|
||||
t24 = currentFieldIndex === fields.length - 1 && <Text dimColor={true}>Enter: Save configuration</Text>;
|
||||
$[57] = currentFieldIndex;
|
||||
$[58] = fields.length;
|
||||
$[59] = t24;
|
||||
} else {
|
||||
t24 = $[59];
|
||||
}
|
||||
let t25;
|
||||
if ($[60] !== t22 || $[61] !== t23 || $[62] !== t24) {
|
||||
t25 = <Box flexDirection="column">{t22}{t23}{t24}</Box>;
|
||||
$[60] = t22;
|
||||
$[61] = t23;
|
||||
$[62] = t24;
|
||||
$[63] = t25;
|
||||
} else {
|
||||
t25 = $[63];
|
||||
}
|
||||
let t26;
|
||||
if ($[64] !== onCancel || $[65] !== subtitle || $[66] !== t20 || $[67] !== t25 || $[68] !== title) {
|
||||
t26 = <Dialog title={title} subtitle={subtitle} onCancel={onCancel} isCancelActive={false}>{t20}{t25}</Dialog>;
|
||||
$[64] = onCancel;
|
||||
$[65] = subtitle;
|
||||
$[66] = t20;
|
||||
$[67] = t25;
|
||||
$[68] = title;
|
||||
$[69] = t26;
|
||||
} else {
|
||||
t26 = $[69];
|
||||
}
|
||||
return t26;
|
||||
}
|
||||
function _temp3(prev_2) {
|
||||
return prev_2.slice(0, -1);
|
||||
}
|
||||
function _temp2(prev_1) {
|
||||
return prev_1 + 1;
|
||||
}
|
||||
function _temp(prev_0) {
|
||||
return prev_0 + 1;
|
||||
|
||||
const isSensitive = fieldSchema.sensitive === true
|
||||
const isRequired = fieldSchema.required === true
|
||||
const displayValue = isSensitive
|
||||
? '*'.repeat(stringWidth(currentInput))
|
||||
: currentInput
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title={title}
|
||||
subtitle={subtitle}
|
||||
onCancel={onCancel}
|
||||
isCancelActive={false}
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text bold={true}>
|
||||
{fieldSchema.title || currentField}
|
||||
{isRequired && <Text color="error"> *</Text>}
|
||||
</Text>
|
||||
{fieldSchema.description && (
|
||||
<Text dimColor={true}>{fieldSchema.description}</Text>
|
||||
)}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text>{figures.pointerSmall} </Text>
|
||||
<Text>{displayValue}</Text>
|
||||
<Text>█</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor={true}>
|
||||
Field {currentFieldIndex + 1} of {fields.length}
|
||||
</Text>
|
||||
{currentFieldIndex < fields.length - 1 && (
|
||||
<Text dimColor={true}>
|
||||
Tab: Next field · Enter: Save and continue
|
||||
</Text>
|
||||
)}
|
||||
{currentFieldIndex === fields.length - 1 && (
|
||||
<Text dimColor={true}>Enter: Save configuration</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,14 +7,26 @@
|
||||
* onDone('skipped') immediately if nothing needs filling.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import type { LoadedPlugin } from '../../types/plugin.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { loadMcpServerUserConfig, saveMcpServerUserConfig } from '../../utils/plugins/mcpbHandler.js';
|
||||
import { getUnconfiguredChannels, type UnconfiguredChannel } from '../../utils/plugins/mcpPluginIntegration.js';
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js';
|
||||
import { getUnconfiguredOptions, loadPluginOptions, type PluginOptionSchema, type PluginOptionValues, savePluginOptions } from '../../utils/plugins/pluginOptionsStorage.js';
|
||||
import { PluginOptionsDialog } from './PluginOptionsDialog.js';
|
||||
import * as React from 'react'
|
||||
import type { LoadedPlugin } from '../../types/plugin.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import {
|
||||
loadMcpServerUserConfig,
|
||||
saveMcpServerUserConfig,
|
||||
} from '../../utils/plugins/mcpbHandler.js'
|
||||
import {
|
||||
getUnconfiguredChannels,
|
||||
type UnconfiguredChannel,
|
||||
} from '../../utils/plugins/mcpPluginIntegration.js'
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js'
|
||||
import {
|
||||
getUnconfiguredOptions,
|
||||
loadPluginOptions,
|
||||
type PluginOptionSchema,
|
||||
type PluginOptionValues,
|
||||
savePluginOptions,
|
||||
} from '../../utils/plugins/pluginOptionsStorage.js'
|
||||
import { PluginOptionsDialog } from './PluginOptionsDialog.js'
|
||||
|
||||
/**
|
||||
* Post-install lookup: return the LoadedPlugin for the just-installed
|
||||
@@ -24,12 +36,13 @@ import { PluginOptionsDialog } from './PluginOptionsDialog.js';
|
||||
*
|
||||
* Install should have cleared caches already; loadAllPlugins reads fresh.
|
||||
*/
|
||||
export async function findPluginOptionsTarget(pluginId: string): Promise<LoadedPlugin | undefined> {
|
||||
const {
|
||||
enabled,
|
||||
disabled
|
||||
} = await loadAllPlugins();
|
||||
return [...enabled, ...disabled].find(p => p.repository === pluginId || p.source === pluginId);
|
||||
export async function findPluginOptionsTarget(
|
||||
pluginId: string,
|
||||
): Promise<LoadedPlugin | undefined> {
|
||||
const { enabled, disabled } = await loadAllPlugins()
|
||||
return [...enabled, ...disabled].find(
|
||||
p => p.repository === pluginId || p.source === pluginId,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,37 +50,39 @@ export async function findPluginOptionsTarget(pluginId: string): Promise<LoadedP
|
||||
* collapse to this shape — the only difference is which save function runs.
|
||||
*/
|
||||
type ConfigStep = {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
schema: PluginOptionSchema;
|
||||
key: string
|
||||
title: string
|
||||
subtitle: string
|
||||
schema: PluginOptionSchema
|
||||
/** Returns any already-saved values so PluginOptionsDialog can pre-fill and
|
||||
* skip unchanged sensitive fields on reconfigure. */
|
||||
load: () => PluginOptionValues | undefined;
|
||||
save: (values: PluginOptionValues) => void;
|
||||
};
|
||||
load: () => PluginOptionValues | undefined
|
||||
save: (values: PluginOptionValues) => void
|
||||
}
|
||||
|
||||
type Props = {
|
||||
plugin: LoadedPlugin;
|
||||
plugin: LoadedPlugin
|
||||
/** `name@marketplace` — the savePluginOptions / saveMcpServerUserConfig key. */
|
||||
pluginId: string;
|
||||
pluginId: string
|
||||
/**
|
||||
* `configured` = user filled all fields. `skipped` = nothing needed
|
||||
* configuring, or user hit cancel. `error` = save threw.
|
||||
*/
|
||||
onDone: (outcome: 'configured' | 'skipped' | 'error', detail?: string) => void;
|
||||
};
|
||||
onDone: (outcome: 'configured' | 'skipped' | 'error', detail?: string) => void
|
||||
}
|
||||
|
||||
export function PluginOptionsFlow({
|
||||
plugin,
|
||||
pluginId,
|
||||
onDone
|
||||
onDone,
|
||||
}: Props): React.ReactNode {
|
||||
// Build the step list once at mount. Re-calling after a save would drop the
|
||||
// item we just configured.
|
||||
const [steps] = React.useState<ConfigStep[]>(() => {
|
||||
const result: ConfigStep[] = [];
|
||||
const result: ConfigStep[] = []
|
||||
|
||||
// Top-level manifest.userConfig
|
||||
const unconfigured = getUnconfiguredOptions(plugin);
|
||||
const unconfigured = getUnconfiguredOptions(plugin)
|
||||
if (Object.keys(unconfigured).length > 0) {
|
||||
result.push({
|
||||
key: 'top-level',
|
||||
@@ -75,60 +90,83 @@ export function PluginOptionsFlow({
|
||||
subtitle: 'Plugin options',
|
||||
schema: unconfigured,
|
||||
load: () => loadPluginOptions(pluginId),
|
||||
save: values => savePluginOptions(pluginId, values, plugin.manifest.userConfig!)
|
||||
});
|
||||
save: values =>
|
||||
savePluginOptions(pluginId, values, plugin.manifest.userConfig!),
|
||||
})
|
||||
}
|
||||
|
||||
// Per-channel userConfig (assistant-mode channels)
|
||||
const channels: UnconfiguredChannel[] = getUnconfiguredChannels(plugin);
|
||||
const channels: UnconfiguredChannel[] = getUnconfiguredChannels(plugin)
|
||||
for (const channel of channels) {
|
||||
result.push({
|
||||
key: `channel:${channel.server}`,
|
||||
title: `Configure ${channel.displayName}`,
|
||||
subtitle: `Plugin: ${plugin.name}`,
|
||||
schema: channel.configSchema,
|
||||
load: () => loadMcpServerUserConfig(pluginId, channel.server) ?? undefined,
|
||||
save: values_0 => saveMcpServerUserConfig(pluginId, channel.server, values_0, channel.configSchema)
|
||||
});
|
||||
load: () =>
|
||||
loadMcpServerUserConfig(pluginId, channel.server) ?? undefined,
|
||||
save: values =>
|
||||
saveMcpServerUserConfig(
|
||||
pluginId,
|
||||
channel.server,
|
||||
values,
|
||||
channel.configSchema,
|
||||
),
|
||||
})
|
||||
}
|
||||
return result;
|
||||
});
|
||||
const [index, setIndex] = React.useState(0);
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const [index, setIndex] = React.useState(0)
|
||||
|
||||
// Latest-ref: lets the effect close over the current onDone without
|
||||
// re-running when the parent re-renders.
|
||||
const onDoneRef = React.useRef(onDone);
|
||||
onDoneRef.current = onDone;
|
||||
const onDoneRef = React.useRef(onDone)
|
||||
onDoneRef.current = onDone
|
||||
|
||||
// Nothing to configure → tell the caller and render nothing. Effect,
|
||||
// not inline call: calling setState in the parent during our render
|
||||
// is a React rules-of-hooks violation.
|
||||
React.useEffect(() => {
|
||||
if (steps.length === 0) {
|
||||
onDoneRef.current('skipped');
|
||||
onDoneRef.current('skipped')
|
||||
}
|
||||
}, [steps.length]);
|
||||
}, [steps.length])
|
||||
|
||||
if (steps.length === 0) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const current = steps[index]!;
|
||||
function handleSave(values_1: PluginOptionValues): void {
|
||||
|
||||
const current = steps[index]!
|
||||
|
||||
function handleSave(values: PluginOptionValues): void {
|
||||
try {
|
||||
current.save(values_1);
|
||||
current.save(values)
|
||||
} catch (err) {
|
||||
onDone('error', errorMessage(err));
|
||||
return;
|
||||
onDone('error', errorMessage(err))
|
||||
return
|
||||
}
|
||||
const next = index + 1;
|
||||
const next = index + 1
|
||||
if (next < steps.length) {
|
||||
setIndex(next);
|
||||
setIndex(next)
|
||||
} else {
|
||||
onDone('configured');
|
||||
onDone('configured')
|
||||
}
|
||||
}
|
||||
|
||||
// key forces a remount when advancing to the next step — React would
|
||||
// otherwise reuse the instance and carry PluginOptionsDialog's
|
||||
// internal useState (field index, typed values) over.
|
||||
return <PluginOptionsDialog key={current.key} title={current.title} subtitle={current.subtitle} configSchema={current.schema} initialValues={current.load()} onSave={handleSave} onCancel={() => onDone('skipped')} />;
|
||||
return (
|
||||
<PluginOptionsDialog
|
||||
key={current.key}
|
||||
title={current.title}
|
||||
subtitle={current.subtitle}
|
||||
configSchema={current.schema}
|
||||
initialValues={current.load()}
|
||||
onSave={handleSave}
|
||||
onCancel={() => onDone('skipped')}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,31 +1,20 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js';
|
||||
export function PluginTrustWarning() {
|
||||
const $ = _c(3);
|
||||
let t0;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t0 = getPluginTrustMessage();
|
||||
$[0] = t0;
|
||||
} else {
|
||||
t0 = $[0];
|
||||
}
|
||||
const customMessage = t0;
|
||||
let t1;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = <Text color="claude">{figures.warning} </Text>;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Box marginBottom={1}>{t1}<Text dimColor={true} italic={true}>Make sure you trust a plugin before installing, updating, or using it. Anthropic does not control what MCP servers, files, or other software are included in plugins and cannot verify that they will work as intended or that they won't change. See each plugin's homepage for more information.{customMessage ? ` ${customMessage}` : ""}</Text></Box>;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
}
|
||||
return t2;
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { getPluginTrustMessage } from '../../utils/plugins/marketplaceHelpers.js'
|
||||
|
||||
export function PluginTrustWarning(): React.ReactNode {
|
||||
const customMessage = getPluginTrustMessage()
|
||||
return (
|
||||
<Box marginBottom={1}>
|
||||
<Text color="claude">{figures.warning} </Text>
|
||||
<Text dimColor italic>
|
||||
Make sure you trust a plugin before installing, updating, or using it.
|
||||
Anthropic does not control what MCP servers, files, or other software
|
||||
are included in plugins and cannot verify that they will work as
|
||||
intended or that they won't change. See each plugin's homepage
|
||||
for more information.{customMessage ? ` ${customMessage}` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,564 +1,151 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { Box, color, Text, useTheme } from '../../ink.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import type { UnifiedInstalledItem } from './unifiedTypes.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { Box, color, Text, useTheme } from '../../ink.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
import type { UnifiedInstalledItem } from './unifiedTypes.js'
|
||||
|
||||
type Props = {
|
||||
item: UnifiedInstalledItem;
|
||||
isSelected: boolean;
|
||||
};
|
||||
export function UnifiedInstalledCell(t0) {
|
||||
const $ = _c(142);
|
||||
const {
|
||||
item,
|
||||
isSelected
|
||||
} = t0;
|
||||
const [theme] = useTheme();
|
||||
if (item.type === "plugin") {
|
||||
let statusIcon;
|
||||
let statusText;
|
||||
if (item.pendingToggle) {
|
||||
let t1;
|
||||
if ($[0] !== theme) {
|
||||
t1 = color("suggestion", theme)(figures.arrowRight);
|
||||
$[0] = theme;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = item.pendingToggle === "will-enable" ? "will enable" : "will disable";
|
||||
} else {
|
||||
if (item.errorCount > 0) {
|
||||
let t1;
|
||||
if ($[2] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[2] = theme;
|
||||
$[3] = t1;
|
||||
} else {
|
||||
t1 = $[3];
|
||||
}
|
||||
statusIcon = t1;
|
||||
const t2 = item.errorCount;
|
||||
let t3;
|
||||
if ($[4] !== item.errorCount) {
|
||||
t3 = plural(item.errorCount, "error");
|
||||
$[4] = item.errorCount;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
statusText = `${t2} ${t3}`;
|
||||
} else {
|
||||
if (!item.isEnabled) {
|
||||
let t1;
|
||||
if ($[6] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[6] = theme;
|
||||
$[7] = t1;
|
||||
} else {
|
||||
t1 = $[7];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = "disabled";
|
||||
} else {
|
||||
let t1;
|
||||
if ($[8] !== theme) {
|
||||
t1 = color("success", theme)(figures.tick);
|
||||
$[8] = theme;
|
||||
$[9] = t1;
|
||||
} else {
|
||||
t1 = $[9];
|
||||
}
|
||||
statusIcon = t1;
|
||||
statusText = "enabled";
|
||||
}
|
||||
}
|
||||
}
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[10] !== t1 || $[11] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[10] = t1;
|
||||
$[11] = t2;
|
||||
$[12] = t3;
|
||||
} else {
|
||||
t3 = $[12];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
let t5;
|
||||
if ($[13] !== item.name || $[14] !== t4) {
|
||||
t5 = <Text color={t4}>{item.name}</Text>;
|
||||
$[13] = item.name;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
} else {
|
||||
t5 = $[15];
|
||||
}
|
||||
const t6 = !isSelected;
|
||||
let t7;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[16] = t7;
|
||||
} else {
|
||||
t7 = $[16];
|
||||
}
|
||||
let t8;
|
||||
if ($[17] !== t6) {
|
||||
t8 = <Text dimColor={t6}>{" "}{t7}</Text>;
|
||||
$[17] = t6;
|
||||
$[18] = t8;
|
||||
} else {
|
||||
t8 = $[18];
|
||||
}
|
||||
let t9;
|
||||
if ($[19] !== item.marketplace) {
|
||||
t9 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[19] = item.marketplace;
|
||||
$[20] = t9;
|
||||
} else {
|
||||
t9 = $[20];
|
||||
}
|
||||
const t10 = !isSelected;
|
||||
let t11;
|
||||
if ($[21] !== statusIcon || $[22] !== t10) {
|
||||
t11 = <Text dimColor={t10}> · {statusIcon} </Text>;
|
||||
$[21] = statusIcon;
|
||||
$[22] = t10;
|
||||
$[23] = t11;
|
||||
} else {
|
||||
t11 = $[23];
|
||||
}
|
||||
const t12 = !isSelected;
|
||||
let t13;
|
||||
if ($[24] !== statusText || $[25] !== t12) {
|
||||
t13 = <Text dimColor={t12}>{statusText}</Text>;
|
||||
$[24] = statusText;
|
||||
$[25] = t12;
|
||||
$[26] = t13;
|
||||
} else {
|
||||
t13 = $[26];
|
||||
}
|
||||
let t14;
|
||||
if ($[27] !== t11 || $[28] !== t13 || $[29] !== t3 || $[30] !== t5 || $[31] !== t8 || $[32] !== t9) {
|
||||
t14 = <Box>{t3}{t5}{t8}{t9}{t11}{t13}</Box>;
|
||||
$[27] = t11;
|
||||
$[28] = t13;
|
||||
$[29] = t3;
|
||||
$[30] = t5;
|
||||
$[31] = t8;
|
||||
$[32] = t9;
|
||||
$[33] = t14;
|
||||
} else {
|
||||
t14 = $[33];
|
||||
}
|
||||
return t14;
|
||||
}
|
||||
if (item.type === "flagged-plugin") {
|
||||
let t1;
|
||||
if ($[34] !== theme) {
|
||||
t1 = color("warning", theme)(figures.warning);
|
||||
$[34] = theme;
|
||||
$[35] = t1;
|
||||
} else {
|
||||
t1 = $[35];
|
||||
}
|
||||
const statusIcon_0 = t1;
|
||||
const t2 = isSelected ? "suggestion" : undefined;
|
||||
const t3 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t4;
|
||||
if ($[36] !== t2 || $[37] !== t3) {
|
||||
t4 = <Text color={t2}>{t3}</Text>;
|
||||
$[36] = t2;
|
||||
$[37] = t3;
|
||||
$[38] = t4;
|
||||
} else {
|
||||
t4 = $[38];
|
||||
}
|
||||
const t5 = isSelected ? "suggestion" : undefined;
|
||||
let t6;
|
||||
if ($[39] !== item.name || $[40] !== t5) {
|
||||
t6 = <Text color={t5}>{item.name}</Text>;
|
||||
$[39] = item.name;
|
||||
$[40] = t5;
|
||||
$[41] = t6;
|
||||
} else {
|
||||
t6 = $[41];
|
||||
}
|
||||
const t7 = !isSelected;
|
||||
let t8;
|
||||
if ($[42] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[42] = t8;
|
||||
} else {
|
||||
t8 = $[42];
|
||||
}
|
||||
let t9;
|
||||
if ($[43] !== t7) {
|
||||
t9 = <Text dimColor={t7}>{" "}{t8}</Text>;
|
||||
$[43] = t7;
|
||||
$[44] = t9;
|
||||
} else {
|
||||
t9 = $[44];
|
||||
}
|
||||
let t10;
|
||||
if ($[45] !== item.marketplace) {
|
||||
t10 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[45] = item.marketplace;
|
||||
$[46] = t10;
|
||||
} else {
|
||||
t10 = $[46];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[47] !== statusIcon_0 || $[48] !== t11) {
|
||||
t12 = <Text dimColor={t11}> · {statusIcon_0} </Text>;
|
||||
$[47] = statusIcon_0;
|
||||
$[48] = t11;
|
||||
$[49] = t12;
|
||||
} else {
|
||||
t12 = $[49];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[50] !== t13) {
|
||||
t14 = <Text dimColor={t13}>removed</Text>;
|
||||
$[50] = t13;
|
||||
$[51] = t14;
|
||||
} else {
|
||||
t14 = $[51];
|
||||
}
|
||||
let t15;
|
||||
if ($[52] !== t10 || $[53] !== t12 || $[54] !== t14 || $[55] !== t4 || $[56] !== t6 || $[57] !== t9) {
|
||||
t15 = <Box>{t4}{t6}{t9}{t10}{t12}{t14}</Box>;
|
||||
$[52] = t10;
|
||||
$[53] = t12;
|
||||
$[54] = t14;
|
||||
$[55] = t4;
|
||||
$[56] = t6;
|
||||
$[57] = t9;
|
||||
$[58] = t15;
|
||||
} else {
|
||||
t15 = $[58];
|
||||
}
|
||||
return t15;
|
||||
}
|
||||
if (item.type === "failed-plugin") {
|
||||
let t1;
|
||||
if ($[59] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[59] = theme;
|
||||
$[60] = t1;
|
||||
} else {
|
||||
t1 = $[60];
|
||||
}
|
||||
const statusIcon_1 = t1;
|
||||
const t2 = item.errorCount;
|
||||
let t3;
|
||||
if ($[61] !== item.errorCount) {
|
||||
t3 = plural(item.errorCount, "error");
|
||||
$[61] = item.errorCount;
|
||||
$[62] = t3;
|
||||
} else {
|
||||
t3 = $[62];
|
||||
}
|
||||
const statusText_0 = `failed to load · ${t2} ${t3}`;
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
const t5 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t6;
|
||||
if ($[63] !== t4 || $[64] !== t5) {
|
||||
t6 = <Text color={t4}>{t5}</Text>;
|
||||
$[63] = t4;
|
||||
$[64] = t5;
|
||||
$[65] = t6;
|
||||
} else {
|
||||
t6 = $[65];
|
||||
}
|
||||
const t7 = isSelected ? "suggestion" : undefined;
|
||||
let t8;
|
||||
if ($[66] !== item.name || $[67] !== t7) {
|
||||
t8 = <Text color={t7}>{item.name}</Text>;
|
||||
$[66] = item.name;
|
||||
$[67] = t7;
|
||||
$[68] = t8;
|
||||
} else {
|
||||
t8 = $[68];
|
||||
}
|
||||
const t9 = !isSelected;
|
||||
let t10;
|
||||
if ($[69] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t10 = <Text backgroundColor="userMessageBackground">Plugin</Text>;
|
||||
$[69] = t10;
|
||||
} else {
|
||||
t10 = $[69];
|
||||
}
|
||||
let t11;
|
||||
if ($[70] !== t9) {
|
||||
t11 = <Text dimColor={t9}>{" "}{t10}</Text>;
|
||||
$[70] = t9;
|
||||
$[71] = t11;
|
||||
} else {
|
||||
t11 = $[71];
|
||||
}
|
||||
let t12;
|
||||
if ($[72] !== item.marketplace) {
|
||||
t12 = <Text dimColor={true}> · {item.marketplace}</Text>;
|
||||
$[72] = item.marketplace;
|
||||
$[73] = t12;
|
||||
} else {
|
||||
t12 = $[73];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[74] !== statusIcon_1 || $[75] !== t13) {
|
||||
t14 = <Text dimColor={t13}> · {statusIcon_1} </Text>;
|
||||
$[74] = statusIcon_1;
|
||||
$[75] = t13;
|
||||
$[76] = t14;
|
||||
} else {
|
||||
t14 = $[76];
|
||||
}
|
||||
const t15 = !isSelected;
|
||||
let t16;
|
||||
if ($[77] !== statusText_0 || $[78] !== t15) {
|
||||
t16 = <Text dimColor={t15}>{statusText_0}</Text>;
|
||||
$[77] = statusText_0;
|
||||
$[78] = t15;
|
||||
$[79] = t16;
|
||||
} else {
|
||||
t16 = $[79];
|
||||
}
|
||||
let t17;
|
||||
if ($[80] !== t11 || $[81] !== t12 || $[82] !== t14 || $[83] !== t16 || $[84] !== t6 || $[85] !== t8) {
|
||||
t17 = <Box>{t6}{t8}{t11}{t12}{t14}{t16}</Box>;
|
||||
$[80] = t11;
|
||||
$[81] = t12;
|
||||
$[82] = t14;
|
||||
$[83] = t16;
|
||||
$[84] = t6;
|
||||
$[85] = t8;
|
||||
$[86] = t17;
|
||||
} else {
|
||||
t17 = $[86];
|
||||
}
|
||||
return t17;
|
||||
}
|
||||
let statusIcon_2;
|
||||
let statusText_1;
|
||||
if (item.status === "connected") {
|
||||
let t1;
|
||||
if ($[87] !== theme) {
|
||||
t1 = color("success", theme)(figures.tick);
|
||||
$[87] = theme;
|
||||
$[88] = t1;
|
||||
} else {
|
||||
t1 = $[88];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "connected";
|
||||
} else {
|
||||
if (item.status === "disabled") {
|
||||
let t1;
|
||||
if ($[89] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[89] = theme;
|
||||
$[90] = t1;
|
||||
} else {
|
||||
t1 = $[90];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "disabled";
|
||||
} else {
|
||||
if (item.status === "pending") {
|
||||
let t1;
|
||||
if ($[91] !== theme) {
|
||||
t1 = color("inactive", theme)(figures.radioOff);
|
||||
$[91] = theme;
|
||||
$[92] = t1;
|
||||
} else {
|
||||
t1 = $[92];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "connecting\u2026";
|
||||
} else {
|
||||
if (item.status === "needs-auth") {
|
||||
let t1;
|
||||
if ($[93] !== theme) {
|
||||
t1 = color("warning", theme)(figures.triangleUpOutline);
|
||||
$[93] = theme;
|
||||
$[94] = t1;
|
||||
} else {
|
||||
t1 = $[94];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "Enter to auth";
|
||||
} else {
|
||||
let t1;
|
||||
if ($[95] !== theme) {
|
||||
t1 = color("error", theme)(figures.cross);
|
||||
$[95] = theme;
|
||||
$[96] = t1;
|
||||
} else {
|
||||
t1 = $[96];
|
||||
}
|
||||
statusIcon_2 = t1;
|
||||
statusText_1 = "failed";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (item.indented) {
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[97] !== t1 || $[98] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[97] = t1;
|
||||
$[98] = t2;
|
||||
$[99] = t3;
|
||||
} else {
|
||||
t3 = $[99];
|
||||
}
|
||||
const t4 = !isSelected;
|
||||
let t5;
|
||||
if ($[100] !== t4) {
|
||||
t5 = <Text dimColor={t4}>└ </Text>;
|
||||
$[100] = t4;
|
||||
$[101] = t5;
|
||||
} else {
|
||||
t5 = $[101];
|
||||
}
|
||||
const t6 = isSelected ? "suggestion" : undefined;
|
||||
let t7;
|
||||
if ($[102] !== item.name || $[103] !== t6) {
|
||||
t7 = <Text color={t6}>{item.name}</Text>;
|
||||
$[102] = item.name;
|
||||
$[103] = t6;
|
||||
$[104] = t7;
|
||||
} else {
|
||||
t7 = $[104];
|
||||
}
|
||||
const t8 = !isSelected;
|
||||
let t9;
|
||||
if ($[105] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Text backgroundColor="userMessageBackground">MCP</Text>;
|
||||
$[105] = t9;
|
||||
} else {
|
||||
t9 = $[105];
|
||||
}
|
||||
let t10;
|
||||
if ($[106] !== t8) {
|
||||
t10 = <Text dimColor={t8}>{" "}{t9}</Text>;
|
||||
$[106] = t8;
|
||||
$[107] = t10;
|
||||
} else {
|
||||
t10 = $[107];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[108] !== statusIcon_2 || $[109] !== t11) {
|
||||
t12 = <Text dimColor={t11}> · {statusIcon_2} </Text>;
|
||||
$[108] = statusIcon_2;
|
||||
$[109] = t11;
|
||||
$[110] = t12;
|
||||
} else {
|
||||
t12 = $[110];
|
||||
}
|
||||
const t13 = !isSelected;
|
||||
let t14;
|
||||
if ($[111] !== statusText_1 || $[112] !== t13) {
|
||||
t14 = <Text dimColor={t13}>{statusText_1}</Text>;
|
||||
$[111] = statusText_1;
|
||||
$[112] = t13;
|
||||
$[113] = t14;
|
||||
} else {
|
||||
t14 = $[113];
|
||||
}
|
||||
let t15;
|
||||
if ($[114] !== t10 || $[115] !== t12 || $[116] !== t14 || $[117] !== t3 || $[118] !== t5 || $[119] !== t7) {
|
||||
t15 = <Box>{t3}{t5}{t7}{t10}{t12}{t14}</Box>;
|
||||
$[114] = t10;
|
||||
$[115] = t12;
|
||||
$[116] = t14;
|
||||
$[117] = t3;
|
||||
$[118] = t5;
|
||||
$[119] = t7;
|
||||
$[120] = t15;
|
||||
} else {
|
||||
t15 = $[120];
|
||||
}
|
||||
return t15;
|
||||
}
|
||||
const t1 = isSelected ? "suggestion" : undefined;
|
||||
const t2 = isSelected ? `${figures.pointer} ` : " ";
|
||||
let t3;
|
||||
if ($[121] !== t1 || $[122] !== t2) {
|
||||
t3 = <Text color={t1}>{t2}</Text>;
|
||||
$[121] = t1;
|
||||
$[122] = t2;
|
||||
$[123] = t3;
|
||||
} else {
|
||||
t3 = $[123];
|
||||
}
|
||||
const t4 = isSelected ? "suggestion" : undefined;
|
||||
let t5;
|
||||
if ($[124] !== item.name || $[125] !== t4) {
|
||||
t5 = <Text color={t4}>{item.name}</Text>;
|
||||
$[124] = item.name;
|
||||
$[125] = t4;
|
||||
$[126] = t5;
|
||||
} else {
|
||||
t5 = $[126];
|
||||
}
|
||||
const t6 = !isSelected;
|
||||
let t7;
|
||||
if ($[127] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t7 = <Text backgroundColor="userMessageBackground">MCP</Text>;
|
||||
$[127] = t7;
|
||||
} else {
|
||||
t7 = $[127];
|
||||
}
|
||||
let t8;
|
||||
if ($[128] !== t6) {
|
||||
t8 = <Text dimColor={t6}>{" "}{t7}</Text>;
|
||||
$[128] = t6;
|
||||
$[129] = t8;
|
||||
} else {
|
||||
t8 = $[129];
|
||||
}
|
||||
const t9 = !isSelected;
|
||||
let t10;
|
||||
if ($[130] !== statusIcon_2 || $[131] !== t9) {
|
||||
t10 = <Text dimColor={t9}> · {statusIcon_2} </Text>;
|
||||
$[130] = statusIcon_2;
|
||||
$[131] = t9;
|
||||
$[132] = t10;
|
||||
} else {
|
||||
t10 = $[132];
|
||||
}
|
||||
const t11 = !isSelected;
|
||||
let t12;
|
||||
if ($[133] !== statusText_1 || $[134] !== t11) {
|
||||
t12 = <Text dimColor={t11}>{statusText_1}</Text>;
|
||||
$[133] = statusText_1;
|
||||
$[134] = t11;
|
||||
$[135] = t12;
|
||||
} else {
|
||||
t12 = $[135];
|
||||
}
|
||||
let t13;
|
||||
if ($[136] !== t10 || $[137] !== t12 || $[138] !== t3 || $[139] !== t5 || $[140] !== t8) {
|
||||
t13 = <Box>{t3}{t5}{t8}{t10}{t12}</Box>;
|
||||
$[136] = t10;
|
||||
$[137] = t12;
|
||||
$[138] = t3;
|
||||
$[139] = t5;
|
||||
$[140] = t8;
|
||||
$[141] = t13;
|
||||
} else {
|
||||
t13 = $[141];
|
||||
}
|
||||
return t13;
|
||||
item: UnifiedInstalledItem
|
||||
isSelected: boolean
|
||||
}
|
||||
|
||||
export function UnifiedInstalledCell({
|
||||
item,
|
||||
isSelected,
|
||||
}: Props): React.ReactNode {
|
||||
const [theme] = useTheme()
|
||||
|
||||
if (item.type === 'plugin') {
|
||||
// Status icon and text
|
||||
let statusIcon: string
|
||||
let statusText: string
|
||||
|
||||
// Show pending toggle status if set, otherwise show current status
|
||||
if (item.pendingToggle) {
|
||||
statusIcon = color('suggestion', theme)(figures.arrowRight)
|
||||
statusText =
|
||||
item.pendingToggle === 'will-enable' ? 'will enable' : 'will disable'
|
||||
} else if (item.errorCount > 0) {
|
||||
statusIcon = color('error', theme)(figures.cross)
|
||||
statusText = `${item.errorCount} ${plural(item.errorCount, 'error')}`
|
||||
} else if (!item.isEnabled) {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
statusText = 'disabled'
|
||||
} else {
|
||||
statusIcon = color('success', theme)(figures.tick)
|
||||
statusText = 'enabled'
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">Plugin</Text>
|
||||
</Text>
|
||||
<Text dimColor> · {item.marketplace}</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'flagged-plugin') {
|
||||
const statusIcon = color('warning', theme)(figures.warning)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">Plugin</Text>
|
||||
</Text>
|
||||
<Text dimColor> · {item.marketplace}</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>removed</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (item.type === 'failed-plugin') {
|
||||
const statusIcon = color('error', theme)(figures.cross)
|
||||
const statusText = `failed to load · ${item.errorCount} ${plural(item.errorCount, 'error')}`
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">Plugin</Text>
|
||||
</Text>
|
||||
<Text dimColor> · {item.marketplace}</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
// MCP server
|
||||
let statusIcon: string
|
||||
let statusText: string
|
||||
|
||||
if (item.status === 'connected') {
|
||||
statusIcon = color('success', theme)(figures.tick)
|
||||
statusText = 'connected'
|
||||
} else if (item.status === 'disabled') {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
statusText = 'disabled'
|
||||
} else if (item.status === 'pending') {
|
||||
statusIcon = color('inactive', theme)(figures.radioOff)
|
||||
statusText = 'connecting…'
|
||||
} else if (item.status === 'needs-auth') {
|
||||
statusIcon = color('warning', theme)(figures.triangleUpOutline)
|
||||
statusText = 'Enter to auth'
|
||||
} else {
|
||||
statusIcon = color('error', theme)(figures.cross)
|
||||
statusText = 'failed'
|
||||
}
|
||||
|
||||
// Indented MCPs (child of a plugin)
|
||||
if (item.indented) {
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}>└ </Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">MCP</Text>
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>
|
||||
{isSelected ? `${figures.pointer} ` : ' '}
|
||||
</Text>
|
||||
<Text color={isSelected ? 'suggestion' : undefined}>{item.name}</Text>
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
<Text backgroundColor="userMessageBackground">MCP</Text>
|
||||
</Text>
|
||||
<Text dimColor={!isSelected}> · {statusIcon} </Text>
|
||||
<Text dimColor={!isSelected}>{statusText}</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,97 +1,103 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { errorMessage } from '../../utils/errors.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { validateManifest } from '../../utils/plugins/validatePlugin.js';
|
||||
import { plural } from '../../utils/stringUtils.js';
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { errorMessage } from '../../utils/errors.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { validateManifest } from '../../utils/plugins/validatePlugin.js'
|
||||
import { plural } from '../../utils/stringUtils.js'
|
||||
|
||||
type Props = {
|
||||
onComplete: (result?: string) => void;
|
||||
path?: string;
|
||||
};
|
||||
export function ValidatePlugin(t0) {
|
||||
const $ = _c(5);
|
||||
const {
|
||||
onComplete,
|
||||
path
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onComplete || $[1] !== path) {
|
||||
t1 = () => {
|
||||
const runValidation = async function runValidation() {
|
||||
if (!path) {
|
||||
onComplete("Usage: /plugin validate <path>\n\nValidate a plugin or marketplace manifest file or directory.\n\nExamples:\n /plugin validate .claude-plugin/plugin.json\n /plugin validate /path/to/plugin-directory\n /plugin validate .\n\nWhen given a directory, automatically validates .claude-plugin/marketplace.json\nor .claude-plugin/plugin.json (prefers marketplace if both exist).\n\nOr from the command line:\n claude plugin validate <path>");
|
||||
return;
|
||||
}
|
||||
;
|
||||
try {
|
||||
const result = await validateManifest(path);
|
||||
let output = "";
|
||||
output = output + `Validating ${result.fileType} manifest: ${result.filePath}\n\n`;
|
||||
output;
|
||||
if (result.errors.length > 0) {
|
||||
output = output + `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, "error")}:\n\n`;
|
||||
output;
|
||||
result.errors.forEach(error_0 => {
|
||||
output = output + ` ${figures.pointer} ${error_0.path}: ${error_0.message}\n`;
|
||||
output;
|
||||
});
|
||||
output = output + "\n";
|
||||
output;
|
||||
}
|
||||
if (result.warnings.length > 0) {
|
||||
output = output + `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, "warning")}:\n\n`;
|
||||
output;
|
||||
result.warnings.forEach(warning => {
|
||||
output = output + ` ${figures.pointer} ${warning.path}: ${warning.message}\n`;
|
||||
output;
|
||||
});
|
||||
output = output + "\n";
|
||||
output;
|
||||
}
|
||||
if (result.success) {
|
||||
if (result.warnings.length > 0) {
|
||||
output = output + `${figures.tick} Validation passed with warnings\n`;
|
||||
output;
|
||||
} else {
|
||||
output = output + `${figures.tick} Validation passed\n`;
|
||||
output;
|
||||
}
|
||||
process.exitCode = 0;
|
||||
} else {
|
||||
output = output + `${figures.cross} Validation failed\n`;
|
||||
output;
|
||||
process.exitCode = 1;
|
||||
}
|
||||
onComplete(output);
|
||||
} catch (t3) {
|
||||
const error = t3;
|
||||
process.exitCode = 2;
|
||||
logError(error);
|
||||
onComplete(`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`);
|
||||
}
|
||||
};
|
||||
runValidation();
|
||||
};
|
||||
t2 = [onComplete, path];
|
||||
$[0] = onComplete;
|
||||
$[1] = path;
|
||||
$[2] = t1;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
t2 = $[3];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = <Box flexDirection="column"><Text>Running validation...</Text></Box>;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
return t3;
|
||||
onComplete: (result?: string) => void
|
||||
path?: string
|
||||
}
|
||||
|
||||
export function ValidatePlugin({ onComplete, path }: Props): React.ReactNode {
|
||||
useEffect(() => {
|
||||
async function runValidation() {
|
||||
// If no path provided, show usage
|
||||
if (!path) {
|
||||
onComplete(
|
||||
'Usage: /plugin validate <path>\n\n' +
|
||||
'Validate a plugin or marketplace manifest file or directory.\n\n' +
|
||||
'Examples:\n' +
|
||||
' /plugin validate .claude-plugin/plugin.json\n' +
|
||||
' /plugin validate /path/to/plugin-directory\n' +
|
||||
' /plugin validate .\n\n' +
|
||||
'When given a directory, automatically validates .claude-plugin/marketplace.json\n' +
|
||||
'or .claude-plugin/plugin.json (prefers marketplace if both exist).\n\n' +
|
||||
'Or from the command line:\n' +
|
||||
' claude plugin validate <path>',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await validateManifest(path)
|
||||
|
||||
let output = ''
|
||||
|
||||
// Add header
|
||||
output += `Validating ${result.fileType} manifest: ${result.filePath}\n\n`
|
||||
|
||||
// Show errors
|
||||
if (result.errors.length > 0) {
|
||||
output += `${figures.cross} Found ${result.errors.length} ${plural(result.errors.length, 'error')}:\n\n`
|
||||
|
||||
result.errors.forEach(error => {
|
||||
output += ` ${figures.pointer} ${error.path}: ${error.message}\n`
|
||||
})
|
||||
|
||||
output += '\n'
|
||||
}
|
||||
|
||||
// Show warnings
|
||||
if (result.warnings.length > 0) {
|
||||
output += `${figures.warning} Found ${result.warnings.length} ${plural(result.warnings.length, 'warning')}:\n\n`
|
||||
|
||||
result.warnings.forEach(warning => {
|
||||
output += ` ${figures.pointer} ${warning.path}: ${warning.message}\n`
|
||||
})
|
||||
|
||||
output += '\n'
|
||||
}
|
||||
|
||||
// Show success or failure
|
||||
if (result.success) {
|
||||
if (result.warnings.length > 0) {
|
||||
output += `${figures.tick} Validation passed with warnings\n`
|
||||
} else {
|
||||
output += `${figures.tick} Validation passed\n`
|
||||
}
|
||||
|
||||
// Exit with code 0 (success)
|
||||
process.exitCode = 0
|
||||
} else {
|
||||
output += `${figures.cross} Validation failed\n`
|
||||
|
||||
// Exit with code 1 (validation failure)
|
||||
process.exitCode = 1
|
||||
}
|
||||
|
||||
onComplete(output)
|
||||
} catch (error) {
|
||||
// Exit with code 2 (unexpected error)
|
||||
process.exitCode = 2
|
||||
|
||||
logError(error)
|
||||
|
||||
onComplete(
|
||||
`${figures.cross} Unexpected error during validation: ${errorMessage(error)}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
void runValidation()
|
||||
}, [onComplete, path])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text>Running validation...</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Command } from '../../commands.js';
|
||||
import type { Command } from '../../commands.js'
|
||||
|
||||
const plugin = {
|
||||
type: 'local-jsx',
|
||||
name: 'plugin',
|
||||
aliases: ['plugins', 'marketplace'],
|
||||
description: 'Manage Claude Code plugins',
|
||||
immediate: true,
|
||||
load: () => import('./plugin.js')
|
||||
} satisfies Command;
|
||||
export default plugin;
|
||||
load: () => import('./plugin.js'),
|
||||
} satisfies Command
|
||||
|
||||
export default plugin
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { PluginSettings } from './PluginSettings.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
return <PluginSettings onComplete={onDone} args={args} />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { PluginSettings } from './PluginSettings.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
return <PluginSettings onComplete={onDone} args={args} />
|
||||
}
|
||||
|
||||
@@ -1,116 +1,123 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
/**
|
||||
* Shared helper functions and types for plugin details views
|
||||
*
|
||||
* Used by both DiscoverPlugins and BrowseMarketplace components.
|
||||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js';
|
||||
import { Byline } from '../../components/design-system/Byline.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js';
|
||||
import * as React from 'react'
|
||||
import { ConfigurableShortcutHint } from '../../components/ConfigurableShortcutHint.js'
|
||||
import { Byline } from '../../components/design-system/Byline.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { PluginMarketplaceEntry } from '../../utils/plugins/schemas.js'
|
||||
|
||||
/**
|
||||
* Represents a plugin available for installation from a marketplace
|
||||
*/
|
||||
export type InstallablePlugin = {
|
||||
entry: PluginMarketplaceEntry;
|
||||
marketplaceName: string;
|
||||
pluginId: string;
|
||||
isInstalled: boolean;
|
||||
};
|
||||
entry: PluginMarketplaceEntry
|
||||
marketplaceName: string
|
||||
pluginId: string
|
||||
isInstalled: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu option for plugin details view
|
||||
*/
|
||||
export type PluginDetailsMenuOption = {
|
||||
label: string;
|
||||
action: string;
|
||||
};
|
||||
label: string
|
||||
action: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract GitHub repo info from a plugin's source
|
||||
*/
|
||||
export function extractGitHubRepo(plugin: InstallablePlugin): string | null {
|
||||
const isGitHub = plugin.entry.source && typeof plugin.entry.source === 'object' && 'source' in plugin.entry.source && plugin.entry.source.source === 'github';
|
||||
if (isGitHub && typeof plugin.entry.source === 'object' && 'repo' in plugin.entry.source) {
|
||||
return plugin.entry.source.repo;
|
||||
const isGitHub =
|
||||
plugin.entry.source &&
|
||||
typeof plugin.entry.source === 'object' &&
|
||||
'source' in plugin.entry.source &&
|
||||
plugin.entry.source.source === 'github'
|
||||
|
||||
if (
|
||||
isGitHub &&
|
||||
typeof plugin.entry.source === 'object' &&
|
||||
'repo' in plugin.entry.source
|
||||
) {
|
||||
return plugin.entry.source.repo
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Build menu options for plugin details view with scoped installation options
|
||||
*/
|
||||
export function buildPluginDetailsMenuOptions(hasHomepage: string | undefined, githubRepo: string | null): PluginDetailsMenuOption[] {
|
||||
const options: PluginDetailsMenuOption[] = [{
|
||||
label: 'Install for you (user scope)',
|
||||
action: 'install-user'
|
||||
}, {
|
||||
label: 'Install for all collaborators on this repository (project scope)',
|
||||
action: 'install-project'
|
||||
}, {
|
||||
label: 'Install for you, in this repo only (local scope)',
|
||||
action: 'install-local'
|
||||
}];
|
||||
export function buildPluginDetailsMenuOptions(
|
||||
hasHomepage: string | undefined,
|
||||
githubRepo: string | null,
|
||||
): PluginDetailsMenuOption[] {
|
||||
const options: PluginDetailsMenuOption[] = [
|
||||
{ label: 'Install for you (user scope)', action: 'install-user' },
|
||||
{
|
||||
label: 'Install for all collaborators on this repository (project scope)',
|
||||
action: 'install-project',
|
||||
},
|
||||
{
|
||||
label: 'Install for you, in this repo only (local scope)',
|
||||
action: 'install-local',
|
||||
},
|
||||
]
|
||||
if (hasHomepage) {
|
||||
options.push({
|
||||
label: 'Open homepage',
|
||||
action: 'homepage'
|
||||
});
|
||||
options.push({ label: 'Open homepage', action: 'homepage' })
|
||||
}
|
||||
if (githubRepo) {
|
||||
options.push({
|
||||
label: 'View on GitHub',
|
||||
action: 'github'
|
||||
});
|
||||
options.push({ label: 'View on GitHub', action: 'github' })
|
||||
}
|
||||
options.push({
|
||||
label: 'Back to plugin list',
|
||||
action: 'back'
|
||||
});
|
||||
return options;
|
||||
options.push({ label: 'Back to plugin list', action: 'back' })
|
||||
return options
|
||||
}
|
||||
|
||||
/**
|
||||
* Key hint component for plugin selection screens
|
||||
*/
|
||||
export function PluginSelectionKeyHint(t0) {
|
||||
const $ = _c(7);
|
||||
const {
|
||||
hasSelection
|
||||
} = t0;
|
||||
let t1;
|
||||
if ($[0] !== hasSelection) {
|
||||
t1 = hasSelection && <ConfigurableShortcutHint action="plugin:install" context="Plugin" fallback="i" description="install" bold={true} />;
|
||||
$[0] = hasSelection;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
let t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <ConfigurableShortcutHint action="plugin:toggle" context="Plugin" fallback="Space" description="toggle" />;
|
||||
t3 = <ConfigurableShortcutHint action="select:accept" context="Select" fallback="Enter" description="details" />;
|
||||
t4 = <ConfigurableShortcutHint action="confirm:no" context="Confirmation" fallback="Esc" description="back" />;
|
||||
$[2] = t2;
|
||||
$[3] = t3;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
t3 = $[3];
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== t1) {
|
||||
t5 = <Box marginTop={1}><Text dimColor={true} italic={true}><Byline>{t1}{t2}{t3}{t4}</Byline></Text></Box>;
|
||||
$[5] = t1;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
return t5;
|
||||
export function PluginSelectionKeyHint({
|
||||
hasSelection,
|
||||
}: {
|
||||
hasSelection: boolean
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor italic>
|
||||
<Byline>
|
||||
{hasSelection && (
|
||||
<ConfigurableShortcutHint
|
||||
action="plugin:install"
|
||||
context="Plugin"
|
||||
fallback="i"
|
||||
description="install"
|
||||
bold
|
||||
/>
|
||||
)}
|
||||
<ConfigurableShortcutHint
|
||||
action="plugin:toggle"
|
||||
context="Plugin"
|
||||
fallback="Space"
|
||||
description="toggle"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="select:accept"
|
||||
context="Select"
|
||||
fallback="Enter"
|
||||
description="details"
|
||||
/>
|
||||
<ConfigurableShortcutHint
|
||||
action="confirm:no"
|
||||
context="Confirmation"
|
||||
fallback="Esc"
|
||||
description="back"
|
||||
/>
|
||||
</Byline>
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,96 @@
|
||||
import * as React from 'react';
|
||||
import { type GroveDecision, GroveDialog, PrivacySettingsDialog } from '../../components/grove/Grove.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../../services/analytics/index.js';
|
||||
import { getGroveNoticeConfig, getGroveSettings, isQualifiedForGrove } from '../../services/api/grove.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
const FALLBACK_MESSAGE = 'Review and manage your privacy settings at https://claude.ai/settings/data-privacy-controls';
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode | null> {
|
||||
const qualified = await isQualifiedForGrove();
|
||||
import * as React from 'react'
|
||||
import {
|
||||
type GroveDecision,
|
||||
GroveDialog,
|
||||
PrivacySettingsDialog,
|
||||
} from '../../components/grove/Grove.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../../services/analytics/index.js'
|
||||
import {
|
||||
getGroveNoticeConfig,
|
||||
getGroveSettings,
|
||||
isQualifiedForGrove,
|
||||
} from '../../services/api/grove.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
const FALLBACK_MESSAGE =
|
||||
'Review and manage your privacy settings at https://claude.ai/settings/data-privacy-controls'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode | null> {
|
||||
const qualified = await isQualifiedForGrove()
|
||||
if (!qualified) {
|
||||
onDone(FALLBACK_MESSAGE);
|
||||
return null;
|
||||
onDone(FALLBACK_MESSAGE)
|
||||
return null
|
||||
}
|
||||
const [settingsResult, configResult] = await Promise.all([getGroveSettings(), getGroveNoticeConfig()]);
|
||||
|
||||
const [settingsResult, configResult] = await Promise.all([
|
||||
getGroveSettings(),
|
||||
getGroveNoticeConfig(),
|
||||
])
|
||||
// Hide dialog on API failure (after retry)
|
||||
if (!settingsResult.success) {
|
||||
onDone(FALLBACK_MESSAGE);
|
||||
return null;
|
||||
onDone(FALLBACK_MESSAGE)
|
||||
return null
|
||||
}
|
||||
const settings = settingsResult.data;
|
||||
const config = configResult.success ? configResult.data : null;
|
||||
const settings = settingsResult.data
|
||||
const config = configResult.success ? configResult.data : null
|
||||
|
||||
async function onDoneWithDecision(decision: GroveDecision) {
|
||||
if (decision === 'escape' || decision === 'defer') {
|
||||
onDone('Privacy settings dialog dismissed', {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
display: 'system',
|
||||
})
|
||||
return
|
||||
}
|
||||
await onDoneWithSettingsCheck();
|
||||
await onDoneWithSettingsCheck()
|
||||
}
|
||||
|
||||
async function onDoneWithSettingsCheck() {
|
||||
const updatedSettingsResult = await getGroveSettings();
|
||||
const updatedSettingsResult = await getGroveSettings()
|
||||
if (!updatedSettingsResult.success) {
|
||||
onDone('Unable to retrieve updated privacy settings', {
|
||||
display: 'system'
|
||||
});
|
||||
return;
|
||||
display: 'system',
|
||||
})
|
||||
return
|
||||
}
|
||||
const updatedSettings = updatedSettingsResult.data;
|
||||
const groveStatus = updatedSettings.grove_enabled ? 'true' : 'false';
|
||||
onDone(`"Help improve Claude" set to ${groveStatus}.`);
|
||||
if (settings.grove_enabled !== null && settings.grove_enabled !== updatedSettings.grove_enabled) {
|
||||
const updatedSettings = updatedSettingsResult.data
|
||||
const groveStatus = updatedSettings.grove_enabled ? 'true' : 'false'
|
||||
onDone(`"Help improve Claude" set to ${groveStatus}.`)
|
||||
if (
|
||||
settings.grove_enabled !== null &&
|
||||
settings.grove_enabled !== updatedSettings.grove_enabled
|
||||
) {
|
||||
logEvent('tengu_grove_policy_toggled', {
|
||||
state: updatedSettings.grove_enabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
location: 'settings' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
state:
|
||||
updatedSettings.grove_enabled as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
location:
|
||||
'settings' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Show privacy settings directly if the user has already accepted the
|
||||
// terms.
|
||||
if (settings.grove_enabled !== null) {
|
||||
return <PrivacySettingsDialog settings={settings} domainExcluded={config?.domain_excluded} onDone={onDoneWithSettingsCheck}></PrivacySettingsDialog>;
|
||||
return (
|
||||
<PrivacySettingsDialog
|
||||
settings={settings}
|
||||
domainExcluded={config?.domain_excluded}
|
||||
onDone={onDoneWithSettingsCheck}
|
||||
></PrivacySettingsDialog>
|
||||
)
|
||||
}
|
||||
|
||||
// Show the GroveDialog for users who haven't accepted terms yet
|
||||
return <GroveDialog showIfAlreadyViewed={true} onDone={onDoneWithDecision} location={'settings'} />;
|
||||
return (
|
||||
<GroveDialog
|
||||
showIfAlreadyViewed={true}
|
||||
onDone={onDoneWithDecision}
|
||||
location={'settings'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,209 +1,177 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import type { CommandResultDisplay, LocalJSXCommandContext } from '../../commands.js';
|
||||
import { type OptionWithDescription, Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getOauthAccountInfo, getRateLimitTier, getSubscriptionType } from '../../utils/auth.js';
|
||||
import { hasClaudeAiBillingAccess } from '../../utils/billing.js';
|
||||
import { call as extraUsageCall } from '../extra-usage/extra-usage.js';
|
||||
import { extraUsage } from '../extra-usage/index.js';
|
||||
import upgrade from '../upgrade/index.js';
|
||||
import { call as upgradeCall } from '../upgrade/upgrade.js';
|
||||
type RateLimitOptionsMenuOptionType = 'upgrade' | 'extra-usage' | 'cancel';
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import type {
|
||||
CommandResultDisplay,
|
||||
LocalJSXCommandContext,
|
||||
} from '../../commands.js'
|
||||
import {
|
||||
type OptionWithDescription,
|
||||
Select,
|
||||
} from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import { useClaudeAiLimits } from '../../services/claudeAiLimitsHook.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
getOauthAccountInfo,
|
||||
getRateLimitTier,
|
||||
getSubscriptionType,
|
||||
} from '../../utils/auth.js'
|
||||
import { hasClaudeAiBillingAccess } from '../../utils/billing.js'
|
||||
import { call as extraUsageCall } from '../extra-usage/extra-usage.js'
|
||||
import { extraUsage } from '../extra-usage/index.js'
|
||||
import upgrade from '../upgrade/index.js'
|
||||
import { call as upgradeCall } from '../upgrade/upgrade.js'
|
||||
|
||||
type RateLimitOptionsMenuOptionType = 'upgrade' | 'extra-usage' | 'cancel'
|
||||
|
||||
type RateLimitOptionsMenuProps = {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay | undefined;
|
||||
} | undefined) => void;
|
||||
context: ToolUseContext & LocalJSXCommandContext;
|
||||
};
|
||||
function RateLimitOptionsMenu(t0) {
|
||||
const $ = _c(25);
|
||||
const {
|
||||
onDone,
|
||||
context
|
||||
} = t0;
|
||||
const [subCommandJSX, setSubCommandJSX] = useState(null);
|
||||
const claudeAiLimits = useClaudeAiLimits();
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = getSubscriptionType();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const subscriptionType = t1;
|
||||
let t2;
|
||||
if ($[1] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = getRateLimitTier();
|
||||
$[1] = t2;
|
||||
} else {
|
||||
t2 = $[1];
|
||||
}
|
||||
const rateLimitTier = t2;
|
||||
const hasExtraUsageEnabled = getOauthAccountInfo()?.hasExtraUsageEnabled === true;
|
||||
const isMax = subscriptionType === "max";
|
||||
const isMax20x = isMax && rateLimitTier === "default_claude_max_20x";
|
||||
const isTeamOrEnterprise = subscriptionType === "team" || subscriptionType === "enterprise";
|
||||
const buyFirst = getFeatureValue_CACHED_MAY_BE_STALE("tengu_jade_anvil_4", false);
|
||||
let t3;
|
||||
bb0: {
|
||||
let actionOptions;
|
||||
if ($[2] !== claudeAiLimits.overageDisabledReason || $[3] !== claudeAiLimits.overageStatus) {
|
||||
actionOptions = [];
|
||||
if (extraUsage.isEnabled()) {
|
||||
const hasBillingAccess = hasClaudeAiBillingAccess();
|
||||
const needsToRequestFromAdmin = isTeamOrEnterprise && !hasBillingAccess;
|
||||
const isOrgSpendCapDepleted = claudeAiLimits.overageDisabledReason === "out_of_credits" || claudeAiLimits.overageDisabledReason === "org_level_disabled_until" || claudeAiLimits.overageDisabledReason === "org_service_zero_credit_limit";
|
||||
if (needsToRequestFromAdmin && isOrgSpendCapDepleted) {} else {
|
||||
const isOverageState = claudeAiLimits.overageStatus === "rejected" || claudeAiLimits.overageStatus === "allowed_warning";
|
||||
let label;
|
||||
if (needsToRequestFromAdmin) {
|
||||
label = isOverageState ? "Request more" : "Request extra usage";
|
||||
} else {
|
||||
label = hasExtraUsageEnabled ? "Add funds to continue with extra usage" : "Switch to extra usage";
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== label) {
|
||||
t4 = {
|
||||
label,
|
||||
value: "extra-usage"
|
||||
};
|
||||
$[5] = label;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
actionOptions.push(t4);
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?:
|
||||
| {
|
||||
display?: CommandResultDisplay | undefined
|
||||
}
|
||||
}
|
||||
if (!isMax20x && !isTeamOrEnterprise && upgrade.isEnabled()) {
|
||||
let t4;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Upgrade your plan",
|
||||
value: "upgrade"
|
||||
};
|
||||
$[7] = t4;
|
||||
| undefined,
|
||||
) => void
|
||||
context: ToolUseContext & LocalJSXCommandContext
|
||||
}
|
||||
|
||||
function RateLimitOptionsMenu({
|
||||
onDone,
|
||||
context,
|
||||
}: RateLimitOptionsMenuProps): React.ReactNode {
|
||||
const [subCommandJSX, setSubCommandJSX] = useState<React.ReactNode>(null)
|
||||
const claudeAiLimits = useClaudeAiLimits()
|
||||
const subscriptionType = getSubscriptionType()
|
||||
const rateLimitTier = getRateLimitTier()
|
||||
const hasExtraUsageEnabled =
|
||||
getOauthAccountInfo()?.hasExtraUsageEnabled === true
|
||||
const isMax = subscriptionType === 'max'
|
||||
const isMax20x = isMax && rateLimitTier === 'default_claude_max_20x'
|
||||
const isTeamOrEnterprise =
|
||||
subscriptionType === 'team' || subscriptionType === 'enterprise'
|
||||
const buyFirst = getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_jade_anvil_4',
|
||||
false,
|
||||
)
|
||||
|
||||
const options = useMemo<
|
||||
OptionWithDescription<RateLimitOptionsMenuOptionType>[]
|
||||
>(() => {
|
||||
const actionOptions: OptionWithDescription<RateLimitOptionsMenuOptionType>[] =
|
||||
[]
|
||||
|
||||
if (extraUsage.isEnabled()) {
|
||||
const hasBillingAccess = hasClaudeAiBillingAccess()
|
||||
const needsToRequestFromAdmin = isTeamOrEnterprise && !hasBillingAccess
|
||||
// Org spend cap depleted - non-admins can't request more since there's nothing to allocate
|
||||
// - out_of_credits: wallet empty
|
||||
// - org_level_disabled_until: org spend cap hit for the month
|
||||
// - org_service_zero_credit_limit: org service has zero credit limit
|
||||
const isOrgSpendCapDepleted =
|
||||
claudeAiLimits.overageDisabledReason === 'out_of_credits' ||
|
||||
claudeAiLimits.overageDisabledReason === 'org_level_disabled_until' ||
|
||||
claudeAiLimits.overageDisabledReason === 'org_service_zero_credit_limit'
|
||||
|
||||
// Hide for non-admin Team/Enterprise users when org spend cap is depleted
|
||||
if (needsToRequestFromAdmin && isOrgSpendCapDepleted) {
|
||||
// Don't show extra-usage option
|
||||
} else {
|
||||
const isOverageState =
|
||||
claudeAiLimits.overageStatus === 'rejected' ||
|
||||
claudeAiLimits.overageStatus === 'allowed_warning'
|
||||
|
||||
let label: string
|
||||
if (needsToRequestFromAdmin) {
|
||||
label = isOverageState ? 'Request more' : 'Request extra usage'
|
||||
} else {
|
||||
t4 = $[7];
|
||||
label = hasExtraUsageEnabled
|
||||
? 'Add funds to continue with extra usage'
|
||||
: 'Switch to extra usage'
|
||||
}
|
||||
actionOptions.push(t4);
|
||||
|
||||
actionOptions.push({
|
||||
label,
|
||||
value: 'extra-usage',
|
||||
})
|
||||
}
|
||||
$[2] = claudeAiLimits.overageDisabledReason;
|
||||
$[3] = claudeAiLimits.overageStatus;
|
||||
$[4] = actionOptions;
|
||||
} else {
|
||||
actionOptions = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = {
|
||||
label: "Stop and wait for limit to reset",
|
||||
value: "cancel"
|
||||
};
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t4 = $[8];
|
||||
|
||||
if (!isMax20x && !isTeamOrEnterprise && upgrade.isEnabled()) {
|
||||
actionOptions.push({
|
||||
label: 'Upgrade your plan',
|
||||
value: 'upgrade',
|
||||
})
|
||||
}
|
||||
const cancelOption = t4;
|
||||
|
||||
const cancelOption: OptionWithDescription<RateLimitOptionsMenuOptionType> =
|
||||
{
|
||||
label: 'Stop and wait for limit to reset',
|
||||
value: 'cancel',
|
||||
}
|
||||
|
||||
if (buyFirst) {
|
||||
let t5;
|
||||
if ($[9] !== actionOptions) {
|
||||
t5 = [...actionOptions, cancelOption];
|
||||
$[9] = actionOptions;
|
||||
$[10] = t5;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
}
|
||||
t3 = t5;
|
||||
break bb0;
|
||||
return [...actionOptions, cancelOption]
|
||||
}
|
||||
let t5;
|
||||
if ($[11] !== actionOptions) {
|
||||
t5 = [cancelOption, ...actionOptions];
|
||||
$[11] = actionOptions;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
t3 = t5;
|
||||
return [cancelOption, ...actionOptions]
|
||||
}, [
|
||||
buyFirst,
|
||||
isMax20x,
|
||||
isTeamOrEnterprise,
|
||||
hasExtraUsageEnabled,
|
||||
claudeAiLimits.overageStatus,
|
||||
claudeAiLimits.overageDisabledReason,
|
||||
])
|
||||
|
||||
function handleCancel(): void {
|
||||
logEvent('tengu_rate_limit_options_menu_cancel', {})
|
||||
onDone(undefined, { display: 'skip' })
|
||||
}
|
||||
const options = t3;
|
||||
let t4;
|
||||
if ($[13] !== onDone) {
|
||||
t4 = function handleCancel() {
|
||||
logEvent("tengu_rate_limit_options_menu_cancel", {});
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
};
|
||||
$[13] = onDone;
|
||||
$[14] = t4;
|
||||
} else {
|
||||
t4 = $[14];
|
||||
}
|
||||
const handleCancel = t4;
|
||||
let t5;
|
||||
if ($[15] !== context || $[16] !== handleCancel || $[17] !== onDone) {
|
||||
t5 = function handleSelect(value) {
|
||||
if (value === "upgrade") {
|
||||
logEvent("tengu_rate_limit_options_menu_select_upgrade", {});
|
||||
upgradeCall(onDone, context).then(jsx => {
|
||||
if (jsx) {
|
||||
setSubCommandJSX(jsx);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (value === "extra-usage") {
|
||||
logEvent("tengu_rate_limit_options_menu_select_extra_usage", {});
|
||||
extraUsageCall(onDone, context).then(jsx_0 => {
|
||||
if (jsx_0) {
|
||||
setSubCommandJSX(jsx_0);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (value === "cancel") {
|
||||
handleCancel();
|
||||
}
|
||||
|
||||
function handleSelect(value: RateLimitOptionsMenuOptionType): void {
|
||||
if (value === 'upgrade') {
|
||||
logEvent('tengu_rate_limit_options_menu_select_upgrade', {})
|
||||
void upgradeCall(onDone, context).then(jsx => {
|
||||
if (jsx) {
|
||||
setSubCommandJSX(jsx)
|
||||
}
|
||||
}
|
||||
};
|
||||
$[15] = context;
|
||||
$[16] = handleCancel;
|
||||
$[17] = onDone;
|
||||
$[18] = t5;
|
||||
} else {
|
||||
t5 = $[18];
|
||||
})
|
||||
} else if (value === 'extra-usage') {
|
||||
logEvent('tengu_rate_limit_options_menu_select_extra_usage', {})
|
||||
void extraUsageCall(onDone, context).then(jsx => {
|
||||
if (jsx) {
|
||||
setSubCommandJSX(jsx)
|
||||
}
|
||||
})
|
||||
} else if (value === 'cancel') {
|
||||
handleCancel()
|
||||
}
|
||||
}
|
||||
const handleSelect = t5;
|
||||
|
||||
if (subCommandJSX) {
|
||||
return subCommandJSX;
|
||||
return subCommandJSX
|
||||
}
|
||||
let t6;
|
||||
if ($[19] !== handleSelect || $[20] !== options) {
|
||||
t6 = <Select options={options} onChange={handleSelect} visibleOptionCount={options.length} />;
|
||||
$[19] = handleSelect;
|
||||
$[20] = options;
|
||||
$[21] = t6;
|
||||
} else {
|
||||
t6 = $[21];
|
||||
}
|
||||
let t7;
|
||||
if ($[22] !== handleCancel || $[23] !== t6) {
|
||||
t7 = <Dialog title="What do you want to do?" onCancel={handleCancel} color="suggestion">{t6}</Dialog>;
|
||||
$[22] = handleCancel;
|
||||
$[23] = t6;
|
||||
$[24] = t7;
|
||||
} else {
|
||||
t7 = $[24];
|
||||
}
|
||||
return t7;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="What do you want to do?"
|
||||
onCancel={handleCancel}
|
||||
color="suggestion"
|
||||
>
|
||||
<Select<RateLimitOptionsMenuOptionType>
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
visibleOptionCount={options.length}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext & LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <RateLimitOptionsMenu onDone={onDone} context={context} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext & LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <RateLimitOptionsMenu onDone={onDone} context={context} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { RemoteEnvironmentDialog } from '../../components/RemoteEnvironmentDialog.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
return <RemoteEnvironmentDialog onDone={onDone} />;
|
||||
import * as React from 'react'
|
||||
import { RemoteEnvironmentDialog } from '../../components/RemoteEnvironmentDialog.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
return <RemoteEnvironmentDialog onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,163 +1,162 @@
|
||||
import { execa } from 'execa';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Select } from '../../components/CustomSelect/index.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { LoadingState } from '../../components/design-system/LoadingState.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { logEvent, type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString } from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { getGhAuthStatus } from '../../utils/github/ghAuthStatus.js';
|
||||
import { createDefaultEnvironment, getCodeWebUrl, type ImportTokenError, importGithubToken, isSignedIn, RedactedGithubToken } from './api.js';
|
||||
type CheckResult = {
|
||||
status: 'not_signed_in';
|
||||
} | {
|
||||
status: 'has_gh_token';
|
||||
token: RedactedGithubToken;
|
||||
} | {
|
||||
status: 'gh_not_installed';
|
||||
} | {
|
||||
status: 'gh_not_authenticated';
|
||||
};
|
||||
import { execa } from 'execa'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Select } from '../../components/CustomSelect/index.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { LoadingState } from '../../components/design-system/LoadingState.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import {
|
||||
logEvent,
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS as SafeString,
|
||||
} from '../../services/analytics/index.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import { getGhAuthStatus } from '../../utils/github/ghAuthStatus.js'
|
||||
import {
|
||||
createDefaultEnvironment,
|
||||
getCodeWebUrl,
|
||||
type ImportTokenError,
|
||||
importGithubToken,
|
||||
isSignedIn,
|
||||
RedactedGithubToken,
|
||||
} from './api.js'
|
||||
|
||||
type CheckResult =
|
||||
| { status: 'not_signed_in' }
|
||||
| { status: 'has_gh_token'; token: RedactedGithubToken }
|
||||
| { status: 'gh_not_installed' }
|
||||
| { status: 'gh_not_authenticated' }
|
||||
|
||||
async function checkLoginState(): Promise<CheckResult> {
|
||||
if (!(await isSignedIn())) {
|
||||
return {
|
||||
status: 'not_signed_in'
|
||||
};
|
||||
return { status: 'not_signed_in' }
|
||||
}
|
||||
const ghStatus = await getGhAuthStatus();
|
||||
|
||||
const ghStatus = await getGhAuthStatus()
|
||||
if (ghStatus === 'not_installed') {
|
||||
return {
|
||||
status: 'gh_not_installed'
|
||||
};
|
||||
return { status: 'gh_not_installed' }
|
||||
}
|
||||
if (ghStatus === 'not_authenticated') {
|
||||
return {
|
||||
status: 'gh_not_authenticated'
|
||||
};
|
||||
return { status: 'gh_not_authenticated' }
|
||||
}
|
||||
|
||||
// ghStatus === 'authenticated'. getGhAuthStatus spawns with stdout:'ignore'
|
||||
// (telemetry-safe); spawn once more with stdout:'pipe' to read the token.
|
||||
const {
|
||||
stdout
|
||||
} = await execa('gh', ['auth', 'token'], {
|
||||
const { stdout } = await execa('gh', ['auth', 'token'], {
|
||||
stdout: 'pipe',
|
||||
stderr: 'ignore',
|
||||
timeout: 5000,
|
||||
reject: false
|
||||
});
|
||||
const trimmed = stdout.trim();
|
||||
reject: false,
|
||||
})
|
||||
const trimmed = stdout.trim()
|
||||
if (!trimmed) {
|
||||
return {
|
||||
status: 'gh_not_authenticated'
|
||||
};
|
||||
return { status: 'gh_not_authenticated' }
|
||||
}
|
||||
return {
|
||||
status: 'has_gh_token',
|
||||
token: new RedactedGithubToken(trimmed)
|
||||
};
|
||||
return { status: 'has_gh_token', token: new RedactedGithubToken(trimmed) }
|
||||
}
|
||||
|
||||
function errorMessage(err: ImportTokenError, codeUrl: string): string {
|
||||
switch (err.kind) {
|
||||
case 'not_signed_in':
|
||||
return `Login failed. Please visit ${codeUrl} and login using the GitHub App`;
|
||||
return `Login failed. Please visit ${codeUrl} and login using the GitHub App`
|
||||
case 'invalid_token':
|
||||
return 'GitHub rejected that token. Run `gh auth login` and try again.';
|
||||
return 'GitHub rejected that token. Run `gh auth login` and try again.'
|
||||
case 'server':
|
||||
return `Server error (${err.status}). Try again in a moment.`;
|
||||
return `Server error (${err.status}). Try again in a moment.`
|
||||
case 'network':
|
||||
return "Couldn't reach the server. Check your connection.";
|
||||
return "Couldn't reach the server. Check your connection."
|
||||
}
|
||||
}
|
||||
type Step = {
|
||||
name: 'checking';
|
||||
} | {
|
||||
name: 'confirm';
|
||||
token: RedactedGithubToken;
|
||||
} | {
|
||||
name: 'uploading';
|
||||
};
|
||||
function Web({
|
||||
onDone
|
||||
}: {
|
||||
onDone: LocalJSXCommandOnDone;
|
||||
}) {
|
||||
const [step, setStep] = useState<Step>({
|
||||
name: 'checking'
|
||||
});
|
||||
|
||||
type Step =
|
||||
| { name: 'checking' }
|
||||
| { name: 'confirm'; token: RedactedGithubToken }
|
||||
| { name: 'uploading' }
|
||||
|
||||
function Web({ onDone }: { onDone: LocalJSXCommandOnDone }) {
|
||||
const [step, setStep] = useState<Step>({ name: 'checking' })
|
||||
|
||||
useEffect(() => {
|
||||
logEvent('tengu_remote_setup_started', {});
|
||||
logEvent('tengu_remote_setup_started', {})
|
||||
void checkLoginState().then(async result => {
|
||||
switch (result.status) {
|
||||
case 'not_signed_in':
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'not_signed_in' as SafeString
|
||||
});
|
||||
onDone('Not signed in to Claude. Run /login first.');
|
||||
return;
|
||||
result: 'not_signed_in' as SafeString,
|
||||
})
|
||||
onDone('Not signed in to Claude. Run /login first.')
|
||||
return
|
||||
case 'gh_not_installed':
|
||||
case 'gh_not_authenticated':
|
||||
{
|
||||
const url = `${getCodeWebUrl()}/onboarding?step=alt-auth`;
|
||||
await openBrowser(url);
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: result.status as SafeString
|
||||
});
|
||||
onDone(result.status === 'gh_not_installed' ? `GitHub CLI not found. Install it via https://cli.github.com/, then run \`gh auth login\`, or connect GitHub on the web: ${url}` : `GitHub CLI not authenticated. Run \`gh auth login\` and try again, or connect GitHub on the web: ${url}`);
|
||||
return;
|
||||
}
|
||||
case 'gh_not_authenticated': {
|
||||
const url = `${getCodeWebUrl()}/onboarding?step=alt-auth`
|
||||
await openBrowser(url)
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: result.status as SafeString,
|
||||
})
|
||||
onDone(
|
||||
result.status === 'gh_not_installed'
|
||||
? `GitHub CLI not found. Install it via https://cli.github.com/, then run \`gh auth login\`, or connect GitHub on the web: ${url}`
|
||||
: `GitHub CLI not authenticated. Run \`gh auth login\` and try again, or connect GitHub on the web: ${url}`,
|
||||
)
|
||||
return
|
||||
}
|
||||
case 'has_gh_token':
|
||||
setStep({
|
||||
name: 'confirm',
|
||||
token: result.token
|
||||
});
|
||||
setStep({ name: 'confirm', token: result.token })
|
||||
}
|
||||
});
|
||||
})
|
||||
// onDone is stable across renders; intentionally not in deps.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [])
|
||||
|
||||
const handleCancel = () => {
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'cancelled' as SafeString
|
||||
});
|
||||
onDone();
|
||||
};
|
||||
result: 'cancelled' as SafeString,
|
||||
})
|
||||
onDone()
|
||||
}
|
||||
|
||||
const handleConfirm = async (token: RedactedGithubToken) => {
|
||||
setStep({
|
||||
name: 'uploading'
|
||||
});
|
||||
const result = await importGithubToken(token);
|
||||
setStep({ name: 'uploading' })
|
||||
|
||||
const result = await importGithubToken(token)
|
||||
if (!result.ok) {
|
||||
const importErr = (result as { ok: false; error: ImportTokenError }).error;
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'import_failed' as SafeString,
|
||||
error_kind: importErr.kind as SafeString
|
||||
});
|
||||
onDone(errorMessage(importErr, getCodeWebUrl()));
|
||||
return;
|
||||
error_kind: result.error.kind as SafeString,
|
||||
})
|
||||
onDone(errorMessage(result.error, getCodeWebUrl()))
|
||||
return
|
||||
}
|
||||
|
||||
// Token import succeeded. Environment creation is best-effort — if it
|
||||
// fails, the web state machine routes to env-setup on landing, which is
|
||||
// one extra click but still better than the OAuth dance.
|
||||
await createDefaultEnvironment();
|
||||
const url = getCodeWebUrl();
|
||||
await openBrowser(url);
|
||||
await createDefaultEnvironment()
|
||||
|
||||
const url = getCodeWebUrl()
|
||||
await openBrowser(url)
|
||||
|
||||
logEvent('tengu_remote_setup_result', {
|
||||
result: 'success' as SafeString
|
||||
});
|
||||
onDone(`Connected as ${result.result.github_username}. Opened ${url}`);
|
||||
};
|
||||
result: 'success' as SafeString,
|
||||
})
|
||||
onDone(`Connected as ${result.result.github_username}. Opened ${url}`)
|
||||
}
|
||||
|
||||
if (step.name === 'checking') {
|
||||
return <LoadingState message="Checking login status…" />;
|
||||
return <LoadingState message="Checking login status…" />
|
||||
}
|
||||
|
||||
if (step.name === 'uploading') {
|
||||
return <LoadingState message="Connecting GitHub to Claude…" />;
|
||||
return <LoadingState message="Connecting GitHub to Claude…" />
|
||||
}
|
||||
const token = step.token;
|
||||
return <Dialog title="Connect Claude on the web to GitHub?" onCancel={handleCancel} hideInputGuide>
|
||||
|
||||
const token = step.token
|
||||
return (
|
||||
<Dialog
|
||||
title="Connect Claude on the web to GitHub?"
|
||||
onCancel={handleCancel}
|
||||
hideInputGuide
|
||||
>
|
||||
<Box flexDirection="column">
|
||||
<Text>
|
||||
Claude on the web requires connecting to your GitHub account to clone
|
||||
@@ -167,21 +166,26 @@ function Web({
|
||||
Your local credentials are used to authenticate with GitHub
|
||||
</Text>
|
||||
</Box>
|
||||
<Select options={[{
|
||||
label: 'Continue',
|
||||
value: 'send'
|
||||
}, {
|
||||
label: 'Cancel',
|
||||
value: 'cancel'
|
||||
}]} onChange={value => {
|
||||
if (value === 'send') {
|
||||
void handleConfirm(token);
|
||||
} else {
|
||||
handleCancel();
|
||||
}
|
||||
}} onCancel={handleCancel} />
|
||||
</Dialog>;
|
||||
<Select
|
||||
options={[
|
||||
{ label: 'Continue', value: 'send' },
|
||||
{ label: 'Cancel', value: 'cancel' },
|
||||
]}
|
||||
onChange={value => {
|
||||
if (value === 'send') {
|
||||
void handleConfirm(token)
|
||||
} else {
|
||||
handleCancel()
|
||||
}
|
||||
}}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone): Promise<React.ReactNode> {
|
||||
return <Web onDone={onDone} />;
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
): Promise<React.ReactNode> {
|
||||
return <Web onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,257 +1,300 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import type { UUID } from 'crypto';
|
||||
import figures from 'figures';
|
||||
import * as React from 'react';
|
||||
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js';
|
||||
import type { CommandResultDisplay, ResumeEntrypoint } from '../../commands.js';
|
||||
import { LogSelector } from '../../components/LogSelector.js';
|
||||
import { MessageResponse } from '../../components/MessageResponse.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import { useIsInsideModal } from '../../context/modalContext.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { setClipboard } from '../../ink/termio/osc.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import type { LogOption } from '../../types/logs.js';
|
||||
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js';
|
||||
import { checkCrossProjectResume } from '../../utils/crossProjectResume.js';
|
||||
import { getWorktreePaths } from '../../utils/getWorktreePaths.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { getLastSessionLog, getSessionIdFromLog, isCustomTitleEnabled, isLiteLog, loadAllProjectsMessageLogs, loadFullLog, loadSameRepoMessageLogs, searchSessionsByCustomTitle } from '../../utils/sessionStorage.js';
|
||||
import { validateUuid } from '../../utils/uuid.js';
|
||||
type ResumeResult = {
|
||||
resultType: 'sessionNotFound';
|
||||
arg: string;
|
||||
} | {
|
||||
resultType: 'multipleMatches';
|
||||
arg: string;
|
||||
count: number;
|
||||
};
|
||||
import chalk from 'chalk'
|
||||
import type { UUID } from 'crypto'
|
||||
import figures from 'figures'
|
||||
import * as React from 'react'
|
||||
import { getOriginalCwd, getSessionId } from '../../bootstrap/state.js'
|
||||
import type { CommandResultDisplay, ResumeEntrypoint } from '../../commands.js'
|
||||
import { LogSelector } from '../../components/LogSelector.js'
|
||||
import { MessageResponse } from '../../components/MessageResponse.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import { useIsInsideModal } from '../../context/modalContext.js'
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
|
||||
import { setClipboard } from '../../ink/termio/osc.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import type { LogOption } from '../../types/logs.js'
|
||||
import { agenticSessionSearch } from '../../utils/agenticSessionSearch.js'
|
||||
import { checkCrossProjectResume } from '../../utils/crossProjectResume.js'
|
||||
import { getWorktreePaths } from '../../utils/getWorktreePaths.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import {
|
||||
getLastSessionLog,
|
||||
getSessionIdFromLog,
|
||||
isCustomTitleEnabled,
|
||||
isLiteLog,
|
||||
loadAllProjectsMessageLogs,
|
||||
loadFullLog,
|
||||
loadSameRepoMessageLogs,
|
||||
searchSessionsByCustomTitle,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
import { validateUuid } from '../../utils/uuid.js'
|
||||
|
||||
type ResumeResult =
|
||||
| { resultType: 'sessionNotFound'; arg: string }
|
||||
| { resultType: 'multipleMatches'; arg: string; count: number }
|
||||
|
||||
function resumeHelpMessage(result: ResumeResult): string {
|
||||
switch (result.resultType) {
|
||||
case 'sessionNotFound':
|
||||
return `Session ${chalk.bold(result.arg)} was not found.`;
|
||||
return `Session ${chalk.bold(result.arg)} was not found.`
|
||||
case 'multipleMatches':
|
||||
return `Found ${result.count} sessions matching ${chalk.bold(result.arg)}. Please use /resume to pick a specific session.`;
|
||||
return `Found ${result.count} sessions matching ${chalk.bold(result.arg)}. Please use /resume to pick a specific session.`
|
||||
}
|
||||
}
|
||||
function ResumeError(t0) {
|
||||
const $ = _c(10);
|
||||
const {
|
||||
message,
|
||||
args,
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = () => {
|
||||
const timer = setTimeout(onDone, 0);
|
||||
return () => clearTimeout(timer);
|
||||
};
|
||||
t2 = [onDone];
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] !== args) {
|
||||
t3 = <Text dimColor={true}>{figures.pointer} /resume {args}</Text>;
|
||||
$[3] = args;
|
||||
$[4] = t3;
|
||||
} else {
|
||||
t3 = $[4];
|
||||
}
|
||||
let t4;
|
||||
if ($[5] !== message) {
|
||||
t4 = <MessageResponse><Text>{message}</Text></MessageResponse>;
|
||||
$[5] = message;
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
let t5;
|
||||
if ($[7] !== t3 || $[8] !== t4) {
|
||||
t5 = <Box flexDirection="column">{t3}{t4}</Box>;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
$[9] = t5;
|
||||
} else {
|
||||
t5 = $[9];
|
||||
}
|
||||
return t5;
|
||||
|
||||
function ResumeError({
|
||||
message,
|
||||
args,
|
||||
onDone,
|
||||
}: {
|
||||
message: string
|
||||
args: string
|
||||
onDone: () => void
|
||||
}): React.ReactNode {
|
||||
React.useEffect(() => {
|
||||
const timer = setTimeout(onDone, 0)
|
||||
return () => clearTimeout(timer)
|
||||
}, [onDone])
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>
|
||||
{figures.pointer} /resume {args}
|
||||
</Text>
|
||||
<MessageResponse>
|
||||
<Text>{message}</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
function ResumeCommand({
|
||||
onDone,
|
||||
onResume
|
||||
onResume,
|
||||
}: {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
onResume: (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => Promise<void>;
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
onResume: (
|
||||
sessionId: UUID,
|
||||
log: LogOption,
|
||||
entrypoint: ResumeEntrypoint,
|
||||
) => Promise<void>
|
||||
}): React.ReactNode {
|
||||
const [logs, setLogs] = React.useState<LogOption[]>([]);
|
||||
const [worktreePaths, setWorktreePaths] = React.useState<string[]>([]);
|
||||
const [loading, setLoading] = React.useState(true);
|
||||
const [resuming, setResuming] = React.useState(false);
|
||||
const [showAllProjects, setShowAllProjects] = React.useState(false);
|
||||
const {
|
||||
rows
|
||||
} = useTerminalSize();
|
||||
const insideModal = useIsInsideModal();
|
||||
const loadLogs = React.useCallback(async (allProjects: boolean, paths: string[]) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const allLogs = allProjects ? await loadAllProjectsMessageLogs() : await loadSameRepoMessageLogs(paths);
|
||||
const resumable = filterResumableSessions(allLogs, getSessionId());
|
||||
if (resumable.length === 0) {
|
||||
onDone('No conversations found to resume');
|
||||
return;
|
||||
const [logs, setLogs] = React.useState<LogOption[]>([])
|
||||
const [worktreePaths, setWorktreePaths] = React.useState<string[]>([])
|
||||
const [loading, setLoading] = React.useState(true)
|
||||
const [resuming, setResuming] = React.useState(false)
|
||||
const [showAllProjects, setShowAllProjects] = React.useState(false)
|
||||
const { rows } = useTerminalSize()
|
||||
const insideModal = useIsInsideModal()
|
||||
|
||||
const loadLogs = React.useCallback(
|
||||
async (allProjects: boolean, paths: string[]) => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const allLogs = allProjects
|
||||
? await loadAllProjectsMessageLogs()
|
||||
: await loadSameRepoMessageLogs(paths)
|
||||
const resumable = filterResumableSessions(allLogs, getSessionId())
|
||||
if (resumable.length === 0) {
|
||||
onDone('No conversations found to resume')
|
||||
return
|
||||
}
|
||||
setLogs(resumable)
|
||||
} catch (_err) {
|
||||
onDone('Failed to load conversations')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
setLogs(resumable);
|
||||
} catch (_err) {
|
||||
onDone('Failed to load conversations');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [onDone]);
|
||||
},
|
||||
[onDone],
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
async function init() {
|
||||
const paths_0 = await getWorktreePaths(getOriginalCwd());
|
||||
setWorktreePaths(paths_0);
|
||||
void loadLogs(false, paths_0);
|
||||
const paths = await getWorktreePaths(getOriginalCwd())
|
||||
setWorktreePaths(paths)
|
||||
void loadLogs(false, paths)
|
||||
}
|
||||
void init();
|
||||
}, [loadLogs]);
|
||||
void init()
|
||||
}, [loadLogs])
|
||||
|
||||
const handleToggleAllProjects = React.useCallback(() => {
|
||||
const newValue = !showAllProjects;
|
||||
setShowAllProjects(newValue);
|
||||
void loadLogs(newValue, worktreePaths);
|
||||
}, [showAllProjects, loadLogs, worktreePaths]);
|
||||
const newValue = !showAllProjects
|
||||
setShowAllProjects(newValue)
|
||||
void loadLogs(newValue, worktreePaths)
|
||||
}, [showAllProjects, loadLogs, worktreePaths])
|
||||
|
||||
async function handleSelect(log: LogOption) {
|
||||
const sessionId = validateUuid(getSessionIdFromLog(log));
|
||||
const sessionId = validateUuid(getSessionIdFromLog(log))
|
||||
if (!sessionId) {
|
||||
onDone('Failed to resume conversation');
|
||||
return;
|
||||
onDone('Failed to resume conversation')
|
||||
return
|
||||
}
|
||||
|
||||
// Load full messages for lite logs
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
|
||||
// Check if this conversation is from a different directory
|
||||
const crossProjectCheck = checkCrossProjectResume(fullLog, showAllProjects, worktreePaths);
|
||||
const crossProjectCheck = checkCrossProjectResume(
|
||||
fullLog,
|
||||
showAllProjects,
|
||||
worktreePaths,
|
||||
)
|
||||
if (crossProjectCheck.isCrossProject) {
|
||||
if (crossProjectCheck.isSameRepoWorktree) {
|
||||
// Same repo worktree - can resume directly
|
||||
setResuming(true);
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker');
|
||||
return;
|
||||
setResuming(true)
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker')
|
||||
return
|
||||
}
|
||||
|
||||
// Different project - show command instead of resuming
|
||||
const crossCmd = (crossProjectCheck as { isCrossProject: true; isSameRepoWorktree: false; command: string }).command;
|
||||
const raw = await setClipboard(crossCmd);
|
||||
if (raw) process.stdout.write(raw);
|
||||
const raw = await setClipboard(crossProjectCheck.command)
|
||||
if (raw) process.stdout.write(raw)
|
||||
|
||||
// Format the output message
|
||||
const message = ['', 'This conversation is from a different directory.', '', 'To resume, run:', ` ${crossCmd}`, '', '(Command copied to clipboard)', ''].join('\n');
|
||||
onDone(message, {
|
||||
display: 'user'
|
||||
});
|
||||
return;
|
||||
const message = [
|
||||
'',
|
||||
'This conversation is from a different directory.',
|
||||
'',
|
||||
'To resume, run:',
|
||||
` ${crossProjectCheck.command}`,
|
||||
'',
|
||||
'(Command copied to clipboard)',
|
||||
'',
|
||||
].join('\n')
|
||||
|
||||
onDone(message, { display: 'user' })
|
||||
return
|
||||
}
|
||||
|
||||
// Same directory - proceed with resume
|
||||
setResuming(true);
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker');
|
||||
setResuming(true)
|
||||
void onResume(sessionId, fullLog, 'slash_command_picker')
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
onDone('Resume cancelled', {
|
||||
display: 'system'
|
||||
});
|
||||
onDone('Resume cancelled', { display: 'system' })
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> Loading conversations…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (resuming) {
|
||||
return <Box>
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text> Resuming conversation…</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return <LogSelector logs={logs} maxHeight={insideModal ? Math.floor(rows / 2) : rows - 2} onCancel={handleCancel} onSelect={handleSelect} onLogsChanged={() => loadLogs(showAllProjects, worktreePaths)} showAllProjects={showAllProjects} onToggleAllProjects={handleToggleAllProjects} onAgenticSearch={agenticSessionSearch} />;
|
||||
|
||||
return (
|
||||
<LogSelector
|
||||
logs={logs}
|
||||
maxHeight={insideModal ? Math.floor(rows / 2) : rows - 2}
|
||||
onCancel={handleCancel}
|
||||
onSelect={handleSelect}
|
||||
onLogsChanged={() => loadLogs(showAllProjects, worktreePaths)}
|
||||
showAllProjects={showAllProjects}
|
||||
onToggleAllProjects={handleToggleAllProjects}
|
||||
onAgenticSearch={agenticSessionSearch}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export function filterResumableSessions(logs: LogOption[], currentSessionId: string): LogOption[] {
|
||||
return logs.filter(l => !l.isSidechain && getSessionIdFromLog(l) !== currentSessionId);
|
||||
|
||||
export function filterResumableSessions(
|
||||
logs: LogOption[],
|
||||
currentSessionId: string,
|
||||
): LogOption[] {
|
||||
return logs.filter(
|
||||
l => !l.isSidechain && getSessionIdFromLog(l) !== currentSessionId,
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const onResume = async (sessionId: UUID, log: LogOption, entrypoint: ResumeEntrypoint) => {
|
||||
const onResume = async (
|
||||
sessionId: UUID,
|
||||
log: LogOption,
|
||||
entrypoint: ResumeEntrypoint,
|
||||
) => {
|
||||
try {
|
||||
await context.resume?.(sessionId, log, entrypoint);
|
||||
onDone(undefined, {
|
||||
display: 'skip'
|
||||
});
|
||||
await context.resume?.(sessionId, log, entrypoint)
|
||||
onDone(undefined, { display: 'skip' })
|
||||
} catch (error) {
|
||||
logError(error as Error);
|
||||
onDone(`Failed to resume: ${(error as Error).message}`);
|
||||
logError(error as Error)
|
||||
onDone(`Failed to resume: ${(error as Error).message}`)
|
||||
}
|
||||
};
|
||||
const arg = args?.trim();
|
||||
}
|
||||
|
||||
const arg = args?.trim()
|
||||
|
||||
// No argument provided - show picker
|
||||
if (!arg) {
|
||||
return <ResumeCommand key={Date.now()} onDone={onDone} onResume={onResume} />;
|
||||
return (
|
||||
<ResumeCommand key={Date.now()} onDone={onDone} onResume={onResume} />
|
||||
)
|
||||
}
|
||||
|
||||
// Load logs to search (includes same-repo worktrees)
|
||||
const worktreePaths = await getWorktreePaths(getOriginalCwd());
|
||||
const logs = await loadSameRepoMessageLogs(worktreePaths);
|
||||
const worktreePaths = await getWorktreePaths(getOriginalCwd())
|
||||
const logs = await loadSameRepoMessageLogs(worktreePaths)
|
||||
if (logs.length === 0) {
|
||||
const message = 'No conversations found to resume.';
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
const message = 'No conversations found to resume.'
|
||||
return (
|
||||
<ResumeError
|
||||
message={message}
|
||||
args={arg}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// First, check if arg is a valid UUID
|
||||
const maybeSessionId = validateUuid(arg);
|
||||
const maybeSessionId = validateUuid(arg)
|
||||
if (maybeSessionId) {
|
||||
const matchingLogs = logs.filter(l => getSessionIdFromLog(l) === maybeSessionId).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
||||
const matchingLogs = logs
|
||||
.filter(l => getSessionIdFromLog(l) === maybeSessionId)
|
||||
.sort((a, b) => b.modified.getTime() - a.modified.getTime())
|
||||
|
||||
if (matchingLogs.length > 0) {
|
||||
const log = matchingLogs[0]!;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
void onResume(maybeSessionId, fullLog, 'slash_command_session_id');
|
||||
return null;
|
||||
const log = matchingLogs[0]!
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
void onResume(maybeSessionId, fullLog, 'slash_command_session_id')
|
||||
return null
|
||||
}
|
||||
|
||||
// Enriched logs didn't find it — try direct file lookup. This handles
|
||||
// sessions filtered out by enrichLogs (e.g., first message >16KB makes
|
||||
// firstPrompt extraction fail, causing the session to be dropped).
|
||||
const directLog = await getLastSessionLog(maybeSessionId);
|
||||
const directLog = await getLastSessionLog(maybeSessionId)
|
||||
if (directLog) {
|
||||
void onResume(maybeSessionId, directLog, 'slash_command_session_id');
|
||||
return null;
|
||||
void onResume(maybeSessionId, directLog, 'slash_command_session_id')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Next, try exact custom title match (only if feature is enabled)
|
||||
if (isCustomTitleEnabled()) {
|
||||
const titleMatches = await searchSessionsByCustomTitle(arg, {
|
||||
exact: true
|
||||
});
|
||||
exact: true,
|
||||
})
|
||||
if (titleMatches.length === 1) {
|
||||
const log = titleMatches[0]!;
|
||||
const sessionId = getSessionIdFromLog(log);
|
||||
const log = titleMatches[0]!
|
||||
const sessionId = getSessionIdFromLog(log)
|
||||
if (sessionId) {
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log;
|
||||
void onResume(sessionId, fullLog, 'slash_command_title');
|
||||
return null;
|
||||
const fullLog = isLiteLog(log) ? await loadFullLog(log) : log
|
||||
void onResume(sessionId, fullLog, 'slash_command_title')
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,16 +303,21 @@ export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const message = resumeHelpMessage({
|
||||
resultType: 'multipleMatches',
|
||||
arg,
|
||||
count: titleMatches.length
|
||||
});
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
count: titleMatches.length,
|
||||
})
|
||||
return (
|
||||
<ResumeError
|
||||
message={message}
|
||||
args={arg}
|
||||
onDone={() => onDone(message)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// No match found - show error
|
||||
const message = resumeHelpMessage({
|
||||
resultType: 'sessionNotFound',
|
||||
arg
|
||||
});
|
||||
return <ResumeError message={message} args={arg} onDone={() => onDone(message)} />;
|
||||
};
|
||||
const message = resumeHelpMessage({ resultType: 'sessionNotFound', arg })
|
||||
return (
|
||||
<ResumeError message={message} args={arg} onDone={() => onDone(message)} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,95 +1,71 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import React, { useCallback, useRef, useState } from 'react'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
|
||||
type Props = {
|
||||
onProceed: (signal: AbortSignal) => Promise<void>;
|
||||
onCancel: () => void;
|
||||
};
|
||||
export function UltrareviewOverageDialog(t0) {
|
||||
const $ = _c(15);
|
||||
const {
|
||||
onProceed,
|
||||
onCancel
|
||||
} = t0;
|
||||
const [isLaunching, setIsLaunching] = useState(false);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = new AbortController();
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
}
|
||||
const abortControllerRef = useRef(t1);
|
||||
let t2;
|
||||
if ($[1] !== onCancel || $[2] !== onProceed) {
|
||||
t2 = value => {
|
||||
if (value === "proceed") {
|
||||
setIsLaunching(true);
|
||||
onProceed(abortControllerRef.current.signal).catch(() => setIsLaunching(false));
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
};
|
||||
$[1] = onCancel;
|
||||
$[2] = onProceed;
|
||||
$[3] = t2;
|
||||
} else {
|
||||
t2 = $[3];
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[4] !== onCancel) {
|
||||
t3 = () => {
|
||||
abortControllerRef.current.abort();
|
||||
onCancel();
|
||||
};
|
||||
$[4] = onCancel;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t3 = $[5];
|
||||
}
|
||||
const handleCancel = t3;
|
||||
let t4;
|
||||
if ($[6] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [{
|
||||
label: "Proceed with Extra Usage billing",
|
||||
value: "proceed"
|
||||
}, {
|
||||
label: "Cancel",
|
||||
value: "cancel"
|
||||
}];
|
||||
$[6] = t4;
|
||||
} else {
|
||||
t4 = $[6];
|
||||
}
|
||||
const options = t4;
|
||||
let t5;
|
||||
if ($[7] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t5 = <Text>Your free ultrareviews for this organization are used. Further reviews bill as Extra Usage (pay-per-use).</Text>;
|
||||
$[7] = t5;
|
||||
} else {
|
||||
t5 = $[7];
|
||||
}
|
||||
let t6;
|
||||
if ($[8] !== handleCancel || $[9] !== handleSelect || $[10] !== isLaunching) {
|
||||
t6 = <Box flexDirection="column" gap={1}>{t5}{isLaunching ? <Text color="background">Launching…</Text> : <Select options={options} onChange={handleSelect} onCancel={handleCancel} />}</Box>;
|
||||
$[8] = handleCancel;
|
||||
$[9] = handleSelect;
|
||||
$[10] = isLaunching;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t6 = $[11];
|
||||
}
|
||||
let t7;
|
||||
if ($[12] !== handleCancel || $[13] !== t6) {
|
||||
t7 = <Dialog title="Ultrareview billing" onCancel={handleCancel} color="background">{t6}</Dialog>;
|
||||
$[12] = handleCancel;
|
||||
$[13] = t6;
|
||||
$[14] = t7;
|
||||
} else {
|
||||
t7 = $[14];
|
||||
}
|
||||
return t7;
|
||||
onProceed: (signal: AbortSignal) => Promise<void>
|
||||
onCancel: () => void
|
||||
}
|
||||
|
||||
export function UltrareviewOverageDialog({
|
||||
onProceed,
|
||||
onCancel,
|
||||
}: Props): React.ReactNode {
|
||||
const [isLaunching, setIsLaunching] = useState(false)
|
||||
const abortControllerRef = useRef(new AbortController())
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(value: string) => {
|
||||
if (value === 'proceed') {
|
||||
setIsLaunching(true)
|
||||
// If onProceed rejects (e.g. launchRemoteReview throws), onDone is
|
||||
// never called and the dialog stays mounted — restore the Select so
|
||||
// the user can retry or cancel instead of staring at "Launching…".
|
||||
void onProceed(abortControllerRef.current.signal).catch(() =>
|
||||
setIsLaunching(false),
|
||||
)
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
},
|
||||
[onProceed, onCancel],
|
||||
)
|
||||
|
||||
// Escape during launch aborts the in-flight onProceed via signal so the
|
||||
// caller can skip side effects (confirmOverage, onDone) — otherwise a
|
||||
// fire-and-forget launch would keep running and bill despite "cancelled".
|
||||
const handleCancel = useCallback(() => {
|
||||
abortControllerRef.current.abort()
|
||||
onCancel()
|
||||
}, [onCancel])
|
||||
|
||||
const options = [
|
||||
{ label: 'Proceed with Extra Usage billing', value: 'proceed' },
|
||||
{ label: 'Cancel', value: 'cancel' },
|
||||
]
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Ultrareview billing"
|
||||
onCancel={handleCancel}
|
||||
color="background"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>
|
||||
Your free ultrareviews for this organization are used. Further reviews
|
||||
bill as Extra Usage (pay-per-use).
|
||||
</Text>
|
||||
{isLaunching ? (
|
||||
<Text color="background">Launching…</Text>
|
||||
) : (
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
onCancel={handleCancel}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,57 +1,89 @@
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js';
|
||||
import React from 'react';
|
||||
import type { LocalJSXCommandCall, LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { checkOverageGate, confirmOverage, launchRemoteReview } from './reviewRemote.js';
|
||||
import { UltrareviewOverageDialog } from './UltrareviewOverageDialog.js';
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/messages.js'
|
||||
import React from 'react'
|
||||
import type {
|
||||
LocalJSXCommandCall,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
import {
|
||||
checkOverageGate,
|
||||
confirmOverage,
|
||||
launchRemoteReview,
|
||||
} from './reviewRemote.js'
|
||||
import { UltrareviewOverageDialog } from './UltrareviewOverageDialog.js'
|
||||
|
||||
function contentBlocksToString(blocks: ContentBlockParam[]): string {
|
||||
return blocks.map(b => b.type === 'text' ? b.text : '').filter(Boolean).join('\n');
|
||||
return blocks
|
||||
.map(b => (b.type === 'text' ? b.text : ''))
|
||||
.filter(Boolean)
|
||||
.join('\n')
|
||||
}
|
||||
async function launchAndDone(args: string, context: Parameters<LocalJSXCommandCall>[1], onDone: LocalJSXCommandOnDone, billingNote: string, signal?: AbortSignal): Promise<void> {
|
||||
const result = await launchRemoteReview(args, context, billingNote);
|
||||
|
||||
async function launchAndDone(
|
||||
args: string,
|
||||
context: Parameters<LocalJSXCommandCall>[1],
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
billingNote: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<void> {
|
||||
const result = await launchRemoteReview(args, context, billingNote)
|
||||
// User hit Escape during the ~5s launch — the dialog already showed
|
||||
// "cancelled" and unmounted, so skip onDone (would write to a dead
|
||||
// transcript slot) and let the caller skip confirmOverage.
|
||||
if (signal?.aborted) return;
|
||||
if (signal?.aborted) return
|
||||
if (result) {
|
||||
onDone(contentBlocksToString(result), {
|
||||
shouldQuery: true
|
||||
});
|
||||
onDone(contentBlocksToString(result), { shouldQuery: true })
|
||||
} else {
|
||||
// Precondition failures now return specific ContentBlockParam[] above.
|
||||
// null only reaches here on teleport failure (PR mode) or non-github
|
||||
// repo — both are CCR/repo connectivity issues.
|
||||
onDone('Ultrareview failed to launch the remote session. Check that this is a GitHub repo and try again.', {
|
||||
display: 'system'
|
||||
});
|
||||
onDone(
|
||||
'Ultrareview failed to launch the remote session. Check that this is a GitHub repo and try again.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const gate = await checkOverageGate();
|
||||
const gate = await checkOverageGate()
|
||||
|
||||
if (gate.kind === 'not-enabled') {
|
||||
onDone('Free ultrareviews used. Enable Extra Usage at https://claude.ai/settings/billing to continue.', {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
onDone(
|
||||
'Free ultrareviews used. Enable Extra Usage at https://claude.ai/settings/billing to continue.',
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (gate.kind === 'low-balance') {
|
||||
onDone(`Balance too low to launch ultrareview ($${gate.available.toFixed(2)} available, $10 minimum). Top up at https://claude.ai/settings/billing`, {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
onDone(
|
||||
`Balance too low to launch ultrareview ($${gate.available.toFixed(2)} available, $10 minimum). Top up at https://claude.ai/settings/billing`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
if (gate.kind === 'needs-confirm') {
|
||||
return <UltrareviewOverageDialog onProceed={async signal => {
|
||||
await launchAndDone(args, context, onDone, ' This review bills as Extra Usage.', signal);
|
||||
// Only persist the confirmation flag after a non-aborted launch —
|
||||
// otherwise Escape-during-launch would leave the flag set and
|
||||
// skip this dialog on the next attempt.
|
||||
if (!signal.aborted) confirmOverage();
|
||||
}} onCancel={() => onDone('Ultrareview cancelled.', {
|
||||
display: 'system'
|
||||
})} />;
|
||||
return (
|
||||
<UltrareviewOverageDialog
|
||||
onProceed={async signal => {
|
||||
await launchAndDone(
|
||||
args,
|
||||
context,
|
||||
onDone,
|
||||
' This review bills as Extra Usage.',
|
||||
signal,
|
||||
)
|
||||
// Only persist the confirmation flag after a non-aborted launch —
|
||||
// otherwise Escape-during-launch would leave the flag set and
|
||||
// skip this dialog on the next attempt.
|
||||
if (!signal.aborted) confirmOverage()
|
||||
}}
|
||||
onCancel={() => onDone('Ultrareview cancelled.', { display: 'system' })}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// gate.kind === 'proceed'
|
||||
await launchAndDone(args, context, onDone, gate.billingNote);
|
||||
return null;
|
||||
};
|
||||
await launchAndDone(args, context, onDone, gate.billingNote)
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,82 +1,127 @@
|
||||
import { relative } from 'path';
|
||||
import React from 'react';
|
||||
import { getCwdState } from '../../bootstrap/state.js';
|
||||
import { SandboxSettings } from '../../components/sandbox/SandboxSettings.js';
|
||||
import { color } from '../../ink.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { addToExcludedCommands, SandboxManager } from '../../utils/sandbox/sandbox-adapter.js';
|
||||
import { getSettings_DEPRECATED, getSettingsFilePathForSource } from '../../utils/settings/settings.js';
|
||||
import type { ThemeName } from '../../utils/theme.js';
|
||||
export async function call(onDone: (result?: string) => void, _context: unknown, args?: string): Promise<React.ReactNode | null> {
|
||||
const settings = getSettings_DEPRECATED();
|
||||
const themeName: ThemeName = settings.theme as ThemeName || 'light';
|
||||
const platform = getPlatform();
|
||||
import { relative } from 'path'
|
||||
import React from 'react'
|
||||
import { getCwdState } from '../../bootstrap/state.js'
|
||||
import { SandboxSettings } from '../../components/sandbox/SandboxSettings.js'
|
||||
import { color } from '../../ink.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import {
|
||||
addToExcludedCommands,
|
||||
SandboxManager,
|
||||
} from '../../utils/sandbox/sandbox-adapter.js'
|
||||
import {
|
||||
getSettings_DEPRECATED,
|
||||
getSettingsFilePathForSource,
|
||||
} from '../../utils/settings/settings.js'
|
||||
import type { ThemeName } from '../../utils/theme.js'
|
||||
|
||||
export async function call(
|
||||
onDone: (result?: string) => void,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode | null> {
|
||||
const settings = getSettings_DEPRECATED()
|
||||
const themeName: ThemeName = (settings.theme as ThemeName) || 'light'
|
||||
|
||||
const platform = getPlatform()
|
||||
|
||||
if (!SandboxManager.isSupportedPlatform()) {
|
||||
// WSL1 users will see this since isSupportedPlatform returns false for WSL1
|
||||
const errorMessage = platform === 'wsl' ? 'Error: Sandboxing requires WSL2. WSL1 is not supported.' : 'Error: Sandboxing is currently only supported on macOS, Linux, and WSL2.';
|
||||
const message = color('error', themeName)(errorMessage);
|
||||
onDone(message);
|
||||
return null;
|
||||
const errorMessage =
|
||||
platform === 'wsl'
|
||||
? 'Error: Sandboxing requires WSL2. WSL1 is not supported.'
|
||||
: 'Error: Sandboxing is currently only supported on macOS, Linux, and WSL2.'
|
||||
const message = color('error', themeName)(errorMessage)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Check dependencies - get structured result with errors/warnings
|
||||
const depCheck = SandboxManager.checkDependencies();
|
||||
const depCheck = SandboxManager.checkDependencies()
|
||||
|
||||
// Check if platform is in enabledPlatforms list (undocumented enterprise setting)
|
||||
if (!SandboxManager.isPlatformInEnabledList()) {
|
||||
const message = color('error', themeName)(`Error: Sandboxing is disabled for this platform (${platform}) via the enabledPlatforms setting.`);
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
`Error: Sandboxing is disabled for this platform (${platform}) via the enabledPlatforms setting.`,
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if sandbox settings are locked by higher-priority settings
|
||||
if (SandboxManager.areSandboxSettingsLockedByPolicy()) {
|
||||
const message = color('error', themeName)('Error: Sandbox settings are overridden by a higher-priority configuration and cannot be changed locally.');
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
'Error: Sandbox settings are overridden by a higher-priority configuration and cannot be changed locally.',
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Parse the arguments
|
||||
const trimmedArgs = args?.trim() || '';
|
||||
const trimmedArgs = args?.trim() || ''
|
||||
|
||||
// If no args, show the interactive menu
|
||||
if (!trimmedArgs) {
|
||||
return <SandboxSettings onComplete={onDone} depCheck={depCheck} />;
|
||||
return <SandboxSettings onComplete={onDone} depCheck={depCheck} />
|
||||
}
|
||||
|
||||
// Handle subcommands
|
||||
if (trimmedArgs) {
|
||||
const parts = trimmedArgs.split(' ');
|
||||
const subcommand = parts[0];
|
||||
const parts = trimmedArgs.split(' ')
|
||||
const subcommand = parts[0]
|
||||
|
||||
if (subcommand === 'exclude') {
|
||||
// Handle exclude subcommand
|
||||
const commandPattern = trimmedArgs.slice('exclude '.length).trim();
|
||||
const commandPattern = trimmedArgs.slice('exclude '.length).trim()
|
||||
|
||||
if (!commandPattern) {
|
||||
const message = color('error', themeName)('Error: Please provide a command pattern to exclude (e.g., /sandbox exclude "npm run test:*")');
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
'Error: Please provide a command pattern to exclude (e.g., /sandbox exclude "npm run test:*")',
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Remove quotes if present
|
||||
const cleanPattern = commandPattern.replace(/^["']|["']$/g, '');
|
||||
const cleanPattern = commandPattern.replace(/^["']|["']$/g, '')
|
||||
|
||||
// Add to excludedCommands
|
||||
addToExcludedCommands(cleanPattern);
|
||||
addToExcludedCommands(cleanPattern)
|
||||
|
||||
// Get the local settings path and make it relative to cwd
|
||||
const localSettingsPath = getSettingsFilePathForSource('localSettings');
|
||||
const relativePath = localSettingsPath ? relative(getCwdState(), localSettingsPath) : '.claude/settings.local.json';
|
||||
const message = color('success', themeName)(`Added "${cleanPattern}" to excluded commands in ${relativePath}`);
|
||||
onDone(message);
|
||||
return null;
|
||||
const localSettingsPath = getSettingsFilePathForSource('localSettings')
|
||||
const relativePath = localSettingsPath
|
||||
? relative(getCwdState(), localSettingsPath)
|
||||
: '.claude/settings.local.json'
|
||||
|
||||
const message = color(
|
||||
'success',
|
||||
themeName,
|
||||
)(`Added "${cleanPattern}" to excluded commands in ${relativePath}`)
|
||||
|
||||
onDone(message)
|
||||
return null
|
||||
} else {
|
||||
// Unknown subcommand
|
||||
const message = color('error', themeName)(`Error: Unknown subcommand "${subcommand}". Available subcommand: exclude`);
|
||||
onDone(message);
|
||||
return null;
|
||||
const message = color(
|
||||
'error',
|
||||
themeName,
|
||||
)(
|
||||
`Error: Unknown subcommand "${subcommand}". Available subcommand: exclude`,
|
||||
)
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// Should never reach here since we handle all cases above
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,139 +1,83 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { toString as qrToString } from 'qrcode';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js';
|
||||
import { useAppState } from '../../state/AppState.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { toString as qrToString } from 'qrcode'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { useKeybinding } from '../../keybindings/useKeybinding.js'
|
||||
import { useAppState } from '../../state/AppState.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
|
||||
type Props = {
|
||||
onDone: () => void;
|
||||
};
|
||||
function SessionInfo(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const remoteSessionUrl = useAppState(_temp);
|
||||
const [qrCode, setQrCode] = useState("");
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== remoteSessionUrl) {
|
||||
t1 = () => {
|
||||
if (!remoteSessionUrl) {
|
||||
return;
|
||||
}
|
||||
const url = remoteSessionUrl;
|
||||
const generateQRCode = async function generateQRCode() {
|
||||
const qr = await qrToString(url, {
|
||||
type: "utf8",
|
||||
errorCorrectionLevel: "L"
|
||||
});
|
||||
setQrCode(qr);
|
||||
};
|
||||
generateQRCode().catch(_temp2);
|
||||
};
|
||||
t2 = [remoteSessionUrl];
|
||||
$[0] = remoteSessionUrl;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
useEffect(t1, t2);
|
||||
let t3;
|
||||
if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t3 = {
|
||||
context: "Confirmation"
|
||||
};
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
useKeybinding("confirm:no", onDone, t3);
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
function SessionInfo({ onDone }: Props): React.ReactNode {
|
||||
const remoteSessionUrl = useAppState(s => s.remoteSessionUrl)
|
||||
const [qrCode, setQrCode] = useState<string>('')
|
||||
|
||||
// Generate QR code when URL is available
|
||||
useEffect(() => {
|
||||
if (!remoteSessionUrl) return
|
||||
|
||||
const url = remoteSessionUrl
|
||||
async function generateQRCode(): Promise<void> {
|
||||
const qr = await qrToString(url, {
|
||||
type: 'utf8',
|
||||
errorCorrectionLevel: 'L',
|
||||
})
|
||||
setQrCode(qr)
|
||||
}
|
||||
// Intentionally silent fail - URL is still shown so QR is non-critical
|
||||
generateQRCode().catch(e => {
|
||||
logForDebugging('QR code generation failed', e)
|
||||
})
|
||||
}, [remoteSessionUrl])
|
||||
|
||||
// Handle ESC to dismiss
|
||||
useKeybinding('confirm:no', onDone, { context: 'Confirmation' })
|
||||
|
||||
// Not in remote mode
|
||||
if (!remoteSessionUrl) {
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Pane><Text color="warning">Not in remote mode. Start with `claude --remote` to use this command.</Text><Text dimColor={true}>(press esc to close)</Text></Pane>;
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
return t4;
|
||||
return (
|
||||
<Pane>
|
||||
<Text color="warning">
|
||||
Not in remote mode. Start with `claude --remote` to use this command.
|
||||
</Text>
|
||||
<Text dimColor>(press esc to close)</Text>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
let T0;
|
||||
let t4;
|
||||
let t5;
|
||||
if ($[5] !== qrCode) {
|
||||
const lines = qrCode.split("\n").filter(_temp3);
|
||||
const isLoading = lines.length === 0;
|
||||
T0 = Pane;
|
||||
if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = <Box marginBottom={1}><Text bold={true}>Remote session</Text></Box>;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
t5 = isLoading ? <Text dimColor={true}>Generating QR code…</Text> : lines.map(_temp4);
|
||||
$[5] = qrCode;
|
||||
$[6] = T0;
|
||||
$[7] = t4;
|
||||
$[8] = t5;
|
||||
} else {
|
||||
T0 = $[6];
|
||||
t4 = $[7];
|
||||
t5 = $[8];
|
||||
}
|
||||
let t6;
|
||||
if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t6 = <Text dimColor={true}>Open in browser: </Text>;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
}
|
||||
let t7;
|
||||
if ($[11] !== remoteSessionUrl) {
|
||||
t7 = <Box marginTop={1}>{t6}<Text color="ide">{remoteSessionUrl}</Text></Box>;
|
||||
$[11] = remoteSessionUrl;
|
||||
$[12] = t7;
|
||||
} else {
|
||||
t7 = $[12];
|
||||
}
|
||||
let t8;
|
||||
if ($[13] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box marginTop={1}><Text dimColor={true}>(press esc to close)</Text></Box>;
|
||||
$[13] = t8;
|
||||
} else {
|
||||
t8 = $[13];
|
||||
}
|
||||
let t9;
|
||||
if ($[14] !== T0 || $[15] !== t4 || $[16] !== t5 || $[17] !== t7) {
|
||||
t9 = <T0>{t4}{t5}{t7}{t8}</T0>;
|
||||
$[14] = T0;
|
||||
$[15] = t4;
|
||||
$[16] = t5;
|
||||
$[17] = t7;
|
||||
$[18] = t9;
|
||||
} else {
|
||||
t9 = $[18];
|
||||
}
|
||||
return t9;
|
||||
}
|
||||
function _temp4(line_0, i) {
|
||||
return <Text key={i}>{line_0}</Text>;
|
||||
}
|
||||
function _temp3(line) {
|
||||
return line.length > 0;
|
||||
}
|
||||
function _temp2(e) {
|
||||
logForDebugging("QR code generation failed", e);
|
||||
}
|
||||
function _temp(s) {
|
||||
return s.remoteSessionUrl;
|
||||
|
||||
const lines = qrCode.split('\n').filter(line => line.length > 0)
|
||||
const isLoading = lines.length === 0
|
||||
|
||||
return (
|
||||
<Pane>
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>Remote session</Text>
|
||||
</Box>
|
||||
|
||||
{/* QR Code - silently fails if generation errors, URL is still shown */}
|
||||
{isLoading ? (
|
||||
<Text dimColor>Generating QR code…</Text>
|
||||
) : (
|
||||
lines.map((line, i) => <Text key={i}>{line}</Text>)
|
||||
)}
|
||||
|
||||
{/* URL */}
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Open in browser: </Text>
|
||||
<Text color="ide">{remoteSessionUrl}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>(press esc to close)</Text>
|
||||
</Box>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async onDone => {
|
||||
return <SessionInfo onDone={onDone} />;
|
||||
};
|
||||
return <SessionInfo onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { SkillsMenu } from '../../components/skills/SkillsMenu.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <SkillsMenu onExit={onDone} commands={context.options.commands} />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { SkillsMenu } from '../../components/skills/SkillsMenu.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <SkillsMenu onExit={onDone} commands={context.options.commands} />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Stats } from '../../components/Stats.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { Stats } from '../../components/Stats.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async onDone => {
|
||||
return <Stats onClose={onDone} />;
|
||||
};
|
||||
return <Stats onClose={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { Settings } from '../../components/Settings/Settings.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Status" />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { Settings } from '../../components/Settings/Settings.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Status" />
|
||||
}
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/index.mjs';
|
||||
import type { Command } from '../commands.js';
|
||||
import { AGENT_TOOL_NAME } from '../tools/AgentTool/constants.js';
|
||||
import type { ContentBlockParam } from '@anthropic-ai/sdk/resources/index.mjs'
|
||||
import type { Command } from '../commands.js'
|
||||
import { AGENT_TOOL_NAME } from '../tools/AgentTool/constants.js'
|
||||
|
||||
const statusline = {
|
||||
type: 'prompt',
|
||||
description: "Set up Claude Code's status line UI",
|
||||
contentLength: 0,
|
||||
// Dynamic content
|
||||
contentLength: 0, // Dynamic content
|
||||
aliases: [],
|
||||
name: 'statusline',
|
||||
progressMessage: 'setting up statusLine',
|
||||
allowedTools: [AGENT_TOOL_NAME, 'Read(~/**)', 'Edit(~/.claude/settings.json)'],
|
||||
allowedTools: [
|
||||
AGENT_TOOL_NAME,
|
||||
'Read(~/**)',
|
||||
'Edit(~/.claude/settings.json)',
|
||||
],
|
||||
source: 'builtin',
|
||||
disableNonInteractive: true,
|
||||
async getPromptForCommand(args): Promise<ContentBlockParam[]> {
|
||||
const prompt = args.trim() || 'Configure my statusLine from my shell PS1 configuration';
|
||||
return [{
|
||||
type: 'text',
|
||||
text: `Create an ${AGENT_TOOL_NAME} with subagent_type "statusline-setup" and the prompt "${prompt}"`
|
||||
}];
|
||||
}
|
||||
} satisfies Command;
|
||||
export default statusline;
|
||||
const prompt =
|
||||
args.trim() || 'Configure my statusLine from my shell PS1 configuration'
|
||||
return [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Create an ${AGENT_TOOL_NAME} with subagent_type "statusline-setup" and the prompt "${prompt}"`,
|
||||
},
|
||||
]
|
||||
},
|
||||
} satisfies Command
|
||||
|
||||
export default statusline
|
||||
|
||||
@@ -1,214 +1,167 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import chalk from 'chalk';
|
||||
import type { UUID } from 'crypto';
|
||||
import * as React from 'react';
|
||||
import { getSessionId } from '../../bootstrap/state.js';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { logEvent } from '../../services/analytics/index.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { recursivelySanitizeUnicode } from '../../utils/sanitization.js';
|
||||
import { getCurrentSessionTag, getTranscriptPath, saveTag } from '../../utils/sessionStorage.js';
|
||||
function ConfirmRemoveTag(t0) {
|
||||
const $ = _c(11);
|
||||
const {
|
||||
tagName,
|
||||
onConfirm,
|
||||
onCancel
|
||||
} = t0;
|
||||
const t1 = `Current tag: #${tagName}`;
|
||||
let t2;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t2 = <Text>This will remove the tag from the current session.</Text>;
|
||||
$[0] = t2;
|
||||
} else {
|
||||
t2 = $[0];
|
||||
}
|
||||
let t3;
|
||||
if ($[1] !== onCancel || $[2] !== onConfirm) {
|
||||
t3 = value => value === "yes" ? onConfirm() : onCancel();
|
||||
$[1] = onCancel;
|
||||
$[2] = onConfirm;
|
||||
$[3] = t3;
|
||||
} else {
|
||||
t3 = $[3];
|
||||
}
|
||||
let t4;
|
||||
if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t4 = [{
|
||||
label: "Yes, remove tag",
|
||||
value: "yes"
|
||||
}, {
|
||||
label: "No, keep tag",
|
||||
value: "no"
|
||||
}];
|
||||
$[4] = t4;
|
||||
} else {
|
||||
t4 = $[4];
|
||||
}
|
||||
let t5;
|
||||
if ($[5] !== t3) {
|
||||
t5 = <Box flexDirection="column" gap={1}>{t2}<Select onChange={t3} options={t4} /></Box>;
|
||||
$[5] = t3;
|
||||
$[6] = t5;
|
||||
} else {
|
||||
t5 = $[6];
|
||||
}
|
||||
let t6;
|
||||
if ($[7] !== onCancel || $[8] !== t1 || $[9] !== t5) {
|
||||
t6 = <Dialog title="Remove tag?" subtitle={t1} onCancel={onCancel} color="warning">{t5}</Dialog>;
|
||||
$[7] = onCancel;
|
||||
$[8] = t1;
|
||||
$[9] = t5;
|
||||
$[10] = t6;
|
||||
} else {
|
||||
t6 = $[10];
|
||||
}
|
||||
return t6;
|
||||
import chalk from 'chalk'
|
||||
import type { UUID } from 'crypto'
|
||||
import * as React from 'react'
|
||||
import { getSessionId } from '../../bootstrap/state.js'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { COMMON_HELP_ARGS, COMMON_INFO_ARGS } from '../../constants/xml.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { logEvent } from '../../services/analytics/index.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import { recursivelySanitizeUnicode } from '../../utils/sanitization.js'
|
||||
import {
|
||||
getCurrentSessionTag,
|
||||
getTranscriptPath,
|
||||
saveTag,
|
||||
} from '../../utils/sessionStorage.js'
|
||||
|
||||
function ConfirmRemoveTag({
|
||||
tagName,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
}: {
|
||||
tagName: string
|
||||
onConfirm: () => void
|
||||
onCancel: () => void
|
||||
}): React.ReactNode {
|
||||
return (
|
||||
<Dialog
|
||||
title="Remove tag?"
|
||||
subtitle={`Current tag: #${tagName}`}
|
||||
onCancel={onCancel}
|
||||
color="warning"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text>This will remove the tag from the current session.</Text>
|
||||
<Select<'yes' | 'no'>
|
||||
onChange={value => (value === 'yes' ? onConfirm() : onCancel())}
|
||||
options={[
|
||||
{ label: 'Yes, remove tag', value: 'yes' },
|
||||
{ label: 'No, keep tag', value: 'no' },
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
function ToggleTagAndClose(t0) {
|
||||
const $ = _c(17);
|
||||
const {
|
||||
tagName,
|
||||
onDone
|
||||
} = t0;
|
||||
const [showConfirm, setShowConfirm] = React.useState(false);
|
||||
const [sessionId, setSessionId] = React.useState(null);
|
||||
let t1;
|
||||
if ($[0] !== tagName) {
|
||||
t1 = recursivelySanitizeUnicode(tagName).trim();
|
||||
$[0] = tagName;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
}
|
||||
const normalizedTag = t1;
|
||||
let t2;
|
||||
let t3;
|
||||
if ($[2] !== normalizedTag || $[3] !== onDone) {
|
||||
t2 = () => {
|
||||
const id = getSessionId() as UUID;
|
||||
if (!id) {
|
||||
onDone("No active session to tag", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!normalizedTag) {
|
||||
onDone("Tag name cannot be empty", {
|
||||
display: "system"
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSessionId(id);
|
||||
const currentTag = getCurrentSessionTag(id);
|
||||
if (currentTag === normalizedTag) {
|
||||
logEvent("tengu_tag_command_remove_prompt", {});
|
||||
setShowConfirm(true);
|
||||
} else {
|
||||
const isReplacing = !!currentTag;
|
||||
logEvent("tengu_tag_command_add", {
|
||||
is_replacing: isReplacing
|
||||
});
|
||||
(async () => {
|
||||
const fullPath = getTranscriptPath();
|
||||
await saveTag(id, normalizedTag, fullPath);
|
||||
onDone(`Tagged session with ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: "system"
|
||||
});
|
||||
})();
|
||||
}
|
||||
};
|
||||
t3 = [normalizedTag, onDone];
|
||||
$[2] = normalizedTag;
|
||||
$[3] = onDone;
|
||||
$[4] = t2;
|
||||
$[5] = t3;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
t3 = $[5];
|
||||
}
|
||||
React.useEffect(t2, t3);
|
||||
|
||||
function ToggleTagAndClose({
|
||||
tagName,
|
||||
onDone,
|
||||
}: {
|
||||
tagName: string
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const [showConfirm, setShowConfirm] = React.useState(false)
|
||||
const [sessionId, setSessionId] = React.useState<UUID | null>(null)
|
||||
// Sanitize unicode to prevent hidden character attacks and normalize
|
||||
const normalizedTag = recursivelySanitizeUnicode(tagName).trim()
|
||||
|
||||
React.useEffect(() => {
|
||||
const id = getSessionId() as UUID
|
||||
|
||||
if (!id) {
|
||||
onDone('No active session to tag', { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
if (!normalizedTag) {
|
||||
onDone('Tag name cannot be empty', { display: 'system' })
|
||||
return
|
||||
}
|
||||
|
||||
setSessionId(id)
|
||||
const currentTag = getCurrentSessionTag(id)
|
||||
|
||||
// If same tag exists, show confirmation dialog
|
||||
if (currentTag === normalizedTag) {
|
||||
logEvent('tengu_tag_command_remove_prompt', {})
|
||||
setShowConfirm(true)
|
||||
} else {
|
||||
// Add the new tag directly
|
||||
const isReplacing = !!currentTag
|
||||
logEvent('tengu_tag_command_add', { is_replacing: isReplacing })
|
||||
void (async () => {
|
||||
const fullPath = getTranscriptPath()
|
||||
await saveTag(id, normalizedTag, fullPath)
|
||||
onDone(`Tagged session with ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
})()
|
||||
}
|
||||
}, [normalizedTag, onDone])
|
||||
|
||||
if (showConfirm && sessionId) {
|
||||
let t4;
|
||||
if ($[6] !== normalizedTag || $[7] !== onDone || $[8] !== sessionId) {
|
||||
t4 = async () => {
|
||||
logEvent("tengu_tag_command_remove_confirmed", {});
|
||||
const fullPath_0 = getTranscriptPath();
|
||||
await saveTag(sessionId, "", fullPath_0);
|
||||
onDone(`Removed tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[6] = normalizedTag;
|
||||
$[7] = onDone;
|
||||
$[8] = sessionId;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== normalizedTag || $[11] !== onDone) {
|
||||
t5 = () => {
|
||||
logEvent("tengu_tag_command_remove_cancelled", {});
|
||||
onDone(`Kept tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[10] = normalizedTag;
|
||||
$[11] = onDone;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== normalizedTag || $[14] !== t4 || $[15] !== t5) {
|
||||
t6 = <ConfirmRemoveTag tagName={normalizedTag} onConfirm={t4} onCancel={t5} />;
|
||||
$[13] = normalizedTag;
|
||||
$[14] = t4;
|
||||
$[15] = t5;
|
||||
$[16] = t6;
|
||||
} else {
|
||||
t6 = $[16];
|
||||
}
|
||||
return t6;
|
||||
return (
|
||||
<ConfirmRemoveTag
|
||||
tagName={normalizedTag}
|
||||
onConfirm={async () => {
|
||||
logEvent('tengu_tag_command_remove_confirmed', {})
|
||||
const fullPath = getTranscriptPath()
|
||||
await saveTag(sessionId, '', fullPath)
|
||||
onDone(`Removed tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
}}
|
||||
onCancel={() => {
|
||||
logEvent('tengu_tag_command_remove_cancelled', {})
|
||||
onDone(`Kept tag ${chalk.cyan(`#${normalizedTag}`)}`, {
|
||||
display: 'system',
|
||||
})
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
function ShowHelp(t0) {
|
||||
const $ = _c(3);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
let t1;
|
||||
let t2;
|
||||
if ($[0] !== onDone) {
|
||||
t1 = () => {
|
||||
onDone("Usage: /tag <tag-name>\n\nToggle a searchable tag on the current session.\nRun the same command again to remove the tag.\nTags are displayed after the branch name in /resume and can be searched with /.\n\nExamples:\n /tag bugfix # Add tag\n /tag bugfix # Remove tag (toggle)\n /tag feature-auth\n /tag wip", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
t2 = [onDone];
|
||||
$[0] = onDone;
|
||||
$[1] = t1;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
t2 = $[2];
|
||||
}
|
||||
React.useEffect(t1, t2);
|
||||
return null;
|
||||
|
||||
function ShowHelp({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
React.useEffect(() => {
|
||||
onDone(
|
||||
`Usage: /tag <tag-name>
|
||||
|
||||
Toggle a searchable tag on the current session.
|
||||
Run the same command again to remove the tag.
|
||||
Tags are displayed after the branch name in /resume and can be searched with /.
|
||||
|
||||
Examples:
|
||||
/tag bugfix # Add tag
|
||||
/tag bugfix # Remove tag (toggle)
|
||||
/tag feature-auth
|
||||
/tag wip`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
}, [onDone])
|
||||
|
||||
return null
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, _context: unknown, args?: string): Promise<React.ReactNode> {
|
||||
args = args?.trim() || '';
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
_context: unknown,
|
||||
args?: string,
|
||||
): Promise<React.ReactNode> {
|
||||
args = args?.trim() || ''
|
||||
|
||||
if (COMMON_INFO_ARGS.includes(args) || COMMON_HELP_ARGS.includes(args)) {
|
||||
return <ShowHelp onDone={onDone} />;
|
||||
return <ShowHelp onDone={onDone} />
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
return <ShowHelp onDone={onDone} />;
|
||||
return <ShowHelp onDone={onDone} />
|
||||
}
|
||||
return <ToggleTagAndClose tagName={args} onDone={onDone} />;
|
||||
|
||||
return <ToggleTagAndClose tagName={args} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { BackgroundTasksDialog } from '../../components/tasks/BackgroundTasksDialog.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode> {
|
||||
return <BackgroundTasksDialog toolUseContext={context} onDone={onDone} />;
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { BackgroundTasksDialog } from '../../components/tasks/BackgroundTasksDialog.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode> {
|
||||
return <BackgroundTasksDialog toolUseContext={context} onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
import chalk from 'chalk';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import { homedir, platform } from 'os';
|
||||
import { dirname, join } from 'path';
|
||||
import type { ThemeName } from 'src/utils/theme.js';
|
||||
import { pathToFileURL } from 'url';
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js';
|
||||
import { color } from '../../ink.js';
|
||||
import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js';
|
||||
import type { ToolUseContext } from '../../Tool.js';
|
||||
import type { LocalJSXCommandContext, LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { backupTerminalPreferences, checkAndRestoreTerminalBackup, getTerminalPlistPath, markTerminalSetupComplete } from '../../utils/appleTerminalBackup.js';
|
||||
import { setupShellCompletion } from '../../utils/completionCache.js';
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
|
||||
import { env } from '../../utils/env.js';
|
||||
import { isFsInaccessible } from '../../utils/errors.js';
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js';
|
||||
import { addItemToJSONCArray, safeParseJSONC } from '../../utils/json.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js';
|
||||
const EOL = '\n';
|
||||
import chalk from 'chalk'
|
||||
import { randomBytes } from 'crypto'
|
||||
import { copyFile, mkdir, readFile, writeFile } from 'fs/promises'
|
||||
import { homedir, platform } from 'os'
|
||||
import { dirname, join } from 'path'
|
||||
import type { ThemeName } from 'src/utils/theme.js'
|
||||
import { pathToFileURL } from 'url'
|
||||
import { supportsHyperlinks } from '../../ink/supports-hyperlinks.js'
|
||||
import { color } from '../../ink.js'
|
||||
import { maybeMarkProjectOnboardingComplete } from '../../projectOnboardingState.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import type {
|
||||
LocalJSXCommandContext,
|
||||
LocalJSXCommandOnDone,
|
||||
} from '../../types/command.js'
|
||||
import {
|
||||
backupTerminalPreferences,
|
||||
checkAndRestoreTerminalBackup,
|
||||
getTerminalPlistPath,
|
||||
markTerminalSetupComplete,
|
||||
} from '../../utils/appleTerminalBackup.js'
|
||||
import { setupShellCompletion } from '../../utils/completionCache.js'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { env } from '../../utils/env.js'
|
||||
import { isFsInaccessible } from '../../utils/errors.js'
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
||||
import { addItemToJSONCArray, safeParseJSONC } from '../../utils/json.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { jsonParse, jsonStringify } from '../../utils/slowOperations.js'
|
||||
|
||||
const EOL = '\n'
|
||||
|
||||
// Terminals that natively support CSI u / Kitty keyboard protocol
|
||||
const NATIVE_CSIU_TERMINALS: Record<string, string> = {
|
||||
@@ -28,8 +37,8 @@ const NATIVE_CSIU_TERMINALS: Record<string, string> = {
|
||||
kitty: 'Kitty',
|
||||
'iTerm.app': 'iTerm2',
|
||||
WezTerm: 'WezTerm',
|
||||
WarpTerminal: 'Warp'
|
||||
};
|
||||
WarpTerminal: 'Warp',
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if we're running in a VSCode Remote SSH session.
|
||||
@@ -37,18 +46,26 @@ const NATIVE_CSIU_TERMINALS: Record<string, string> = {
|
||||
* not the remote server where Claude is running.
|
||||
*/
|
||||
function isVSCodeRemoteSSH(): boolean {
|
||||
const askpassMain = process.env.VSCODE_GIT_ASKPASS_MAIN ?? '';
|
||||
const path = process.env.PATH ?? '';
|
||||
const askpassMain = process.env.VSCODE_GIT_ASKPASS_MAIN ?? ''
|
||||
const path = process.env.PATH ?? ''
|
||||
|
||||
// Check both env vars - VSCODE_GIT_ASKPASS_MAIN is more reliable when git extension
|
||||
// is active, and PATH is a fallback. Omit path separator for Windows compatibility.
|
||||
return askpassMain.includes('.vscode-server') || askpassMain.includes('.cursor-server') || askpassMain.includes('.windsurf-server') || path.includes('.vscode-server') || path.includes('.cursor-server') || path.includes('.windsurf-server');
|
||||
return (
|
||||
askpassMain.includes('.vscode-server') ||
|
||||
askpassMain.includes('.cursor-server') ||
|
||||
askpassMain.includes('.windsurf-server') ||
|
||||
path.includes('.vscode-server') ||
|
||||
path.includes('.cursor-server') ||
|
||||
path.includes('.windsurf-server')
|
||||
)
|
||||
}
|
||||
|
||||
export function getNativeCSIuTerminalDisplayName(): string | null {
|
||||
if (!env.terminal || !(env.terminal in NATIVE_CSIU_TERMINALS)) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
return NATIVE_CSIU_TERMINALS[env.terminal] ?? null;
|
||||
return NATIVE_CSIU_TERMINALS[env.terminal] ?? null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,101 +81,120 @@ export function getNativeCSIuTerminalDisplayName(): string | null {
|
||||
*/
|
||||
function formatPathLink(filePath: string): string {
|
||||
if (!supportsHyperlinks()) {
|
||||
return filePath;
|
||||
return filePath
|
||||
}
|
||||
const fileUrl = pathToFileURL(filePath).href;
|
||||
const fileUrl = pathToFileURL(filePath).href
|
||||
// OSC 8 hyperlink: \e]8;;URL\a TEXT \e]8;;\a
|
||||
return `\x1b]8;;${fileUrl}\x07${filePath}\x1b]8;;\x07`;
|
||||
return `\x1b]8;;${fileUrl}\x07${filePath}\x1b]8;;\x07`
|
||||
}
|
||||
|
||||
export function shouldOfferTerminalSetup(): boolean {
|
||||
// iTerm2, WezTerm, Ghostty, Kitty, and Warp natively support CSI u / Kitty
|
||||
// keyboard protocol, which Claude Code already parses. No setup needed for
|
||||
// these terminals.
|
||||
return platform() === 'darwin' && env.terminal === 'Apple_Terminal' || env.terminal === 'vscode' || env.terminal === 'cursor' || env.terminal === 'windsurf' || env.terminal === 'alacritty' || env.terminal === 'zed';
|
||||
return (
|
||||
(platform() === 'darwin' && env.terminal === 'Apple_Terminal') ||
|
||||
env.terminal === 'vscode' ||
|
||||
env.terminal === 'cursor' ||
|
||||
env.terminal === 'windsurf' ||
|
||||
env.terminal === 'alacritty' ||
|
||||
env.terminal === 'zed'
|
||||
)
|
||||
}
|
||||
|
||||
export async function setupTerminal(theme: ThemeName): Promise<string> {
|
||||
let result = '';
|
||||
let result = ''
|
||||
|
||||
switch (env.terminal) {
|
||||
case 'Apple_Terminal':
|
||||
result = await enableOptionAsMetaForTerminal(theme);
|
||||
break;
|
||||
result = await enableOptionAsMetaForTerminal(theme)
|
||||
break
|
||||
case 'vscode':
|
||||
result = await installBindingsForVSCodeTerminal('VSCode', theme);
|
||||
break;
|
||||
result = await installBindingsForVSCodeTerminal('VSCode', theme)
|
||||
break
|
||||
case 'cursor':
|
||||
result = await installBindingsForVSCodeTerminal('Cursor', theme);
|
||||
break;
|
||||
result = await installBindingsForVSCodeTerminal('Cursor', theme)
|
||||
break
|
||||
case 'windsurf':
|
||||
result = await installBindingsForVSCodeTerminal('Windsurf', theme);
|
||||
break;
|
||||
result = await installBindingsForVSCodeTerminal('Windsurf', theme)
|
||||
break
|
||||
case 'alacritty':
|
||||
result = await installBindingsForAlacritty(theme);
|
||||
break;
|
||||
result = await installBindingsForAlacritty(theme)
|
||||
break
|
||||
case 'zed':
|
||||
result = await installBindingsForZed(theme);
|
||||
break;
|
||||
result = await installBindingsForZed(theme)
|
||||
break
|
||||
case null:
|
||||
break;
|
||||
break
|
||||
}
|
||||
|
||||
saveGlobalConfig(current => {
|
||||
if (['vscode', 'cursor', 'windsurf', 'alacritty', 'zed'].includes(env.terminal ?? '')) {
|
||||
if (current.shiftEnterKeyBindingInstalled === true) return current;
|
||||
return {
|
||||
...current,
|
||||
shiftEnterKeyBindingInstalled: true
|
||||
};
|
||||
if (
|
||||
['vscode', 'cursor', 'windsurf', 'alacritty', 'zed'].includes(
|
||||
env.terminal ?? '',
|
||||
)
|
||||
) {
|
||||
if (current.shiftEnterKeyBindingInstalled === true) return current
|
||||
return { ...current, shiftEnterKeyBindingInstalled: true }
|
||||
} else if (env.terminal === 'Apple_Terminal') {
|
||||
if (current.optionAsMetaKeyInstalled === true) return current;
|
||||
return {
|
||||
...current,
|
||||
optionAsMetaKeyInstalled: true
|
||||
};
|
||||
if (current.optionAsMetaKeyInstalled === true) return current
|
||||
return { ...current, optionAsMetaKeyInstalled: true }
|
||||
}
|
||||
return current;
|
||||
});
|
||||
maybeMarkProjectOnboardingComplete();
|
||||
return current
|
||||
})
|
||||
|
||||
maybeMarkProjectOnboardingComplete()
|
||||
|
||||
// Install shell completions (ant-only, since the completion command is ant-only)
|
||||
if ((process.env.USER_TYPE) === 'ant') {
|
||||
result += await setupShellCompletion(theme);
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
result += await setupShellCompletion(theme)
|
||||
}
|
||||
return result;
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function isShiftEnterKeyBindingInstalled(): boolean {
|
||||
return getGlobalConfig().shiftEnterKeyBindingInstalled === true;
|
||||
return getGlobalConfig().shiftEnterKeyBindingInstalled === true
|
||||
}
|
||||
|
||||
export function hasUsedBackslashReturn(): boolean {
|
||||
return getGlobalConfig().hasUsedBackslashReturn === true;
|
||||
return getGlobalConfig().hasUsedBackslashReturn === true
|
||||
}
|
||||
|
||||
export function markBackslashReturnUsed(): void {
|
||||
const config = getGlobalConfig();
|
||||
const config = getGlobalConfig()
|
||||
if (!config.hasUsedBackslashReturn) {
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
hasUsedBackslashReturn: true
|
||||
}));
|
||||
hasUsedBackslashReturn: true,
|
||||
}))
|
||||
}
|
||||
}
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: ToolUseContext & LocalJSXCommandContext, _args: string): Promise<null> {
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: ToolUseContext & LocalJSXCommandContext,
|
||||
_args: string,
|
||||
): Promise<null> {
|
||||
if (env.terminal && env.terminal in NATIVE_CSIU_TERMINALS) {
|
||||
const message = `Shift+Enter is natively supported in ${NATIVE_CSIU_TERMINALS[env.terminal]}.
|
||||
|
||||
No configuration needed. Just use Shift+Enter to add newlines.`;
|
||||
onDone(message);
|
||||
return null;
|
||||
No configuration needed. Just use Shift+Enter to add newlines.`
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
|
||||
// Check if terminal is supported
|
||||
if (!shouldOfferTerminalSetup()) {
|
||||
const terminalName = env.terminal || 'your current terminal';
|
||||
const currentPlatform = getPlatform();
|
||||
const terminalName = env.terminal || 'your current terminal'
|
||||
const currentPlatform = getPlatform()
|
||||
|
||||
// Build platform-specific terminal suggestions
|
||||
let platformTerminals = '';
|
||||
let platformTerminals = ''
|
||||
if (currentPlatform === 'macos') {
|
||||
platformTerminals = ' • macOS: Apple Terminal\n';
|
||||
platformTerminals = ' • macOS: Apple Terminal\n'
|
||||
} else if (currentPlatform === 'windows') {
|
||||
platformTerminals = ' • Windows: Windows Terminal\n';
|
||||
platformTerminals = ' • Windows: Windows Terminal\n'
|
||||
}
|
||||
// For Linux and other platforms, we don't show native terminal options
|
||||
// since they're not currently supported
|
||||
@@ -175,356 +211,482 @@ ${platformTerminals} • IDE: VSCode, Cursor, Windsurf, Zed
|
||||
• Other: Alacritty
|
||||
3. Return to tmux/screen - settings will persist
|
||||
|
||||
${chalk.dim('Note: iTerm2, WezTerm, Ghostty, Kitty, and Warp support Shift+Enter natively.')}`;
|
||||
onDone(message);
|
||||
return null;
|
||||
${chalk.dim('Note: iTerm2, WezTerm, Ghostty, Kitty, and Warp support Shift+Enter natively.')}`
|
||||
onDone(message)
|
||||
return null
|
||||
}
|
||||
const result = await setupTerminal(context.options.theme);
|
||||
onDone(result);
|
||||
return null;
|
||||
|
||||
const result = await setupTerminal(context.options.theme)
|
||||
onDone(result)
|
||||
return null
|
||||
}
|
||||
|
||||
type VSCodeKeybinding = {
|
||||
key: string;
|
||||
command: string;
|
||||
args: {
|
||||
text: string;
|
||||
};
|
||||
when: string;
|
||||
};
|
||||
async function installBindingsForVSCodeTerminal(editor: 'VSCode' | 'Cursor' | 'Windsurf' = 'VSCode', theme: ThemeName): Promise<string> {
|
||||
key: string
|
||||
command: string
|
||||
args: { text: string }
|
||||
when: string
|
||||
}
|
||||
|
||||
async function installBindingsForVSCodeTerminal(
|
||||
editor: 'VSCode' | 'Cursor' | 'Windsurf' = 'VSCode',
|
||||
theme: ThemeName,
|
||||
): Promise<string> {
|
||||
// Check if we're running in a VSCode Remote SSH session
|
||||
// In this case, keybindings need to be installed on the LOCAL machine
|
||||
if (isVSCodeRemoteSSH()) {
|
||||
return `${color('warning', theme)(`Cannot install keybindings from a remote ${editor} session.`)}${EOL}${EOL}${editor} keybindings must be installed on your local machine, not the remote server.${EOL}${EOL}To install the Shift+Enter keybinding:${EOL}1. Open ${editor} on your local machine (not connected to remote)${EOL}2. Open the Command Palette (Cmd/Ctrl+Shift+P) → "Preferences: Open Keyboard Shortcuts (JSON)"${EOL}3. Add this keybinding (the file must be a JSON array):${EOL}${EOL}${chalk.dim(`[
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
`Cannot install keybindings from a remote ${editor} session.`,
|
||||
)}${EOL}${EOL}${editor} keybindings must be installed on your local machine, not the remote server.${EOL}${EOL}To install the Shift+Enter keybinding:${EOL}1. Open ${editor} on your local machine (not connected to remote)${EOL}2. Open the Command Palette (Cmd/Ctrl+Shift+P) → "Preferences: Open Keyboard Shortcuts (JSON)"${EOL}3. Add this keybinding (the file must be a JSON array):${EOL}${EOL}${chalk.dim(`[
|
||||
{
|
||||
"key": "shift+enter",
|
||||
"command": "workbench.action.terminal.sendSequence",
|
||||
"args": { "text": "\\u001b\\r" },
|
||||
"when": "terminalFocus"
|
||||
}
|
||||
]`)}${EOL}`;
|
||||
]`)}${EOL}`
|
||||
}
|
||||
const editorDir = editor === 'VSCode' ? 'Code' : editor;
|
||||
const userDirPath = join(homedir(), platform() === 'win32' ? join('AppData', 'Roaming', editorDir, 'User') : platform() === 'darwin' ? join('Library', 'Application Support', editorDir, 'User') : join('.config', editorDir, 'User'));
|
||||
const keybindingsPath = join(userDirPath, 'keybindings.json');
|
||||
|
||||
const editorDir = editor === 'VSCode' ? 'Code' : editor
|
||||
const userDirPath = join(
|
||||
homedir(),
|
||||
platform() === 'win32'
|
||||
? join('AppData', 'Roaming', editorDir, 'User')
|
||||
: platform() === 'darwin'
|
||||
? join('Library', 'Application Support', editorDir, 'User')
|
||||
: join('.config', editorDir, 'User'),
|
||||
)
|
||||
const keybindingsPath = join(userDirPath, 'keybindings.json')
|
||||
|
||||
try {
|
||||
// Ensure user directory exists (idempotent with recursive)
|
||||
await mkdir(userDirPath, {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(userDirPath, { recursive: true })
|
||||
|
||||
// Read existing keybindings file, or default to empty array if it doesn't exist
|
||||
let content = '[]';
|
||||
let keybindings: VSCodeKeybinding[] = [];
|
||||
let fileExists = false;
|
||||
let content = '[]'
|
||||
let keybindings: VSCodeKeybinding[] = []
|
||||
let fileExists = false
|
||||
try {
|
||||
content = await readFile(keybindingsPath, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
fileExists = true;
|
||||
keybindings = safeParseJSONC(content) as VSCodeKeybinding[] ?? [];
|
||||
content = await readFile(keybindingsPath, { encoding: 'utf-8' })
|
||||
fileExists = true
|
||||
keybindings = (safeParseJSONC(content) as VSCodeKeybinding[]) ?? []
|
||||
} catch (e: unknown) {
|
||||
if (!isFsInaccessible(e)) throw e;
|
||||
if (!isFsInaccessible(e)) throw e
|
||||
}
|
||||
|
||||
// Backup the existing file before modifying it
|
||||
if (fileExists) {
|
||||
const randomSha = randomBytes(4).toString('hex');
|
||||
const backupPath = `${keybindingsPath}.${randomSha}.bak`;
|
||||
const randomSha = randomBytes(4).toString('hex')
|
||||
const backupPath = `${keybindingsPath}.${randomSha}.bak`
|
||||
try {
|
||||
await copyFile(keybindingsPath, backupPath);
|
||||
await copyFile(keybindingsPath, backupPath)
|
||||
} catch {
|
||||
return `${color('warning', theme)(`Error backing up existing ${editor} terminal keybindings. Bailing out.`)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
`Error backing up existing ${editor} terminal keybindings. Bailing out.`,
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`
|
||||
}
|
||||
}
|
||||
|
||||
// Check if keybinding already exists
|
||||
const existingBinding = keybindings.find(binding => binding.key === 'shift+enter' && binding.command === 'workbench.action.terminal.sendSequence' && binding.when === 'terminalFocus');
|
||||
const existingBinding = keybindings.find(
|
||||
binding =>
|
||||
binding.key === 'shift+enter' &&
|
||||
binding.command === 'workbench.action.terminal.sendSequence' &&
|
||||
binding.when === 'terminalFocus',
|
||||
)
|
||||
if (existingBinding) {
|
||||
return `${color('warning', theme)(`Found existing ${editor} terminal Shift+Enter key binding. Remove it to continue.`)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
`Found existing ${editor} terminal Shift+Enter key binding. Remove it to continue.`,
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`
|
||||
}
|
||||
|
||||
// Create the new keybinding
|
||||
const newKeybinding: VSCodeKeybinding = {
|
||||
key: 'shift+enter',
|
||||
command: 'workbench.action.terminal.sendSequence',
|
||||
args: {
|
||||
text: '\u001b\r'
|
||||
},
|
||||
when: 'terminalFocus'
|
||||
};
|
||||
args: { text: '\u001b\r' },
|
||||
when: 'terminalFocus',
|
||||
}
|
||||
|
||||
// Modify the content by adding the new keybinding while preserving comments and formatting
|
||||
const updatedContent = addItemToJSONCArray(content, newKeybinding);
|
||||
const updatedContent = addItemToJSONCArray(content, newKeybinding)
|
||||
|
||||
// Write the updated content back to the file
|
||||
await writeFile(keybindingsPath, updatedContent, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
return `${color('success', theme)(`Installed ${editor} terminal Shift+Enter key binding`)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`;
|
||||
await writeFile(keybindingsPath, updatedContent, { encoding: 'utf-8' })
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
`Installed ${editor} terminal Shift+Enter key binding`,
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keybindingsPath)}`)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
throw new Error(`Failed to install ${editor} terminal Shift+Enter key binding`);
|
||||
logError(error)
|
||||
throw new Error(
|
||||
`Failed to install ${editor} terminal Shift+Enter key binding`,
|
||||
)
|
||||
}
|
||||
}
|
||||
async function enableOptionAsMetaForProfile(profileName: string): Promise<boolean> {
|
||||
|
||||
async function enableOptionAsMetaForProfile(
|
||||
profileName: string,
|
||||
): Promise<boolean> {
|
||||
// First try to add the property (in case it doesn't exist)
|
||||
// Quote the profile name to handle names with spaces (e.g., "Man Page", "Red Sands")
|
||||
const {
|
||||
code: addCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Add :'Window Settings':'${profileName}':useOptionAsMetaKey bool true`, getTerminalPlistPath()]);
|
||||
const { code: addCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Add :'Window Settings':'${profileName}':useOptionAsMetaKey bool true`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
// If adding fails (likely because it already exists), try setting it instead
|
||||
if (addCode !== 0) {
|
||||
const {
|
||||
code: setCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Set :'Window Settings':'${profileName}':useOptionAsMetaKey true`, getTerminalPlistPath()]);
|
||||
const { code: setCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Set :'Window Settings':'${profileName}':useOptionAsMetaKey true`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
if (setCode !== 0) {
|
||||
logError(new Error(`Failed to enable Option as Meta key for Terminal.app profile: ${profileName}`));
|
||||
return false;
|
||||
logError(
|
||||
new Error(
|
||||
`Failed to enable Option as Meta key for Terminal.app profile: ${profileName}`,
|
||||
),
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return true
|
||||
}
|
||||
async function disableAudioBellForProfile(profileName: string): Promise<boolean> {
|
||||
|
||||
async function disableAudioBellForProfile(
|
||||
profileName: string,
|
||||
): Promise<boolean> {
|
||||
// First try to add the property (in case it doesn't exist)
|
||||
// Quote the profile name to handle names with spaces (e.g., "Man Page", "Red Sands")
|
||||
const {
|
||||
code: addCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Add :'Window Settings':'${profileName}':Bell bool false`, getTerminalPlistPath()]);
|
||||
const { code: addCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Add :'Window Settings':'${profileName}':Bell bool false`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
// If adding fails (likely because it already exists), try setting it instead
|
||||
if (addCode !== 0) {
|
||||
const {
|
||||
code: setCode
|
||||
} = await execFileNoThrow('/usr/libexec/PlistBuddy', ['-c', `Set :'Window Settings':'${profileName}':Bell false`, getTerminalPlistPath()]);
|
||||
const { code: setCode } = await execFileNoThrow('/usr/libexec/PlistBuddy', [
|
||||
'-c',
|
||||
`Set :'Window Settings':'${profileName}':Bell false`,
|
||||
getTerminalPlistPath(),
|
||||
])
|
||||
|
||||
if (setCode !== 0) {
|
||||
logError(new Error(`Failed to disable audio bell for Terminal.app profile: ${profileName}`));
|
||||
return false;
|
||||
logError(
|
||||
new Error(
|
||||
`Failed to disable audio bell for Terminal.app profile: ${profileName}`,
|
||||
),
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Enable Option as Meta key for Terminal.app
|
||||
async function enableOptionAsMetaForTerminal(theme: ThemeName): Promise<string> {
|
||||
async function enableOptionAsMetaForTerminal(
|
||||
theme: ThemeName,
|
||||
): Promise<string> {
|
||||
try {
|
||||
// Create a backup of the current plist file
|
||||
const backupPath = await backupTerminalPreferences();
|
||||
const backupPath = await backupTerminalPreferences()
|
||||
if (!backupPath) {
|
||||
throw new Error('Failed to create backup of Terminal.app preferences, bailing out');
|
||||
throw new Error(
|
||||
'Failed to create backup of Terminal.app preferences, bailing out',
|
||||
)
|
||||
}
|
||||
|
||||
// Read the current default profile from the plist
|
||||
const {
|
||||
stdout: defaultProfile,
|
||||
code: readCode
|
||||
} = await execFileNoThrow('defaults', ['read', 'com.apple.Terminal', 'Default Window Settings']);
|
||||
const { stdout: defaultProfile, code: readCode } = await execFileNoThrow(
|
||||
'defaults',
|
||||
['read', 'com.apple.Terminal', 'Default Window Settings'],
|
||||
)
|
||||
|
||||
if (readCode !== 0 || !defaultProfile.trim()) {
|
||||
throw new Error('Failed to read default Terminal.app profile');
|
||||
throw new Error('Failed to read default Terminal.app profile')
|
||||
}
|
||||
const {
|
||||
stdout: startupProfile,
|
||||
code: startupCode
|
||||
} = await execFileNoThrow('defaults', ['read', 'com.apple.Terminal', 'Startup Window Settings']);
|
||||
|
||||
const { stdout: startupProfile, code: startupCode } = await execFileNoThrow(
|
||||
'defaults',
|
||||
['read', 'com.apple.Terminal', 'Startup Window Settings'],
|
||||
)
|
||||
if (startupCode !== 0 || !startupProfile.trim()) {
|
||||
throw new Error('Failed to read startup Terminal.app profile');
|
||||
throw new Error('Failed to read startup Terminal.app profile')
|
||||
}
|
||||
let wasAnyProfileUpdated = false;
|
||||
const defaultProfileName = defaultProfile.trim();
|
||||
const optionAsMetaEnabled = await enableOptionAsMetaForProfile(defaultProfileName);
|
||||
const audioBellDisabled = await disableAudioBellForProfile(defaultProfileName);
|
||||
|
||||
let wasAnyProfileUpdated = false
|
||||
|
||||
const defaultProfileName = defaultProfile.trim()
|
||||
const optionAsMetaEnabled =
|
||||
await enableOptionAsMetaForProfile(defaultProfileName)
|
||||
const audioBellDisabled =
|
||||
await disableAudioBellForProfile(defaultProfileName)
|
||||
|
||||
if (optionAsMetaEnabled || audioBellDisabled) {
|
||||
wasAnyProfileUpdated = true;
|
||||
wasAnyProfileUpdated = true
|
||||
}
|
||||
const startupProfileName = startupProfile.trim();
|
||||
|
||||
const startupProfileName = startupProfile.trim()
|
||||
|
||||
// Only proceed if the startup profile is different from the default profile
|
||||
if (startupProfileName !== defaultProfileName) {
|
||||
const startupOptionAsMetaEnabled = await enableOptionAsMetaForProfile(startupProfileName);
|
||||
const startupAudioBellDisabled = await disableAudioBellForProfile(startupProfileName);
|
||||
const startupOptionAsMetaEnabled =
|
||||
await enableOptionAsMetaForProfile(startupProfileName)
|
||||
const startupAudioBellDisabled =
|
||||
await disableAudioBellForProfile(startupProfileName)
|
||||
|
||||
if (startupOptionAsMetaEnabled || startupAudioBellDisabled) {
|
||||
wasAnyProfileUpdated = true;
|
||||
wasAnyProfileUpdated = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!wasAnyProfileUpdated) {
|
||||
throw new Error('Failed to enable Option as Meta key or disable audio bell for any Terminal.app profile');
|
||||
throw new Error(
|
||||
'Failed to enable Option as Meta key or disable audio bell for any Terminal.app profile',
|
||||
)
|
||||
}
|
||||
|
||||
// Flush the preferences cache
|
||||
await execFileNoThrow('killall', ['cfprefsd']);
|
||||
markTerminalSetupComplete();
|
||||
return `${color('success', theme)(`Configured Terminal.app settings:`)}${EOL}${color('success', theme)('- Enabled "Use Option as Meta key"')}${EOL}${color('success', theme)('- Switched to visual bell')}${EOL}${chalk.dim('Option+Enter will now enter a newline.')}${EOL}${chalk.dim('You must restart Terminal.app for changes to take effect.', theme)}${EOL}`;
|
||||
await execFileNoThrow('killall', ['cfprefsd'])
|
||||
|
||||
markTerminalSetupComplete()
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
`Configured Terminal.app settings:`,
|
||||
)}${EOL}${color('success', theme)('- Enabled "Use Option as Meta key"')}${EOL}${color('success', theme)('- Switched to visual bell')}${EOL}${chalk.dim('Option+Enter will now enter a newline.')}${EOL}${chalk.dim('You must restart Terminal.app for changes to take effect.', theme)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
logError(error)
|
||||
|
||||
// Attempt to restore from backup
|
||||
const restoreResult = await checkAndRestoreTerminalBackup();
|
||||
const errorMessage = 'Failed to enable Option as Meta key for Terminal.app.';
|
||||
const restoreResult = await checkAndRestoreTerminalBackup()
|
||||
|
||||
const errorMessage = 'Failed to enable Option as Meta key for Terminal.app.'
|
||||
if (restoreResult.status === 'restored') {
|
||||
throw new Error(`${errorMessage} Your settings have been restored from backup.`);
|
||||
throw new Error(
|
||||
`${errorMessage} Your settings have been restored from backup.`,
|
||||
)
|
||||
} else if (restoreResult.status === 'failed') {
|
||||
throw new Error(`${errorMessage} Restoring from backup failed, try manually with: defaults import com.apple.Terminal ${restoreResult.backupPath}`);
|
||||
throw new Error(
|
||||
`${errorMessage} Restoring from backup failed, try manually with: defaults import com.apple.Terminal ${restoreResult.backupPath}`,
|
||||
)
|
||||
} else {
|
||||
throw new Error(`${errorMessage} No backup was available to restore from.`);
|
||||
throw new Error(
|
||||
`${errorMessage} No backup was available to restore from.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function installBindingsForAlacritty(theme: ThemeName): Promise<string> {
|
||||
const ALACRITTY_KEYBINDING = `[[keyboard.bindings]]
|
||||
key = "Return"
|
||||
mods = "Shift"
|
||||
chars = "\\u001B\\r"`;
|
||||
chars = "\\u001B\\r"`
|
||||
|
||||
// Get Alacritty config file paths in order of preference
|
||||
const configPaths: string[] = [];
|
||||
const configPaths: string[] = []
|
||||
|
||||
// XDG config path (Linux and macOS)
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME;
|
||||
const xdgConfigHome = process.env.XDG_CONFIG_HOME
|
||||
if (xdgConfigHome) {
|
||||
configPaths.push(join(xdgConfigHome, 'alacritty', 'alacritty.toml'));
|
||||
configPaths.push(join(xdgConfigHome, 'alacritty', 'alacritty.toml'))
|
||||
} else {
|
||||
configPaths.push(join(homedir(), '.config', 'alacritty', 'alacritty.toml'));
|
||||
configPaths.push(join(homedir(), '.config', 'alacritty', 'alacritty.toml'))
|
||||
}
|
||||
|
||||
// Windows-specific path
|
||||
if (platform() === 'win32') {
|
||||
const appData = process.env.APPDATA;
|
||||
const appData = process.env.APPDATA
|
||||
if (appData) {
|
||||
configPaths.push(join(appData, 'alacritty', 'alacritty.toml'));
|
||||
configPaths.push(join(appData, 'alacritty', 'alacritty.toml'))
|
||||
}
|
||||
}
|
||||
|
||||
// Find existing config file by attempting to read it, or use first preferred path
|
||||
let configPath: string | null = null;
|
||||
let configContent = '';
|
||||
let configExists = false;
|
||||
let configPath: string | null = null
|
||||
let configContent = ''
|
||||
let configExists = false
|
||||
|
||||
for (const path of configPaths) {
|
||||
try {
|
||||
configContent = await readFile(path, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
configPath = path;
|
||||
configExists = true;
|
||||
break;
|
||||
configContent = await readFile(path, { encoding: 'utf-8' })
|
||||
configPath = path
|
||||
configExists = true
|
||||
break
|
||||
} catch (e: unknown) {
|
||||
if (!isFsInaccessible(e)) throw e;
|
||||
if (!isFsInaccessible(e)) throw e
|
||||
// File missing or inaccessible — try next config path
|
||||
}
|
||||
}
|
||||
|
||||
// If no config exists, use the first path (XDG/default location)
|
||||
if (!configPath) {
|
||||
configPath = configPaths[0] ?? null;
|
||||
configPath = configPaths[0] ?? null
|
||||
}
|
||||
|
||||
if (!configPath) {
|
||||
throw new Error('No valid config path found for Alacritty');
|
||||
throw new Error('No valid config path found for Alacritty')
|
||||
}
|
||||
|
||||
try {
|
||||
if (configExists) {
|
||||
// Check if keybinding already exists (look for Shift+Return binding)
|
||||
if (configContent.includes('mods = "Shift"') && configContent.includes('key = "Return"')) {
|
||||
return `${color('warning', theme)('Found existing Alacritty Shift+Enter key binding. Remove it to continue.')}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`;
|
||||
if (
|
||||
configContent.includes('mods = "Shift"') &&
|
||||
configContent.includes('key = "Return"')
|
||||
) {
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Found existing Alacritty Shift+Enter key binding. Remove it to continue.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`
|
||||
}
|
||||
|
||||
// Create backup
|
||||
const randomSha = randomBytes(4).toString('hex');
|
||||
const backupPath = `${configPath}.${randomSha}.bak`;
|
||||
const randomSha = randomBytes(4).toString('hex')
|
||||
const backupPath = `${configPath}.${randomSha}.bak`
|
||||
try {
|
||||
await copyFile(configPath, backupPath);
|
||||
await copyFile(configPath, backupPath)
|
||||
} catch {
|
||||
return `${color('warning', theme)('Error backing up existing Alacritty config. Bailing out.')}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Error backing up existing Alacritty config. Bailing out.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`
|
||||
}
|
||||
} else {
|
||||
// Ensure config directory exists (idempotent with recursive)
|
||||
await mkdir(dirname(configPath), {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(dirname(configPath), { recursive: true })
|
||||
}
|
||||
|
||||
// Add the keybinding to the config
|
||||
let updatedContent = configContent;
|
||||
let updatedContent = configContent
|
||||
if (configContent && !configContent.endsWith('\n')) {
|
||||
updatedContent += '\n';
|
||||
updatedContent += '\n'
|
||||
}
|
||||
updatedContent += '\n' + ALACRITTY_KEYBINDING + '\n';
|
||||
updatedContent += '\n' + ALACRITTY_KEYBINDING + '\n'
|
||||
|
||||
// Write the updated config
|
||||
await writeFile(configPath, updatedContent, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
return `${color('success', theme)('Installed Alacritty Shift+Enter key binding')}${EOL}${color('success', theme)('You may need to restart Alacritty for changes to take effect')}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`;
|
||||
await writeFile(configPath, updatedContent, { encoding: 'utf-8' })
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)('Installed Alacritty Shift+Enter key binding')}${EOL}${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
'You may need to restart Alacritty for changes to take effect',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(configPath)}`)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
throw new Error('Failed to install Alacritty Shift+Enter key binding');
|
||||
logError(error)
|
||||
throw new Error('Failed to install Alacritty Shift+Enter key binding')
|
||||
}
|
||||
}
|
||||
|
||||
async function installBindingsForZed(theme: ThemeName): Promise<string> {
|
||||
// Zed uses JSON keybindings similar to VSCode
|
||||
const zedDir = join(homedir(), '.config', 'zed');
|
||||
const keymapPath = join(zedDir, 'keymap.json');
|
||||
const zedDir = join(homedir(), '.config', 'zed')
|
||||
const keymapPath = join(zedDir, 'keymap.json')
|
||||
|
||||
try {
|
||||
// Ensure zed directory exists (idempotent with recursive)
|
||||
await mkdir(zedDir, {
|
||||
recursive: true
|
||||
});
|
||||
await mkdir(zedDir, { recursive: true })
|
||||
|
||||
// Read existing keymap file, or default to empty array if it doesn't exist
|
||||
let keymapContent = '[]';
|
||||
let fileExists = false;
|
||||
let keymapContent = '[]'
|
||||
let fileExists = false
|
||||
try {
|
||||
keymapContent = await readFile(keymapPath, {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
fileExists = true;
|
||||
keymapContent = await readFile(keymapPath, { encoding: 'utf-8' })
|
||||
fileExists = true
|
||||
} catch (e: unknown) {
|
||||
if (!isFsInaccessible(e)) throw e;
|
||||
if (!isFsInaccessible(e)) throw e
|
||||
}
|
||||
|
||||
if (fileExists) {
|
||||
// Check if keybinding already exists
|
||||
if (keymapContent.includes('shift-enter')) {
|
||||
return `${color('warning', theme)('Found existing Zed Shift+Enter key binding. Remove it to continue.')}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Found existing Zed Shift+Enter key binding. Remove it to continue.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`
|
||||
}
|
||||
|
||||
// Create backup
|
||||
const randomSha = randomBytes(4).toString('hex');
|
||||
const backupPath = `${keymapPath}.${randomSha}.bak`;
|
||||
const randomSha = randomBytes(4).toString('hex')
|
||||
const backupPath = `${keymapPath}.${randomSha}.bak`
|
||||
try {
|
||||
await copyFile(keymapPath, backupPath);
|
||||
await copyFile(keymapPath, backupPath)
|
||||
} catch {
|
||||
return `${color('warning', theme)('Error backing up existing Zed keymap. Bailing out.')}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`;
|
||||
return `${color(
|
||||
'warning',
|
||||
theme,
|
||||
)(
|
||||
'Error backing up existing Zed keymap. Bailing out.',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}${chalk.dim(`Backup path: ${formatPathLink(backupPath)}`)}${EOL}`
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and modify the keymap
|
||||
let keymap: Array<{
|
||||
context?: string;
|
||||
bindings: Record<string, string | string[]>;
|
||||
}>;
|
||||
context?: string
|
||||
bindings: Record<string, string | string[]>
|
||||
}>
|
||||
try {
|
||||
keymap = jsonParse(keymapContent);
|
||||
keymap = jsonParse(keymapContent)
|
||||
if (!Array.isArray(keymap)) {
|
||||
keymap = [];
|
||||
keymap = []
|
||||
}
|
||||
} catch {
|
||||
keymap = [];
|
||||
keymap = []
|
||||
}
|
||||
|
||||
// Add the new keybinding for terminal context
|
||||
keymap.push({
|
||||
context: 'Terminal',
|
||||
bindings: {
|
||||
'shift-enter': ['terminal::SendText', '\u001b\r']
|
||||
}
|
||||
});
|
||||
'shift-enter': ['terminal::SendText', '\u001b\r'],
|
||||
},
|
||||
})
|
||||
|
||||
// Write the updated keymap
|
||||
await writeFile(keymapPath, jsonStringify(keymap, null, 2) + '\n', {
|
||||
encoding: 'utf-8'
|
||||
});
|
||||
return `${color('success', theme)('Installed Zed Shift+Enter key binding')}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`;
|
||||
encoding: 'utf-8',
|
||||
})
|
||||
|
||||
return `${color(
|
||||
'success',
|
||||
theme,
|
||||
)(
|
||||
'Installed Zed Shift+Enter key binding',
|
||||
)}${EOL}${chalk.dim(`See ${formatPathLink(keymapPath)}`)}${EOL}`
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
throw new Error('Failed to install Zed Shift+Enter key binding');
|
||||
logError(error)
|
||||
throw new Error('Failed to install Zed Shift+Enter key binding')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +1,36 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import * as React from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Pane } from '../../components/design-system/Pane.js';
|
||||
import { ThemePicker } from '../../components/ThemePicker.js';
|
||||
import { useTheme } from '../../ink.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Pane } from '../../components/design-system/Pane.js'
|
||||
import { ThemePicker } from '../../components/ThemePicker.js'
|
||||
import { useTheme } from '../../ink.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
type Props = {
|
||||
onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
}) => void;
|
||||
};
|
||||
function ThemePickerCommand(t0) {
|
||||
const $ = _c(8);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const [, setTheme] = useTheme();
|
||||
let t1;
|
||||
if ($[0] !== onDone || $[1] !== setTheme) {
|
||||
t1 = setting => {
|
||||
setTheme(setting);
|
||||
onDone(`Theme set to ${setting}`);
|
||||
};
|
||||
$[0] = onDone;
|
||||
$[1] = setTheme;
|
||||
$[2] = t1;
|
||||
} else {
|
||||
t1 = $[2];
|
||||
}
|
||||
let t2;
|
||||
if ($[3] !== onDone) {
|
||||
t2 = () => {
|
||||
onDone("Theme picker dismissed", {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[3] = onDone;
|
||||
$[4] = t2;
|
||||
} else {
|
||||
t2 = $[4];
|
||||
}
|
||||
let t3;
|
||||
if ($[5] !== t1 || $[6] !== t2) {
|
||||
t3 = <Pane color="permission"><ThemePicker onThemeSelect={t1} onCancel={t2} skipExitHandling={true} /></Pane>;
|
||||
$[5] = t1;
|
||||
$[6] = t2;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
return t3;
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay },
|
||||
) => void
|
||||
}
|
||||
|
||||
function ThemePickerCommand({ onDone }: Props): React.ReactNode {
|
||||
const [, setTheme] = useTheme()
|
||||
|
||||
return (
|
||||
<Pane color="permission">
|
||||
<ThemePicker
|
||||
onThemeSelect={setting => {
|
||||
setTheme(setting)
|
||||
onDone(`Theme set to ${setting}`)
|
||||
}}
|
||||
onCancel={() => {
|
||||
onDone('Theme picker dismissed', { display: 'system' })
|
||||
}}
|
||||
skipExitHandling={true}
|
||||
/>
|
||||
</Pane>
|
||||
)
|
||||
}
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, _context) => {
|
||||
return <ThemePickerCommand onDone={onDone} />;
|
||||
};
|
||||
return <ThemePickerCommand onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,67 +1,84 @@
|
||||
import { c as _c } from "react/compiler-runtime";
|
||||
import { execa } from 'execa';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import type { CommandResultDisplay } from '../../commands.js';
|
||||
import { Select } from '../../components/CustomSelect/select.js';
|
||||
import { Dialog } from '../../components/design-system/Dialog.js';
|
||||
import { Spinner } from '../../components/Spinner.js';
|
||||
import instances from '../../ink/instances.js';
|
||||
import { Box, Text } from '../../ink.js';
|
||||
import { enablePluginOp } from '../../services/plugins/pluginOperations.js';
|
||||
import { logForDebugging } from '../../utils/debug.js';
|
||||
import { isENOENT, toError } from '../../utils/errors.js';
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js';
|
||||
import { pathExists } from '../../utils/file.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { getPlatform } from '../../utils/platform.js';
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js';
|
||||
import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js';
|
||||
import { addMarketplaceSource, clearMarketplacesCache, loadKnownMarketplacesConfig, refreshMarketplace } from '../../utils/plugins/marketplaceManager.js';
|
||||
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js';
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js';
|
||||
import { installSelectedPlugins } from '../../utils/plugins/pluginStartupCheck.js';
|
||||
import { execa } from 'execa'
|
||||
import { readFile } from 'fs/promises'
|
||||
import { join } from 'path'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import type { CommandResultDisplay } from '../../commands.js'
|
||||
import { Select } from '../../components/CustomSelect/select.js'
|
||||
import { Dialog } from '../../components/design-system/Dialog.js'
|
||||
import { Spinner } from '../../components/Spinner.js'
|
||||
import instances from '../../ink/instances.js'
|
||||
import { Box, Text } from '../../ink.js'
|
||||
import { enablePluginOp } from '../../services/plugins/pluginOperations.js'
|
||||
import { logForDebugging } from '../../utils/debug.js'
|
||||
import { isENOENT, toError } from '../../utils/errors.js'
|
||||
import { execFileNoThrow } from '../../utils/execFileNoThrow.js'
|
||||
import { pathExists } from '../../utils/file.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { getPlatform } from '../../utils/platform.js'
|
||||
import { clearAllCaches } from '../../utils/plugins/cacheUtils.js'
|
||||
import { isPluginInstalled } from '../../utils/plugins/installedPluginsManager.js'
|
||||
import {
|
||||
addMarketplaceSource,
|
||||
clearMarketplacesCache,
|
||||
loadKnownMarketplacesConfig,
|
||||
refreshMarketplace,
|
||||
} from '../../utils/plugins/marketplaceManager.js'
|
||||
import { OFFICIAL_MARKETPLACE_NAME } from '../../utils/plugins/officialMarketplace.js'
|
||||
import { loadAllPlugins } from '../../utils/plugins/pluginLoader.js'
|
||||
import { installSelectedPlugins } from '../../utils/plugins/pluginStartupCheck.js'
|
||||
|
||||
// Marketplace and plugin identifiers - varies by user type
|
||||
const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace';
|
||||
const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace';
|
||||
const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official';
|
||||
const INTERNAL_MARKETPLACE_NAME = 'claude-code-marketplace'
|
||||
const INTERNAL_MARKETPLACE_REPO = 'anthropics/claude-code-marketplace'
|
||||
const OFFICIAL_MARKETPLACE_REPO = 'anthropics/claude-plugins-official'
|
||||
|
||||
function getMarketplaceName(): string {
|
||||
return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_NAME : OFFICIAL_MARKETPLACE_NAME;
|
||||
return process.env.USER_TYPE === 'ant'
|
||||
? INTERNAL_MARKETPLACE_NAME
|
||||
: OFFICIAL_MARKETPLACE_NAME
|
||||
}
|
||||
|
||||
function getMarketplaceRepo(): string {
|
||||
return (process.env.USER_TYPE) === 'ant' ? INTERNAL_MARKETPLACE_REPO : OFFICIAL_MARKETPLACE_REPO;
|
||||
return process.env.USER_TYPE === 'ant'
|
||||
? INTERNAL_MARKETPLACE_REPO
|
||||
: OFFICIAL_MARKETPLACE_REPO
|
||||
}
|
||||
|
||||
function getPluginId(): string {
|
||||
return `thinkback@${getMarketplaceName()}`;
|
||||
return `thinkback@${getMarketplaceName()}`
|
||||
}
|
||||
const SKILL_NAME = 'thinkback';
|
||||
|
||||
const SKILL_NAME = 'thinkback'
|
||||
|
||||
/**
|
||||
* Get the thinkback skill directory from the installed plugin's cache path
|
||||
*/
|
||||
async function getThinkbackSkillDir(): Promise<string | null> {
|
||||
const {
|
||||
enabled
|
||||
} = await loadAllPlugins();
|
||||
const thinkbackPlugin = enabled.find(p => p.name === 'thinkback' || p.source && p.source.includes(getPluginId()));
|
||||
const { enabled } = await loadAllPlugins()
|
||||
const thinkbackPlugin = enabled.find(
|
||||
p =>
|
||||
p.name === 'thinkback' || (p.source && p.source.includes(getPluginId())),
|
||||
)
|
||||
|
||||
if (!thinkbackPlugin) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const skillDir = join(thinkbackPlugin.path, 'skills', SKILL_NAME);
|
||||
|
||||
const skillDir = join(thinkbackPlugin.path, 'skills', SKILL_NAME)
|
||||
if (await pathExists(skillDir)) {
|
||||
return skillDir;
|
||||
return skillDir
|
||||
}
|
||||
return null;
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export async function playAnimation(skillDir: string): Promise<{
|
||||
success: boolean;
|
||||
message: string;
|
||||
success: boolean
|
||||
message: string
|
||||
}> {
|
||||
const dataPath = join(skillDir, 'year_in_review.js');
|
||||
const playerPath = join(skillDir, 'player.js');
|
||||
const dataPath = join(skillDir, 'year_in_review.js')
|
||||
const playerPath = join(skillDir, 'player.js')
|
||||
|
||||
// Both files are prerequisites for the node subprocess. Read them here
|
||||
// (not at call sites) so all callers get consistent error messaging. The
|
||||
@@ -72,482 +89,433 @@ export async function playAnimation(skillDir: string): Promise<{
|
||||
// than thrown — the old pathExists-based code never threw, and one caller
|
||||
// (handleSelect) uses `void playAnimation().then(...)` without a .catch().
|
||||
try {
|
||||
await readFile(dataPath);
|
||||
await readFile(dataPath)
|
||||
} catch (e: unknown) {
|
||||
if (isENOENT(e)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'No animation found. Run /think-back first to generate one.'
|
||||
};
|
||||
message: 'No animation found. Run /think-back first to generate one.',
|
||||
}
|
||||
}
|
||||
logError(e);
|
||||
logError(e)
|
||||
return {
|
||||
success: false,
|
||||
message: `Could not access animation data: ${toError(e).message}`
|
||||
};
|
||||
message: `Could not access animation data: ${toError(e).message}`,
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await readFile(playerPath);
|
||||
await readFile(playerPath)
|
||||
} catch (e: unknown) {
|
||||
if (isENOENT(e)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Player script not found. The player.js file is missing from the thinkback skill.'
|
||||
};
|
||||
message:
|
||||
'Player script not found. The player.js file is missing from the thinkback skill.',
|
||||
}
|
||||
}
|
||||
logError(e);
|
||||
logError(e)
|
||||
return {
|
||||
success: false,
|
||||
message: `Could not access player script: ${toError(e).message}`
|
||||
};
|
||||
message: `Could not access player script: ${toError(e).message}`,
|
||||
}
|
||||
}
|
||||
|
||||
// Get ink instance for terminal takeover
|
||||
const inkInstance = instances.get(process.stdout);
|
||||
const inkInstance = instances.get(process.stdout)
|
||||
if (!inkInstance) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to access terminal instance'
|
||||
};
|
||||
return { success: false, message: 'Failed to access terminal instance' }
|
||||
}
|
||||
inkInstance.enterAlternateScreen();
|
||||
|
||||
inkInstance.enterAlternateScreen()
|
||||
try {
|
||||
await execa('node', [playerPath], {
|
||||
stdio: 'inherit',
|
||||
cwd: skillDir,
|
||||
reject: false
|
||||
});
|
||||
reject: false,
|
||||
})
|
||||
} catch {
|
||||
// Animation may have been interrupted (e.g., Ctrl+C)
|
||||
} finally {
|
||||
inkInstance.exitAlternateScreen();
|
||||
inkInstance.exitAlternateScreen()
|
||||
}
|
||||
|
||||
// Open the HTML file in browser for video download
|
||||
const htmlPath = join(skillDir, 'year_in_review.html');
|
||||
const htmlPath = join(skillDir, 'year_in_review.html')
|
||||
if (await pathExists(htmlPath)) {
|
||||
const platform = getPlatform();
|
||||
const openCmd = platform === 'macos' ? 'open' : platform === 'windows' ? 'start' : 'xdg-open';
|
||||
void execFileNoThrow(openCmd, [htmlPath]);
|
||||
const platform = getPlatform()
|
||||
const openCmd =
|
||||
platform === 'macos'
|
||||
? 'open'
|
||||
: platform === 'windows'
|
||||
? 'start'
|
||||
: 'xdg-open'
|
||||
void execFileNoThrow(openCmd, [htmlPath])
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
message: 'Year in review animation complete!'
|
||||
};
|
||||
|
||||
return { success: true, message: 'Year in review animation complete!' }
|
||||
}
|
||||
type InstallState = {
|
||||
phase: 'checking';
|
||||
} | {
|
||||
phase: 'installing-marketplace';
|
||||
} | {
|
||||
phase: 'installing-plugin';
|
||||
} | {
|
||||
phase: 'enabling-plugin';
|
||||
} | {
|
||||
phase: 'ready';
|
||||
} | {
|
||||
phase: 'error';
|
||||
message: string;
|
||||
};
|
||||
|
||||
type InstallState =
|
||||
| { phase: 'checking' }
|
||||
| { phase: 'installing-marketplace' }
|
||||
| { phase: 'installing-plugin' }
|
||||
| { phase: 'enabling-plugin' }
|
||||
| { phase: 'ready' }
|
||||
| { phase: 'error'; message: string }
|
||||
|
||||
function ThinkbackInstaller({
|
||||
onReady,
|
||||
onError
|
||||
onError,
|
||||
}: {
|
||||
onReady: () => void;
|
||||
onError: (message: string) => void;
|
||||
onReady: () => void
|
||||
onError: (message: string) => void
|
||||
}): React.ReactNode {
|
||||
const [state, setState] = useState<InstallState>({
|
||||
phase: 'checking'
|
||||
});
|
||||
const [progressMessage, setProgressMessage] = useState('');
|
||||
const [state, setState] = useState<InstallState>({ phase: 'checking' })
|
||||
const [progressMessage, setProgressMessage] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
async function checkAndInstall(): Promise<void> {
|
||||
try {
|
||||
// Check if marketplace is installed
|
||||
const knownMarketplaces = await loadKnownMarketplacesConfig();
|
||||
const marketplaceName = getMarketplaceName();
|
||||
const marketplaceRepo = getMarketplaceRepo();
|
||||
const pluginId = getPluginId();
|
||||
const marketplaceInstalled = marketplaceName in knownMarketplaces;
|
||||
const knownMarketplaces = await loadKnownMarketplacesConfig()
|
||||
const marketplaceName = getMarketplaceName()
|
||||
const marketplaceRepo = getMarketplaceRepo()
|
||||
const pluginId = getPluginId()
|
||||
const marketplaceInstalled = marketplaceName in knownMarketplaces
|
||||
|
||||
// Check if plugin is already installed first
|
||||
const pluginAlreadyInstalled = isPluginInstalled(pluginId);
|
||||
const pluginAlreadyInstalled = isPluginInstalled(pluginId)
|
||||
|
||||
if (!marketplaceInstalled) {
|
||||
// Install the marketplace
|
||||
setState({
|
||||
phase: 'installing-marketplace'
|
||||
});
|
||||
logForDebugging(`Installing marketplace ${marketplaceRepo}`);
|
||||
await addMarketplaceSource({
|
||||
source: 'github',
|
||||
repo: marketplaceRepo
|
||||
}, message => {
|
||||
setProgressMessage(message);
|
||||
});
|
||||
clearAllCaches();
|
||||
logForDebugging(`Marketplace ${marketplaceName} installed`);
|
||||
setState({ phase: 'installing-marketplace' })
|
||||
logForDebugging(`Installing marketplace ${marketplaceRepo}`)
|
||||
|
||||
await addMarketplaceSource(
|
||||
{ source: 'github', repo: marketplaceRepo },
|
||||
message => {
|
||||
setProgressMessage(message)
|
||||
},
|
||||
)
|
||||
clearAllCaches()
|
||||
logForDebugging(`Marketplace ${marketplaceName} installed`)
|
||||
} else if (!pluginAlreadyInstalled) {
|
||||
// Marketplace installed but plugin not installed - refresh to get latest plugins
|
||||
// Only refresh when needed to avoid potentially destructive git operations
|
||||
setState({
|
||||
phase: 'installing-marketplace'
|
||||
});
|
||||
setProgressMessage('Updating marketplace…');
|
||||
logForDebugging(`Refreshing marketplace ${marketplaceName}`);
|
||||
await refreshMarketplace(marketplaceName, message_0 => {
|
||||
setProgressMessage(message_0);
|
||||
});
|
||||
clearMarketplacesCache();
|
||||
clearAllCaches();
|
||||
logForDebugging(`Marketplace ${marketplaceName} refreshed`);
|
||||
setState({ phase: 'installing-marketplace' })
|
||||
setProgressMessage('Updating marketplace…')
|
||||
logForDebugging(`Refreshing marketplace ${marketplaceName}`)
|
||||
|
||||
await refreshMarketplace(marketplaceName, message => {
|
||||
setProgressMessage(message)
|
||||
})
|
||||
clearMarketplacesCache()
|
||||
clearAllCaches()
|
||||
logForDebugging(`Marketplace ${marketplaceName} refreshed`)
|
||||
}
|
||||
|
||||
if (!pluginAlreadyInstalled) {
|
||||
// Install the plugin
|
||||
setState({
|
||||
phase: 'installing-plugin'
|
||||
});
|
||||
logForDebugging(`Installing plugin ${pluginId}`);
|
||||
const result = await installSelectedPlugins([pluginId]);
|
||||
setState({ phase: 'installing-plugin' })
|
||||
logForDebugging(`Installing plugin ${pluginId}`)
|
||||
|
||||
const result = await installSelectedPlugins([pluginId])
|
||||
|
||||
if (result.failed.length > 0) {
|
||||
const errorMsg = result.failed.map(f => `${f.name}: ${f.error}`).join(', ');
|
||||
throw new Error(`Failed to install plugin: ${errorMsg}`);
|
||||
const errorMsg = result.failed
|
||||
.map(f => `${f.name}: ${f.error}`)
|
||||
.join(', ')
|
||||
throw new Error(`Failed to install plugin: ${errorMsg}`)
|
||||
}
|
||||
clearAllCaches();
|
||||
logForDebugging(`Plugin ${pluginId} installed`);
|
||||
|
||||
clearAllCaches()
|
||||
logForDebugging(`Plugin ${pluginId} installed`)
|
||||
} else {
|
||||
// Plugin is installed, check if it's enabled
|
||||
const {
|
||||
disabled
|
||||
} = await loadAllPlugins();
|
||||
const isDisabled = disabled.some(p => p.name === 'thinkback' || p.source?.includes(pluginId));
|
||||
const { disabled } = await loadAllPlugins()
|
||||
const isDisabled = disabled.some(
|
||||
p => p.name === 'thinkback' || p.source?.includes(pluginId),
|
||||
)
|
||||
|
||||
if (isDisabled) {
|
||||
// Enable the plugin
|
||||
setState({
|
||||
phase: 'enabling-plugin'
|
||||
});
|
||||
logForDebugging(`Enabling plugin ${pluginId}`);
|
||||
const enableResult = await enablePluginOp(pluginId);
|
||||
setState({ phase: 'enabling-plugin' })
|
||||
logForDebugging(`Enabling plugin ${pluginId}`)
|
||||
|
||||
const enableResult = await enablePluginOp(pluginId)
|
||||
if (!enableResult.success) {
|
||||
throw new Error(`Failed to enable plugin: ${enableResult.message}`);
|
||||
throw new Error(
|
||||
`Failed to enable plugin: ${enableResult.message}`,
|
||||
)
|
||||
}
|
||||
clearAllCaches();
|
||||
logForDebugging(`Plugin ${pluginId} enabled`);
|
||||
|
||||
clearAllCaches()
|
||||
logForDebugging(`Plugin ${pluginId} enabled`)
|
||||
}
|
||||
}
|
||||
setState({
|
||||
phase: 'ready'
|
||||
});
|
||||
onReady();
|
||||
|
||||
setState({ phase: 'ready' })
|
||||
onReady()
|
||||
} catch (error) {
|
||||
const err = toError(error);
|
||||
logError(err);
|
||||
setState({
|
||||
phase: 'error',
|
||||
message: err.message
|
||||
});
|
||||
onError(err.message);
|
||||
const err = toError(error)
|
||||
logError(err)
|
||||
setState({ phase: 'error', message: err.message })
|
||||
onError(err.message)
|
||||
}
|
||||
}
|
||||
void checkAndInstall();
|
||||
}, [onReady, onError]);
|
||||
|
||||
void checkAndInstall()
|
||||
}, [onReady, onError])
|
||||
|
||||
if (state.phase === 'error') {
|
||||
return <Box flexDirection="column">
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">Error: {state.message}</Text>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (state.phase === 'ready') {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
const statusMessage = state.phase === 'checking' ? 'Checking thinkback installation…' : state.phase === 'installing-marketplace' ? 'Installing marketplace…' : state.phase === 'enabling-plugin' ? 'Enabling thinkback plugin…' : 'Installing thinkback plugin…';
|
||||
return <Box flexDirection="column">
|
||||
|
||||
const statusMessage =
|
||||
state.phase === 'checking'
|
||||
? 'Checking thinkback installation…'
|
||||
: state.phase === 'installing-marketplace'
|
||||
? 'Installing marketplace…'
|
||||
: state.phase === 'enabling-plugin'
|
||||
? 'Enabling thinkback plugin…'
|
||||
: 'Installing thinkback plugin…'
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>{progressMessage || statusMessage}</Text>
|
||||
</Box>
|
||||
</Box>;
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
type MenuAction = 'play' | 'edit' | 'fix' | 'regenerate';
|
||||
type GenerativeAction = Exclude<MenuAction, 'play'>;
|
||||
function ThinkbackMenu(t0) {
|
||||
const $ = _c(19);
|
||||
const {
|
||||
onDone,
|
||||
onAction,
|
||||
skillDir,
|
||||
hasGenerated
|
||||
} = t0;
|
||||
const [hasSelected, setHasSelected] = useState(false);
|
||||
let t1;
|
||||
if ($[0] !== hasGenerated) {
|
||||
t1 = hasGenerated ? [{
|
||||
label: "Play animation",
|
||||
value: "play" as const,
|
||||
description: "Watch your year in review"
|
||||
}, {
|
||||
label: "Edit content",
|
||||
value: "edit" as const,
|
||||
description: "Modify the animation"
|
||||
}, {
|
||||
label: "Fix errors",
|
||||
value: "fix" as const,
|
||||
description: "Fix validation or rendering issues"
|
||||
}, {
|
||||
label: "Regenerate",
|
||||
value: "regenerate" as const,
|
||||
description: "Create a new animation from scratch"
|
||||
}] : [{
|
||||
label: "Let's go!",
|
||||
value: "regenerate" as const,
|
||||
description: "Generate your personalized animation"
|
||||
}];
|
||||
$[0] = hasGenerated;
|
||||
$[1] = t1;
|
||||
} else {
|
||||
t1 = $[1];
|
||||
|
||||
type MenuAction = 'play' | 'edit' | 'fix' | 'regenerate'
|
||||
type GenerativeAction = Exclude<MenuAction, 'play'>
|
||||
|
||||
function ThinkbackMenu({
|
||||
onDone,
|
||||
onAction,
|
||||
skillDir,
|
||||
hasGenerated,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
||||
) => void
|
||||
onAction: (action: GenerativeAction) => void
|
||||
skillDir: string
|
||||
hasGenerated: boolean
|
||||
}): React.ReactNode {
|
||||
const [hasSelected, setHasSelected] = useState(false)
|
||||
|
||||
const options = hasGenerated
|
||||
? [
|
||||
{
|
||||
label: 'Play animation',
|
||||
value: 'play' as const,
|
||||
description: 'Watch your year in review',
|
||||
},
|
||||
{
|
||||
label: 'Edit content',
|
||||
value: 'edit' as const,
|
||||
description: 'Modify the animation',
|
||||
},
|
||||
{
|
||||
label: 'Fix errors',
|
||||
value: 'fix' as const,
|
||||
description: 'Fix validation or rendering issues',
|
||||
},
|
||||
{
|
||||
label: 'Regenerate',
|
||||
value: 'regenerate' as const,
|
||||
description: 'Create a new animation from scratch',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: "Let's go!",
|
||||
value: 'regenerate' as const,
|
||||
description: 'Generate your personalized animation',
|
||||
},
|
||||
]
|
||||
|
||||
function handleSelect(value: MenuAction): void {
|
||||
setHasSelected(true)
|
||||
if (value === 'play') {
|
||||
// Play runs the terminal-takeover animation, then signal done with skip
|
||||
void playAnimation(skillDir).then(() => {
|
||||
onDone(undefined, { display: 'skip' })
|
||||
})
|
||||
} else {
|
||||
onAction(value)
|
||||
}
|
||||
}
|
||||
const options = t1;
|
||||
let t2;
|
||||
if ($[2] !== onAction || $[3] !== onDone || $[4] !== skillDir) {
|
||||
t2 = function handleSelect(value) {
|
||||
setHasSelected(true);
|
||||
if (value === "play") {
|
||||
playAnimation(skillDir).then(() => {
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
});
|
||||
} else {
|
||||
onAction(value);
|
||||
}
|
||||
};
|
||||
$[2] = onAction;
|
||||
$[3] = onDone;
|
||||
$[4] = skillDir;
|
||||
$[5] = t2;
|
||||
} else {
|
||||
t2 = $[5];
|
||||
|
||||
function handleCancel(): void {
|
||||
onDone(undefined, { display: 'skip' })
|
||||
}
|
||||
const handleSelect = t2;
|
||||
let t3;
|
||||
if ($[6] !== onDone) {
|
||||
t3 = function handleCancel() {
|
||||
onDone(undefined, {
|
||||
display: "skip"
|
||||
});
|
||||
};
|
||||
$[6] = onDone;
|
||||
$[7] = t3;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
}
|
||||
const handleCancel = t3;
|
||||
|
||||
if (hasSelected) {
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
let t4;
|
||||
if ($[8] !== hasGenerated) {
|
||||
t4 = !hasGenerated && <Box flexDirection="column"><Text>Relive your year of coding with Claude.</Text><Text dimColor={true}>{"We'll create a personalized ASCII animation celebrating your journey."}</Text></Box>;
|
||||
$[8] = hasGenerated;
|
||||
$[9] = t4;
|
||||
} else {
|
||||
t4 = $[9];
|
||||
}
|
||||
let t5;
|
||||
if ($[10] !== handleSelect || $[11] !== options) {
|
||||
t5 = <Select options={options} onChange={handleSelect} visibleOptionCount={5} />;
|
||||
$[10] = handleSelect;
|
||||
$[11] = options;
|
||||
$[12] = t5;
|
||||
} else {
|
||||
t5 = $[12];
|
||||
}
|
||||
let t6;
|
||||
if ($[13] !== t4 || $[14] !== t5) {
|
||||
t6 = <Box flexDirection="column" gap={1}>{t4}{t5}</Box>;
|
||||
$[13] = t4;
|
||||
$[14] = t5;
|
||||
$[15] = t6;
|
||||
} else {
|
||||
t6 = $[15];
|
||||
}
|
||||
let t7;
|
||||
if ($[16] !== handleCancel || $[17] !== t6) {
|
||||
t7 = <Dialog title="Think Back on 2025 with Claude Code" subtitle="Generate your 2025 Claude Code Think Back (takes a few minutes to run)" onCancel={handleCancel} color="claude">{t6}</Dialog>;
|
||||
$[16] = handleCancel;
|
||||
$[17] = t6;
|
||||
$[18] = t7;
|
||||
} else {
|
||||
t7 = $[18];
|
||||
}
|
||||
return t7;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
title="Think Back on 2025 with Claude Code"
|
||||
subtitle="Generate your 2025 Claude Code Think Back (takes a few minutes to run)"
|
||||
onCancel={handleCancel}
|
||||
color="claude"
|
||||
>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{/* Description for first-time users */}
|
||||
{!hasGenerated && (
|
||||
<Box flexDirection="column">
|
||||
<Text>Relive your year of coding with Claude.</Text>
|
||||
<Text dimColor>
|
||||
{
|
||||
"We'll create a personalized ASCII animation celebrating your journey."
|
||||
}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Menu */}
|
||||
<Select
|
||||
options={options}
|
||||
onChange={handleSelect}
|
||||
visibleOptionCount={5}
|
||||
/>
|
||||
</Box>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
const EDIT_PROMPT = 'Use the Skill tool to invoke the "thinkback" skill with mode=edit to modify my existing Claude Code year in review animation. Ask me what I want to change. When the animation is ready, tell the user to run /think-back again to play it.';
|
||||
const FIX_PROMPT = 'Use the Skill tool to invoke the "thinkback" skill with mode=fix to fix validation or rendering errors in my existing Claude Code year in review animation. Run the validator, identify errors, and fix them. When the animation is ready, tell the user to run /think-back again to play it.';
|
||||
const REGENERATE_PROMPT = 'Use the Skill tool to invoke the "thinkback" skill with mode=regenerate to create a completely new Claude Code year in review animation from scratch. Delete the existing animation and start fresh. When the animation is ready, tell the user to run /think-back again to play it.';
|
||||
function ThinkbackFlow(t0) {
|
||||
const $ = _c(27);
|
||||
const {
|
||||
onDone
|
||||
} = t0;
|
||||
const [installComplete, setInstallComplete] = useState(false);
|
||||
const [installError, setInstallError] = useState(null);
|
||||
const [skillDir, setSkillDir] = useState(null);
|
||||
const [hasGenerated, setHasGenerated] = useState(null);
|
||||
let t1;
|
||||
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t1 = function handleReady() {
|
||||
setInstallComplete(true);
|
||||
};
|
||||
$[0] = t1;
|
||||
} else {
|
||||
t1 = $[0];
|
||||
|
||||
const EDIT_PROMPT =
|
||||
'Use the Skill tool to invoke the "thinkback" skill with mode=edit to modify my existing Claude Code year in review animation. Ask me what I want to change. When the animation is ready, tell the user to run /think-back again to play it.'
|
||||
|
||||
const FIX_PROMPT =
|
||||
'Use the Skill tool to invoke the "thinkback" skill with mode=fix to fix validation or rendering errors in my existing Claude Code year in review animation. Run the validator, identify errors, and fix them. When the animation is ready, tell the user to run /think-back again to play it.'
|
||||
|
||||
const REGENERATE_PROMPT =
|
||||
'Use the Skill tool to invoke the "thinkback" skill with mode=regenerate to create a completely new Claude Code year in review animation from scratch. Delete the existing animation and start fresh. When the animation is ready, tell the user to run /think-back again to play it.'
|
||||
|
||||
function ThinkbackFlow({
|
||||
onDone,
|
||||
}: {
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
||||
) => void
|
||||
}): React.ReactNode {
|
||||
const [installComplete, setInstallComplete] = useState(false)
|
||||
const [installError, setInstallError] = useState<string | null>(null)
|
||||
const [skillDir, setSkillDir] = useState<string | null>(null)
|
||||
const [hasGenerated, setHasGenerated] = useState<boolean | null>(null)
|
||||
|
||||
function handleReady(): void {
|
||||
setInstallComplete(true)
|
||||
}
|
||||
const handleReady = t1;
|
||||
let t2;
|
||||
if ($[1] !== onDone) {
|
||||
t2 = message => {
|
||||
setInstallError(message);
|
||||
onDone(`Error with thinkback: ${message}. Try running /plugin to manually install the think-back plugin.`, {
|
||||
display: "system"
|
||||
});
|
||||
};
|
||||
$[1] = onDone;
|
||||
$[2] = t2;
|
||||
} else {
|
||||
t2 = $[2];
|
||||
|
||||
const handleError = useCallback(
|
||||
(message: string): void => {
|
||||
setInstallError(message)
|
||||
// Call onDone with the error message so the model can continue
|
||||
onDone(
|
||||
`Error with thinkback: ${message}. Try running /plugin to manually install the think-back plugin.`,
|
||||
{ display: 'system' },
|
||||
)
|
||||
},
|
||||
[onDone],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (installComplete && !skillDir && !installError) {
|
||||
// Get the skill directory after installation
|
||||
void getThinkbackSkillDir().then(dir => {
|
||||
if (dir) {
|
||||
logForDebugging(`Thinkback skill directory: ${dir}`)
|
||||
setSkillDir(dir)
|
||||
} else {
|
||||
handleError('Could not find thinkback skill directory')
|
||||
}
|
||||
})
|
||||
}
|
||||
}, [installComplete, skillDir, installError, handleError])
|
||||
|
||||
// Check for generated file once we have skillDir
|
||||
useEffect(() => {
|
||||
if (!skillDir) {
|
||||
return
|
||||
}
|
||||
|
||||
const dataPath = join(skillDir, 'year_in_review.js')
|
||||
void pathExists(dataPath).then(exists => {
|
||||
logForDebugging(
|
||||
`Checking for ${dataPath}: ${exists ? 'found' : 'not found'}`,
|
||||
)
|
||||
setHasGenerated(exists)
|
||||
})
|
||||
}, [skillDir])
|
||||
|
||||
function handleAction(action: GenerativeAction): void {
|
||||
// Send prompt to model based on action
|
||||
const prompts: Record<GenerativeAction, string> = {
|
||||
edit: EDIT_PROMPT,
|
||||
fix: FIX_PROMPT,
|
||||
regenerate: REGENERATE_PROMPT,
|
||||
}
|
||||
onDone(prompts[action], { display: 'user', shouldQuery: true })
|
||||
}
|
||||
const handleError = t2;
|
||||
let t3;
|
||||
let t4;
|
||||
if ($[3] !== handleError || $[4] !== installComplete || $[5] !== installError || $[6] !== skillDir) {
|
||||
t3 = () => {
|
||||
if (installComplete && !skillDir && !installError) {
|
||||
getThinkbackSkillDir().then(dir => {
|
||||
if (dir) {
|
||||
logForDebugging(`Thinkback skill directory: ${dir}`);
|
||||
setSkillDir(dir);
|
||||
} else {
|
||||
handleError("Could not find thinkback skill directory");
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
t4 = [installComplete, skillDir, installError, handleError];
|
||||
$[3] = handleError;
|
||||
$[4] = installComplete;
|
||||
$[5] = installError;
|
||||
$[6] = skillDir;
|
||||
$[7] = t3;
|
||||
$[8] = t4;
|
||||
} else {
|
||||
t3 = $[7];
|
||||
t4 = $[8];
|
||||
}
|
||||
useEffect(t3, t4);
|
||||
let t5;
|
||||
let t6;
|
||||
if ($[9] !== skillDir) {
|
||||
t5 = () => {
|
||||
if (!skillDir) {
|
||||
return;
|
||||
}
|
||||
const dataPath = join(skillDir, "year_in_review.js");
|
||||
pathExists(dataPath).then(exists => {
|
||||
logForDebugging(`Checking for ${dataPath}: ${exists ? "found" : "not found"}`);
|
||||
setHasGenerated(exists);
|
||||
});
|
||||
};
|
||||
t6 = [skillDir];
|
||||
$[9] = skillDir;
|
||||
$[10] = t5;
|
||||
$[11] = t6;
|
||||
} else {
|
||||
t5 = $[10];
|
||||
t6 = $[11];
|
||||
}
|
||||
useEffect(t5, t6);
|
||||
let t7;
|
||||
if ($[12] !== onDone) {
|
||||
t7 = function handleAction(action) {
|
||||
const prompts = {
|
||||
edit: EDIT_PROMPT,
|
||||
fix: FIX_PROMPT,
|
||||
regenerate: REGENERATE_PROMPT
|
||||
};
|
||||
onDone(prompts[action], {
|
||||
display: "user",
|
||||
shouldQuery: true
|
||||
});
|
||||
};
|
||||
$[12] = onDone;
|
||||
$[13] = t7;
|
||||
} else {
|
||||
t7 = $[13];
|
||||
}
|
||||
const handleAction = t7;
|
||||
|
||||
if (installError) {
|
||||
let t8;
|
||||
if ($[14] !== installError) {
|
||||
t8 = <Text color="error">Error: {installError}</Text>;
|
||||
$[14] = installError;
|
||||
$[15] = t8;
|
||||
} else {
|
||||
t8 = $[15];
|
||||
}
|
||||
let t9;
|
||||
if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t9 = <Text dimColor={true}>Try running /plugin to manually install the think-back plugin.</Text>;
|
||||
$[16] = t9;
|
||||
} else {
|
||||
t9 = $[16];
|
||||
}
|
||||
let t10;
|
||||
if ($[17] !== t8) {
|
||||
t10 = <Box flexDirection="column">{t8}{t9}</Box>;
|
||||
$[17] = t8;
|
||||
$[18] = t10;
|
||||
} else {
|
||||
t10 = $[18];
|
||||
}
|
||||
return t10;
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color="error">Error: {installError}</Text>
|
||||
<Text dimColor>
|
||||
Try running /plugin to manually install the think-back plugin.
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (!installComplete) {
|
||||
let t8;
|
||||
if ($[19] !== handleError) {
|
||||
t8 = <ThinkbackInstaller onReady={handleReady} onError={handleError} />;
|
||||
$[19] = handleError;
|
||||
$[20] = t8;
|
||||
} else {
|
||||
t8 = $[20];
|
||||
}
|
||||
return t8;
|
||||
return <ThinkbackInstaller onReady={handleReady} onError={handleError} />
|
||||
}
|
||||
|
||||
if (!skillDir || hasGenerated === null) {
|
||||
let t8;
|
||||
if ($[21] === Symbol.for("react.memo_cache_sentinel")) {
|
||||
t8 = <Box><Spinner /><Text>Loading thinkback skill…</Text></Box>;
|
||||
$[21] = t8;
|
||||
} else {
|
||||
t8 = $[21];
|
||||
}
|
||||
return t8;
|
||||
return (
|
||||
<Box>
|
||||
<Spinner />
|
||||
<Text>Loading thinkback skill…</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
let t8;
|
||||
if ($[22] !== handleAction || $[23] !== hasGenerated || $[24] !== onDone || $[25] !== skillDir) {
|
||||
t8 = <ThinkbackMenu onDone={onDone} onAction={handleAction} skillDir={skillDir} hasGenerated={hasGenerated} />;
|
||||
$[22] = handleAction;
|
||||
$[23] = hasGenerated;
|
||||
$[24] = onDone;
|
||||
$[25] = skillDir;
|
||||
$[26] = t8;
|
||||
} else {
|
||||
t8 = $[26];
|
||||
}
|
||||
return t8;
|
||||
|
||||
return (
|
||||
<ThinkbackMenu
|
||||
onDone={onDone}
|
||||
onAction={handleAction}
|
||||
skillDir={skillDir}
|
||||
hasGenerated={hasGenerated}
|
||||
/>
|
||||
)
|
||||
}
|
||||
export async function call(onDone: (result?: string, options?: {
|
||||
display?: CommandResultDisplay;
|
||||
shouldQuery?: boolean;
|
||||
}) => void): Promise<React.ReactNode> {
|
||||
return <ThinkbackFlow onDone={onDone} />;
|
||||
|
||||
export async function call(
|
||||
onDone: (
|
||||
result?: string,
|
||||
options?: { display?: CommandResultDisplay; shouldQuery?: boolean },
|
||||
) => void,
|
||||
): Promise<React.ReactNode> {
|
||||
return <ThinkbackFlow onDone={onDone} />
|
||||
}
|
||||
|
||||
@@ -1,28 +1,42 @@
|
||||
import { readFileSync } from 'fs';
|
||||
import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js';
|
||||
import type { Command } from '../commands.js';
|
||||
import { DIAMOND_OPEN } from '../constants/figures.js';
|
||||
import { getRemoteSessionUrl } from '../constants/product.js';
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js';
|
||||
import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent } from '../services/analytics/index.js';
|
||||
import type { AppState } from '../state/AppStateStore.js';
|
||||
import { checkRemoteAgentEligibility, formatPreconditionError, RemoteAgentTask, type RemoteAgentTaskState, registerRemoteAgentTask } from '../tasks/RemoteAgentTask/RemoteAgentTask.js';
|
||||
import type { LocalJSXCommandCall } from '../types/command.js';
|
||||
import { logForDebugging } from '../utils/debug.js';
|
||||
import { errorMessage } from '../utils/errors.js';
|
||||
import { logError } from '../utils/log.js';
|
||||
import { enqueuePendingNotification } from '../utils/messageQueueManager.js';
|
||||
import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js';
|
||||
import { updateTaskState } from '../utils/task/framework.js';
|
||||
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js';
|
||||
import { pollForApprovedExitPlanMode, UltraplanPollError } from '../utils/ultraplan/ccrSession.js';
|
||||
import { readFileSync } from 'fs'
|
||||
import { REMOTE_CONTROL_DISCONNECTED_MSG } from '../bridge/types.js'
|
||||
import type { Command } from '../commands.js'
|
||||
import { DIAMOND_OPEN } from '../constants/figures.js'
|
||||
import { getRemoteSessionUrl } from '../constants/product.js'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js'
|
||||
import {
|
||||
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
logEvent,
|
||||
} from '../services/analytics/index.js'
|
||||
import type { AppState } from '../state/AppStateStore.js'
|
||||
import {
|
||||
checkRemoteAgentEligibility,
|
||||
formatPreconditionError,
|
||||
RemoteAgentTask,
|
||||
type RemoteAgentTaskState,
|
||||
registerRemoteAgentTask,
|
||||
} from '../tasks/RemoteAgentTask/RemoteAgentTask.js'
|
||||
import type { LocalJSXCommandCall } from '../types/command.js'
|
||||
import { logForDebugging } from '../utils/debug.js'
|
||||
import { errorMessage } from '../utils/errors.js'
|
||||
import { logError } from '../utils/log.js'
|
||||
import { enqueuePendingNotification } from '../utils/messageQueueManager.js'
|
||||
import { ALL_MODEL_CONFIGS } from '../utils/model/configs.js'
|
||||
import { updateTaskState } from '../utils/task/framework.js'
|
||||
import { archiveRemoteSession, teleportToRemote } from '../utils/teleport.js'
|
||||
import {
|
||||
pollForApprovedExitPlanMode,
|
||||
UltraplanPollError,
|
||||
} from '../utils/ultraplan/ccrSession.js'
|
||||
|
||||
// TODO(prod-hardening): OAuth token may go stale over the 30min poll;
|
||||
// consider refresh.
|
||||
|
||||
// Multi-agent exploration is slow; 30min timeout.
|
||||
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000;
|
||||
export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the-web';
|
||||
const ULTRAPLAN_TIMEOUT_MS = 30 * 60 * 1000
|
||||
|
||||
export const CCR_TERMS_URL =
|
||||
'https://code.claude.com/docs/en/claude-code-on-the-web'
|
||||
|
||||
// CCR runs against the first-party API — use the canonical ID, not the
|
||||
// provider-specific string getModelStrings() would return (which may be a
|
||||
@@ -30,7 +44,10 @@ export const CCR_TERMS_URL = 'https://code.claude.com/docs/en/claude-code-on-the
|
||||
// load: the GrowthBook cache is empty at import and `/config` Gates can flip
|
||||
// it between invocations.
|
||||
function getUltraplanModel(): string {
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_ultraplan_model', ALL_MODEL_CONFIGS.opus46.firstParty);
|
||||
return getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_ultraplan_model',
|
||||
ALL_MODEL_CONFIGS.opus46.firstParty,
|
||||
)
|
||||
}
|
||||
|
||||
// prompt.txt is wrapped in <system-reminder> so the CCR browser hides
|
||||
@@ -43,9 +60,11 @@ function getUltraplanModel(): string {
|
||||
//
|
||||
// Bundler inlines .txt as a string; the test runner wraps it as {default}.
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const _rawPrompt = require('../utils/ultraplan/prompt.txt');
|
||||
const _rawPrompt = require('../utils/ultraplan/prompt.txt')
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPrompt : _rawPrompt.default).trimEnd();
|
||||
const DEFAULT_INSTRUCTIONS: string = (
|
||||
typeof _rawPrompt === 'string' ? _rawPrompt : _rawPrompt.default
|
||||
).trimEnd()
|
||||
|
||||
// Dev-only prompt override resolved eagerly at module load.
|
||||
// Gated to ant builds (USER_TYPE is a build-time define,
|
||||
@@ -53,7 +72,10 @@ const DEFAULT_INSTRUCTIONS: string = (typeof _rawPrompt === 'string' ? _rawPromp
|
||||
// Shell-set env only, so top-level process.env read is fine
|
||||
// — settings.env never injects this.
|
||||
/* eslint-disable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs -- ant-only dev override; eager top-level read is the point (crash at startup, not silently inside the slash-command try/catch) */
|
||||
const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE ? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd() : DEFAULT_INSTRUCTIONS;
|
||||
const ULTRAPLAN_INSTRUCTIONS: string =
|
||||
process.env.USER_TYPE === 'ant' && process.env.ULTRAPLAN_PROMPT_FILE
|
||||
? readFileSync(process.env.ULTRAPLAN_PROMPT_FILE, 'utf8').trimEnd()
|
||||
: DEFAULT_INSTRUCTIONS
|
||||
/* eslint-enable custom-rules/no-process-env-top-level, custom-rules/no-sync-fs */
|
||||
|
||||
/**
|
||||
@@ -61,107 +83,123 @@ const ULTRAPLAN_INSTRUCTIONS: string = (process.env.USER_TYPE) === 'ant' && proc
|
||||
* system-reminder so the browser renders them; scaffolding is hidden.
|
||||
*/
|
||||
export function buildUltraplanPrompt(blurb: string, seedPlan?: string): string {
|
||||
const parts: string[] = [];
|
||||
const parts: string[] = []
|
||||
if (seedPlan) {
|
||||
parts.push('Here is a draft plan to refine:', '', seedPlan, '');
|
||||
parts.push('Here is a draft plan to refine:', '', seedPlan, '')
|
||||
}
|
||||
parts.push(ULTRAPLAN_INSTRUCTIONS);
|
||||
parts.push(ULTRAPLAN_INSTRUCTIONS)
|
||||
if (blurb) {
|
||||
parts.push('', blurb);
|
||||
parts.push('', blurb)
|
||||
}
|
||||
return parts.join('\n');
|
||||
return parts.join('\n')
|
||||
}
|
||||
function startDetachedPoll(taskId: string, sessionId: string, url: string, getAppState: () => AppState, setAppState: (f: (prev: AppState) => AppState) => void): void {
|
||||
const started = Date.now();
|
||||
let failed = false;
|
||||
|
||||
function startDetachedPoll(
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
url: string,
|
||||
getAppState: () => AppState,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): void {
|
||||
const started = Date.now()
|
||||
let failed = false
|
||||
void (async () => {
|
||||
try {
|
||||
const {
|
||||
plan,
|
||||
rejectCount,
|
||||
executionTarget
|
||||
} = await pollForApprovedExitPlanMode(sessionId, ULTRAPLAN_TIMEOUT_MS, phase => {
|
||||
if (phase === 'needs_input') logEvent('tengu_ultraplan_awaiting_input', {});
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
|
||||
if (t.status !== 'running') return t;
|
||||
const next = phase === 'running' ? undefined : phase;
|
||||
return t.ultraplanPhase === next ? t : {
|
||||
...t,
|
||||
ultraplanPhase: next
|
||||
};
|
||||
});
|
||||
}, () => getAppState().tasks?.[taskId]?.status !== 'running');
|
||||
const { plan, rejectCount, executionTarget } =
|
||||
await pollForApprovedExitPlanMode(
|
||||
sessionId,
|
||||
ULTRAPLAN_TIMEOUT_MS,
|
||||
phase => {
|
||||
if (phase === 'needs_input')
|
||||
logEvent('tengu_ultraplan_awaiting_input', {})
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => {
|
||||
if (t.status !== 'running') return t
|
||||
const next = phase === 'running' ? undefined : phase
|
||||
return t.ultraplanPhase === next
|
||||
? t
|
||||
: { ...t, ultraplanPhase: next }
|
||||
})
|
||||
},
|
||||
() => getAppState().tasks?.[taskId]?.status !== 'running',
|
||||
)
|
||||
logEvent('tengu_ultraplan_approved', {
|
||||
duration_ms: Date.now() - started,
|
||||
plan_length: plan.length,
|
||||
reject_count: rejectCount,
|
||||
execution_target: executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
execution_target:
|
||||
executionTarget as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
if (executionTarget === 'remote') {
|
||||
// User chose "execute in CCR" in the browser PlanModal — the remote
|
||||
// session is now coding. Skip archive (ARCHIVE has no running-check,
|
||||
// would kill mid-execution) and skip the choice dialog (already chose).
|
||||
// Guard on task status so a poll that resolves after stopUltraplan
|
||||
// doesn't notify for a killed session.
|
||||
const task = getAppState().tasks?.[taskId];
|
||||
if (task?.status !== 'running') return;
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => t.status !== 'running' ? t : {
|
||||
...t,
|
||||
status: 'completed',
|
||||
endTime: Date.now()
|
||||
});
|
||||
setAppState(prev => prev.ultraplanSessionUrl === url ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined
|
||||
} : prev);
|
||||
const task = getAppState().tasks?.[taskId]
|
||||
if (task?.status !== 'running') return
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
|
||||
t.status !== 'running'
|
||||
? t
|
||||
: { ...t, status: 'completed', endTime: Date.now() },
|
||||
)
|
||||
setAppState(prev =>
|
||||
prev.ultraplanSessionUrl === url
|
||||
? { ...prev, ultraplanSessionUrl: undefined }
|
||||
: prev,
|
||||
)
|
||||
enqueuePendingNotification({
|
||||
value: [`Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`, '', 'Results will land as a pull request when the remote session finishes. There is nothing to do here.'].join('\n'),
|
||||
mode: 'task-notification'
|
||||
});
|
||||
value: [
|
||||
`Ultraplan approved — executing in Claude Code on the web. Follow along at: ${url}`,
|
||||
'',
|
||||
'Results will land as a pull request when the remote session finishes. There is nothing to do here.',
|
||||
].join('\n'),
|
||||
mode: 'task-notification',
|
||||
})
|
||||
} else {
|
||||
// Teleport: set pendingChoice so REPL mounts UltraplanChoiceDialog.
|
||||
// The dialog owns archive + URL clear on choice. Guard on task status
|
||||
// so a poll that resolves after stopUltraplan doesn't resurrect the
|
||||
// dialog for a killed session.
|
||||
setAppState(prev => {
|
||||
const task = prev.tasks?.[taskId];
|
||||
if (!task || task.status !== 'running') return prev;
|
||||
const task = prev.tasks?.[taskId]
|
||||
if (!task || task.status !== 'running') return prev
|
||||
return {
|
||||
...prev,
|
||||
ultraplanPendingChoice: {
|
||||
plan,
|
||||
sessionId,
|
||||
taskId
|
||||
}
|
||||
};
|
||||
});
|
||||
ultraplanPendingChoice: { plan, sessionId, taskId },
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// If the task was stopped (stopUltraplan sets status=killed), the poll
|
||||
// erroring is expected — skip the failure notification and cleanup
|
||||
// (kill() already archived; stopUltraplan cleared the URL).
|
||||
const task = getAppState().tasks?.[taskId];
|
||||
if (task?.status !== 'running') return;
|
||||
failed = true;
|
||||
const task = getAppState().tasks?.[taskId]
|
||||
if (task?.status !== 'running') return
|
||||
failed = true
|
||||
logEvent('tengu_ultraplan_failed', {
|
||||
duration_ms: Date.now() - started,
|
||||
reason: (e instanceof UltraplanPollError ? e.reason : 'network_or_unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
reject_count: e instanceof UltraplanPollError ? e.rejectCount : undefined
|
||||
});
|
||||
reason: (e instanceof UltraplanPollError
|
||||
? e.reason
|
||||
: 'network_or_unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
reject_count:
|
||||
e instanceof UltraplanPollError ? e.rejectCount : undefined,
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value: `Ultraplan failed: ${errorMessage(e)}\n\nSession: ${url}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
mode: 'task-notification',
|
||||
})
|
||||
// Error path owns cleanup; teleport path defers to the dialog; remote
|
||||
// path handled its own cleanup above.
|
||||
void archiveRemoteSession(sessionId).catch(e => logForDebugging(`ultraplan archive failed: ${String(e)}`));
|
||||
void archiveRemoteSession(sessionId).catch(e =>
|
||||
logForDebugging(`ultraplan archive failed: ${String(e)}`),
|
||||
)
|
||||
setAppState(prev =>
|
||||
// Compare against this poll's URL so a newer relaunched session's
|
||||
// URL isn't cleared by a stale poll erroring out.
|
||||
prev.ultraplanSessionUrl === url ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined
|
||||
} : prev);
|
||||
// Compare against this poll's URL so a newer relaunched session's
|
||||
// URL isn't cleared by a stale poll erroring out.
|
||||
prev.ultraplanSessionUrl === url
|
||||
? { ...prev, ultraplanSessionUrl: undefined }
|
||||
: prev,
|
||||
)
|
||||
} finally {
|
||||
// Remote path already set status=completed above; teleport path
|
||||
// leaves status=running so the pill shows the ultraplanPhase state
|
||||
@@ -170,27 +208,31 @@ function startDetachedPoll(taskId: string, sessionId: string, url: string, getAp
|
||||
// isBackgroundTask before the pill can render the phase state.
|
||||
// Failure path has no dialog, so it owns the status transition here.
|
||||
if (failed) {
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t => t.status !== 'running' ? t : {
|
||||
...t,
|
||||
status: 'failed',
|
||||
endTime: Date.now()
|
||||
});
|
||||
updateTaskState<RemoteAgentTaskState>(taskId, setAppState, t =>
|
||||
t.status !== 'running'
|
||||
? t
|
||||
: { ...t, status: 'failed', endTime: Date.now() },
|
||||
)
|
||||
}
|
||||
}
|
||||
})();
|
||||
})()
|
||||
}
|
||||
|
||||
// Renders immediately so the terminal doesn't appear hung during the
|
||||
// multi-second teleportToRemote round-trip.
|
||||
function buildLaunchMessage(disconnectedBridge?: boolean): string {
|
||||
const prefix = disconnectedBridge ? `${REMOTE_CONTROL_DISCONNECTED_MSG} ` : '';
|
||||
return `${DIAMOND_OPEN} ultraplan\n${prefix}Starting Claude Code on the web…`;
|
||||
const prefix = disconnectedBridge ? `${REMOTE_CONTROL_DISCONNECTED_MSG} ` : ''
|
||||
return `${DIAMOND_OPEN} ultraplan\n${prefix}Starting Claude Code on the web…`
|
||||
}
|
||||
|
||||
function buildSessionReadyMessage(url: string): string {
|
||||
return `${DIAMOND_OPEN} ultraplan · Monitor progress in Claude Code on the web ${url}\nYou can continue working — when the ${DIAMOND_OPEN} fills, press ↓ to view results`;
|
||||
return `${DIAMOND_OPEN} ultraplan · Monitor progress in Claude Code on the web ${url}\nYou can continue working — when the ${DIAMOND_OPEN} fills, press ↓ to view results`
|
||||
}
|
||||
|
||||
function buildAlreadyActiveMessage(url: string | undefined): string {
|
||||
return url ? `ultraplan: already polling. Open ${url} to check status, or wait for the plan to land here.` : 'ultraplan: already launching. Please wait for the session to start.';
|
||||
return url
|
||||
? `ultraplan: already polling. Open ${url} to check status, or wait for the plan to land here.`
|
||||
: 'ultraplan: already launching. Please wait for the session to start.'
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,26 +242,37 @@ function buildAlreadyActiveMessage(url: string | undefined): string {
|
||||
* shouldStop callback sees the killed status on its next tick and throws;
|
||||
* the catch block early-returns when status !== 'running'.
|
||||
*/
|
||||
export async function stopUltraplan(taskId: string, sessionId: string, setAppState: (f: (prev: AppState) => AppState) => void): Promise<void> {
|
||||
export async function stopUltraplan(
|
||||
taskId: string,
|
||||
sessionId: string,
|
||||
setAppState: (f: (prev: AppState) => AppState) => void,
|
||||
): Promise<void> {
|
||||
// RemoteAgentTask.kill archives the session (with .catch) — no separate
|
||||
// archive call needed here.
|
||||
await RemoteAgentTask.kill(taskId, setAppState);
|
||||
setAppState(prev => prev.ultraplanSessionUrl || prev.ultraplanPendingChoice || prev.ultraplanLaunching ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined,
|
||||
ultraplanPendingChoice: undefined,
|
||||
ultraplanLaunching: undefined
|
||||
} : prev);
|
||||
const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL);
|
||||
await RemoteAgentTask.kill(taskId, setAppState)
|
||||
setAppState(prev =>
|
||||
prev.ultraplanSessionUrl ||
|
||||
prev.ultraplanPendingChoice ||
|
||||
prev.ultraplanLaunching
|
||||
? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined,
|
||||
ultraplanPendingChoice: undefined,
|
||||
ultraplanLaunching: undefined,
|
||||
}
|
||||
: prev,
|
||||
)
|
||||
const url = getRemoteSessionUrl(sessionId, process.env.SESSION_INGRESS_URL)
|
||||
enqueuePendingNotification({
|
||||
value: `Ultraplan stopped.\n\nSession: ${url}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
enqueuePendingNotification({
|
||||
value: 'The user stopped the ultraplan session above. Do not respond to the stop notification — wait for their next message.',
|
||||
mode: 'task-notification',
|
||||
isMeta: true
|
||||
});
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value:
|
||||
'The user stopped the ultraplan session above. Do not respond to the stop notification — wait for their next message.',
|
||||
mode: 'task-notification',
|
||||
isMeta: true,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -232,13 +285,13 @@ export async function stopUltraplan(taskId: string, sessionId: string, setAppSta
|
||||
* enqueuePendingNotification.
|
||||
*/
|
||||
export async function launchUltraplan(opts: {
|
||||
blurb: string;
|
||||
seedPlan?: string;
|
||||
getAppState: () => AppState;
|
||||
setAppState: (f: (prev: AppState) => AppState) => void;
|
||||
signal: AbortSignal;
|
||||
blurb: string
|
||||
seedPlan?: string
|
||||
getAppState: () => AppState
|
||||
setAppState: (f: (prev: AppState) => AppState) => void
|
||||
signal: AbortSignal
|
||||
/** True if the caller disconnected Remote Control before launching. */
|
||||
disconnectedBridge?: boolean;
|
||||
disconnectedBridge?: boolean
|
||||
/**
|
||||
* Called once teleportToRemote resolves with a session URL. Callers that
|
||||
* have setMessages (REPL) append this as a second transcript message so the
|
||||
@@ -246,7 +299,7 @@ export async function launchUltraplan(opts: {
|
||||
* transcript access (ExitPlanModePermissionRequest) omit this — the pill
|
||||
* still shows live status.
|
||||
*/
|
||||
onSessionReady?: (msg: string) => void;
|
||||
onSessionReady?: (msg: string) => void
|
||||
}): Promise<string> {
|
||||
const {
|
||||
blurb,
|
||||
@@ -255,79 +308,90 @@ export async function launchUltraplan(opts: {
|
||||
setAppState,
|
||||
signal,
|
||||
disconnectedBridge,
|
||||
onSessionReady
|
||||
} = opts;
|
||||
const {
|
||||
ultraplanSessionUrl: active,
|
||||
ultraplanLaunching
|
||||
} = getAppState();
|
||||
onSessionReady,
|
||||
} = opts
|
||||
|
||||
const { ultraplanSessionUrl: active, ultraplanLaunching } = getAppState()
|
||||
if (active || ultraplanLaunching) {
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: (active ? 'already_polling' : 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
return buildAlreadyActiveMessage(active);
|
||||
reason: (active
|
||||
? 'already_polling'
|
||||
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
return buildAlreadyActiveMessage(active)
|
||||
}
|
||||
|
||||
if (!blurb && !seedPlan) {
|
||||
// No event — bare /ultraplan is a usage query, not an attempt.
|
||||
return [
|
||||
// Rendered via <Markdown>; raw <message> is tokenized as HTML
|
||||
// and dropped. Backslash-escape the brackets.
|
||||
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere', 'in your prompt', '', 'Advanced multi-agent plan mode with our most powerful model', '(Opus). Runs in Claude Code on the web. When the plan is ready,', 'you can execute it in the web session or send it back here.', 'Terminal stays free while the remote plans.', 'Requires /login.', '', `Terms: ${CCR_TERMS_URL}`].join('\n');
|
||||
// Rendered via <Markdown>; raw <message> is tokenized as HTML
|
||||
// and dropped. Backslash-escape the brackets.
|
||||
'Usage: /ultraplan \\<prompt\\>, or include "ultraplan" anywhere',
|
||||
'in your prompt',
|
||||
'',
|
||||
'Advanced multi-agent plan mode with our most powerful model',
|
||||
'(Opus). Runs in Claude Code on the web. When the plan is ready,',
|
||||
'you can execute it in the web session or send it back here.',
|
||||
'Terminal stays free while the remote plans.',
|
||||
'Requires /login.',
|
||||
'',
|
||||
`Terms: ${CCR_TERMS_URL}`,
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
// Set synchronously before the detached flow to prevent duplicate launches
|
||||
// during the teleportToRemote window.
|
||||
setAppState(prev => prev.ultraplanLaunching ? prev : {
|
||||
...prev,
|
||||
ultraplanLaunching: true
|
||||
});
|
||||
setAppState(prev =>
|
||||
prev.ultraplanLaunching ? prev : { ...prev, ultraplanLaunching: true },
|
||||
)
|
||||
void launchDetached({
|
||||
blurb,
|
||||
seedPlan,
|
||||
getAppState,
|
||||
setAppState,
|
||||
signal,
|
||||
onSessionReady
|
||||
});
|
||||
return buildLaunchMessage(disconnectedBridge);
|
||||
onSessionReady,
|
||||
})
|
||||
return buildLaunchMessage(disconnectedBridge)
|
||||
}
|
||||
|
||||
async function launchDetached(opts: {
|
||||
blurb: string;
|
||||
seedPlan?: string;
|
||||
getAppState: () => AppState;
|
||||
setAppState: (f: (prev: AppState) => AppState) => void;
|
||||
signal: AbortSignal;
|
||||
onSessionReady?: (msg: string) => void;
|
||||
blurb: string
|
||||
seedPlan?: string
|
||||
getAppState: () => AppState
|
||||
setAppState: (f: (prev: AppState) => AppState) => void
|
||||
signal: AbortSignal
|
||||
onSessionReady?: (msg: string) => void
|
||||
}): Promise<void> {
|
||||
const {
|
||||
blurb,
|
||||
seedPlan,
|
||||
getAppState,
|
||||
setAppState,
|
||||
signal,
|
||||
onSessionReady
|
||||
} = opts;
|
||||
const { blurb, seedPlan, getAppState, setAppState, signal, onSessionReady } =
|
||||
opts
|
||||
// Hoisted so the catch block can archive the remote session if an error
|
||||
// occurs after teleportToRemote succeeds (avoids 30min orphan).
|
||||
let sessionId: string | undefined;
|
||||
let sessionId: string | undefined
|
||||
try {
|
||||
const model = getUltraplanModel();
|
||||
const eligibility = await checkRemoteAgentEligibility();
|
||||
const model = getUltraplanModel()
|
||||
|
||||
const eligibility = await checkRemoteAgentEligibility()
|
||||
if (!eligibility.eligible) {
|
||||
const eligErrors = (eligibility as { eligible: false; errors: Array<{ type: string }> }).errors;
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: 'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
precondition_errors: eligErrors.map(e => e.type).join(',') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
const reasons = eligErrors.map(formatPreconditionError).join('\n');
|
||||
reason:
|
||||
'precondition' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
precondition_errors: eligibility.errors
|
||||
.map(e => e.type)
|
||||
.join(
|
||||
',',
|
||||
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
const reasons = eligibility.errors.map(formatPreconditionError).join('\n')
|
||||
enqueuePendingNotification({
|
||||
value: `ultraplan: cannot launch remote session —\n${reasons}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
return;
|
||||
mode: 'task-notification',
|
||||
})
|
||||
return
|
||||
}
|
||||
const prompt = buildUltraplanPrompt(blurb, seedPlan);
|
||||
let bundleFailMsg: string | undefined;
|
||||
|
||||
const prompt = buildUltraplanPrompt(blurb, seedPlan)
|
||||
let bundleFailMsg: string | undefined
|
||||
const session = await teleportToRemote({
|
||||
initialMessage: prompt,
|
||||
description: blurb || 'Refine local plan',
|
||||
@@ -337,80 +401,85 @@ async function launchDetached(opts: {
|
||||
signal,
|
||||
useDefaultEnvironment: true,
|
||||
onBundleFail: msg => {
|
||||
bundleFailMsg = msg;
|
||||
}
|
||||
});
|
||||
bundleFailMsg = msg
|
||||
},
|
||||
})
|
||||
if (!session) {
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: (bundleFailMsg ? 'bundle_fail' : 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
reason: (bundleFailMsg
|
||||
? 'bundle_fail'
|
||||
: 'teleport_null') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value: `ultraplan: session creation failed${bundleFailMsg ? ` — ${bundleFailMsg}` : ''}. See --debug for details.`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
return;
|
||||
mode: 'task-notification',
|
||||
})
|
||||
return
|
||||
}
|
||||
sessionId = session.id;
|
||||
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL);
|
||||
sessionId = session.id
|
||||
|
||||
const url = getRemoteSessionUrl(session.id, process.env.SESSION_INGRESS_URL)
|
||||
setAppState(prev => ({
|
||||
...prev,
|
||||
ultraplanSessionUrl: url,
|
||||
ultraplanLaunching: undefined
|
||||
}));
|
||||
onSessionReady?.(buildSessionReadyMessage(url));
|
||||
ultraplanLaunching: undefined,
|
||||
}))
|
||||
onSessionReady?.(buildSessionReadyMessage(url))
|
||||
logEvent('tengu_ultraplan_launched', {
|
||||
has_seed_plan: Boolean(seedPlan),
|
||||
model: model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
model:
|
||||
model as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
// TODO(#23985): replace registerRemoteAgentTask + startDetachedPoll with
|
||||
// ExitPlanModeScanner inside startRemoteSessionPolling.
|
||||
const {
|
||||
taskId
|
||||
} = registerRemoteAgentTask({
|
||||
const { taskId } = registerRemoteAgentTask({
|
||||
remoteTaskType: 'ultraplan',
|
||||
session: {
|
||||
id: session.id,
|
||||
title: blurb || 'Ultraplan'
|
||||
},
|
||||
session: { id: session.id, title: blurb || 'Ultraplan' },
|
||||
command: blurb,
|
||||
context: {
|
||||
abortController: new AbortController(),
|
||||
getAppState,
|
||||
setAppState
|
||||
setAppState,
|
||||
},
|
||||
isUltraplan: true
|
||||
});
|
||||
startDetachedPoll(taskId, session.id, url, getAppState, setAppState);
|
||||
isUltraplan: true,
|
||||
})
|
||||
startDetachedPoll(taskId, session.id, url, getAppState, setAppState)
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
logError(e)
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: 'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
reason:
|
||||
'unexpected_error' as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
enqueuePendingNotification({
|
||||
value: `ultraplan: unexpected error — ${errorMessage(e)}`,
|
||||
mode: 'task-notification'
|
||||
});
|
||||
mode: 'task-notification',
|
||||
})
|
||||
if (sessionId) {
|
||||
// Error after teleport succeeded — archive so the remote doesn't sit
|
||||
// running for 30min with nobody polling it.
|
||||
void archiveRemoteSession(sessionId).catch(err => logForDebugging('ultraplan: failed to archive orphaned session', err));
|
||||
void archiveRemoteSession(sessionId).catch(err =>
|
||||
logForDebugging('ultraplan: failed to archive orphaned session', err),
|
||||
)
|
||||
// ultraplanSessionUrl may have been set before the throw; clear it so
|
||||
// the "already polling" guard doesn't block future launches.
|
||||
setAppState(prev => prev.ultraplanSessionUrl ? {
|
||||
...prev,
|
||||
ultraplanSessionUrl: undefined
|
||||
} : prev);
|
||||
setAppState(prev =>
|
||||
prev.ultraplanSessionUrl
|
||||
? { ...prev, ultraplanSessionUrl: undefined }
|
||||
: prev,
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
// No-op on success: the url-setting setAppState already cleared this.
|
||||
setAppState(prev => prev.ultraplanLaunching ? {
|
||||
...prev,
|
||||
ultraplanLaunching: undefined
|
||||
} : prev);
|
||||
setAppState(prev =>
|
||||
prev.ultraplanLaunching
|
||||
? { ...prev, ultraplanLaunching: undefined }
|
||||
: prev,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
const blurb = args.trim();
|
||||
const blurb = args.trim()
|
||||
|
||||
// Bare /ultraplan (no args, no seed plan) just shows usage — no dialog.
|
||||
if (!blurb) {
|
||||
@@ -418,54 +487,42 @@ const call: LocalJSXCommandCall = async (onDone, context, args) => {
|
||||
blurb,
|
||||
getAppState: context.getAppState,
|
||||
setAppState: context.setAppState,
|
||||
signal: context.abortController.signal
|
||||
});
|
||||
onDone(msg, {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
signal: context.abortController.signal,
|
||||
})
|
||||
onDone(msg, { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// Guard matches launchUltraplan's own check — showing the dialog when a
|
||||
// session is already active or launching would waste the user's click and set
|
||||
// hasSeenUltraplanTerms before the launch fails.
|
||||
const {
|
||||
ultraplanSessionUrl: active,
|
||||
ultraplanLaunching
|
||||
} = context.getAppState();
|
||||
const { ultraplanSessionUrl: active, ultraplanLaunching } =
|
||||
context.getAppState()
|
||||
if (active || ultraplanLaunching) {
|
||||
logEvent('tengu_ultraplan_create_failed', {
|
||||
reason: (active ? 'already_polling' : 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS
|
||||
});
|
||||
onDone(buildAlreadyActiveMessage(active), {
|
||||
display: 'system'
|
||||
});
|
||||
return null;
|
||||
reason: (active
|
||||
? 'already_polling'
|
||||
: 'already_launching') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
||||
})
|
||||
onDone(buildAlreadyActiveMessage(active), { display: 'system' })
|
||||
return null
|
||||
}
|
||||
|
||||
// Mount the pre-launch dialog via focusedInputDialog (bottom region, like
|
||||
// permission dialogs) rather than returning JSX (transcript area, anchors
|
||||
// at top of scrollback). REPL.tsx handles launch/clear/cancel on choice.
|
||||
context.setAppState(prev => ({
|
||||
...prev,
|
||||
ultraplanLaunchPending: {
|
||||
blurb
|
||||
}
|
||||
}));
|
||||
context.setAppState(prev => ({ ...prev, ultraplanLaunchPending: { blurb } }))
|
||||
// 'skip' suppresses the (no content) echo — the dialog's choice handler
|
||||
// adds the real /ultraplan echo + launch confirmation.
|
||||
onDone(undefined, {
|
||||
display: 'skip'
|
||||
});
|
||||
return null;
|
||||
};
|
||||
onDone(undefined, { display: 'skip' })
|
||||
return null
|
||||
}
|
||||
|
||||
export default {
|
||||
type: 'local-jsx',
|
||||
name: 'ultraplan',
|
||||
description: `~10–30 min · Claude Code on the web drafts an advanced plan you can edit and approve. See ${CCR_TERMS_URL}`,
|
||||
argumentHint: '<prompt>',
|
||||
isEnabled: () => (process.env.USER_TYPE) === 'ant',
|
||||
load: () => Promise.resolve({
|
||||
call
|
||||
})
|
||||
} satisfies Command;
|
||||
isEnabled: () => process.env.USER_TYPE === 'ant',
|
||||
load: () => Promise.resolve({ call }),
|
||||
} satisfies Command
|
||||
|
||||
@@ -1,37 +1,67 @@
|
||||
import * as React from 'react';
|
||||
import type { LocalJSXCommandContext } from '../../commands.js';
|
||||
import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js';
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js';
|
||||
import { getClaudeAIOAuthTokens, isClaudeAISubscriber } from '../../utils/auth.js';
|
||||
import { openBrowser } from '../../utils/browser.js';
|
||||
import { logError } from '../../utils/log.js';
|
||||
import { Login } from '../login/login.js';
|
||||
export async function call(onDone: LocalJSXCommandOnDone, context: LocalJSXCommandContext): Promise<React.ReactNode | null> {
|
||||
import * as React from 'react'
|
||||
import type { LocalJSXCommandContext } from '../../commands.js'
|
||||
import { getOauthProfileFromOauthToken } from '../../services/oauth/getOauthProfile.js'
|
||||
import type { LocalJSXCommandOnDone } from '../../types/command.js'
|
||||
import {
|
||||
getClaudeAIOAuthTokens,
|
||||
isClaudeAISubscriber,
|
||||
} from '../../utils/auth.js'
|
||||
import { openBrowser } from '../../utils/browser.js'
|
||||
import { logError } from '../../utils/log.js'
|
||||
import { Login } from '../login/login.js'
|
||||
|
||||
export async function call(
|
||||
onDone: LocalJSXCommandOnDone,
|
||||
context: LocalJSXCommandContext,
|
||||
): Promise<React.ReactNode | null> {
|
||||
try {
|
||||
// Check if user is already on the highest Max plan (20x)
|
||||
if (isClaudeAISubscriber()) {
|
||||
const tokens = getClaudeAIOAuthTokens();
|
||||
let isMax20x = false;
|
||||
const tokens = getClaudeAIOAuthTokens()
|
||||
let isMax20x = false
|
||||
|
||||
if (tokens?.subscriptionType && tokens?.rateLimitTier) {
|
||||
isMax20x = tokens.subscriptionType === 'max' && tokens.rateLimitTier === 'default_claude_max_20x';
|
||||
isMax20x =
|
||||
tokens.subscriptionType === 'max' &&
|
||||
tokens.rateLimitTier === 'default_claude_max_20x'
|
||||
} else if (tokens?.accessToken) {
|
||||
const profile = await getOauthProfileFromOauthToken(tokens.accessToken);
|
||||
isMax20x = profile?.organization?.organization_type === 'claude_max' && profile?.organization?.rate_limit_tier === 'default_claude_max_20x';
|
||||
const profile = await getOauthProfileFromOauthToken(tokens.accessToken)
|
||||
isMax20x =
|
||||
profile?.organization?.organization_type === 'claude_max' &&
|
||||
profile?.organization?.rate_limit_tier === 'default_claude_max_20x'
|
||||
}
|
||||
|
||||
if (isMax20x) {
|
||||
setTimeout(onDone, 0, 'You are already on the highest Max subscription plan. For additional usage, run /login to switch to an API usage-billed account.');
|
||||
return null;
|
||||
setTimeout(
|
||||
onDone,
|
||||
0,
|
||||
'You are already on the highest Max subscription plan. For additional usage, run /login to switch to an API usage-billed account.',
|
||||
)
|
||||
return null
|
||||
}
|
||||
}
|
||||
const url = 'https://claude.ai/upgrade/max';
|
||||
await openBrowser(url);
|
||||
return <Login startingMessage={'Starting new login following /upgrade. Exit with Ctrl-C to use existing account.'} onDone={success => {
|
||||
context.onChangeAPIKey();
|
||||
onDone(success ? 'Login successful' : 'Login interrupted');
|
||||
}} />;
|
||||
|
||||
const url = 'https://claude.ai/upgrade/max'
|
||||
await openBrowser(url)
|
||||
|
||||
return (
|
||||
<Login
|
||||
startingMessage={
|
||||
'Starting new login following /upgrade. Exit with Ctrl-C to use existing account.'
|
||||
}
|
||||
onDone={success => {
|
||||
context.onChangeAPIKey()
|
||||
onDone(success ? 'Login successful' : 'Login interrupted')
|
||||
}}
|
||||
/>
|
||||
)
|
||||
} catch (error) {
|
||||
logError(error as Error);
|
||||
setTimeout(onDone, 0, 'Failed to open browser. Please visit https://claude.ai/upgrade/max to upgrade.');
|
||||
logError(error as Error)
|
||||
setTimeout(
|
||||
onDone,
|
||||
0,
|
||||
'Failed to open browser. Please visit https://claude.ai/upgrade/max to upgrade.',
|
||||
)
|
||||
}
|
||||
return null;
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
import { Settings } from '../../components/Settings/Settings.js';
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js';
|
||||
import * as React from 'react'
|
||||
import { Settings } from '../../components/Settings/Settings.js'
|
||||
import type { LocalJSXCommandCall } from '../../types/command.js'
|
||||
|
||||
export const call: LocalJSXCommandCall = async (onDone, context) => {
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Usage" />;
|
||||
};
|
||||
return <Settings onClose={onDone} context={context} defaultTab="Usage" />
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user