@xelauvas/xela-cli 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/xela.js +0 -12
- package/local/README.md +112 -0
- package/local/start-qwen.sh +47 -0
- package/local/start-tunnel.sh +36 -0
- package/local/xela-local.sh +139 -0
- package/package.json +9 -5
- package/run.sh +33 -0
- package/src/Tool.ts +1 -1
- package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
- package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
- package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
- package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
- package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
- package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
- package/src/_shims/bun_bundle.js +79 -4
- package/src/_shims/cjs_stub.cjs +32 -10
- package/src/_shims/loader.js +104 -5
- package/src/_shims/openai_adapter.ts +446 -230
- package/src/_shims/register.js +16 -3
- package/src/assistant/gate.ts +15 -0
- package/src/assistant/index.ts +89 -0
- package/src/bootstrap/state.ts +6 -1
- package/src/bridge/peerSessions.ts +18 -0
- package/src/cli/print.ts +5 -0
- package/src/commands/assistant/assistant.ts +17 -0
- package/src/commands/assistant/index.ts +11 -0
- package/src/commands/brief.ts +3 -1
- package/src/commands/buddy/buddy.ts +88 -0
- package/src/commands/buddy/index.ts +12 -0
- package/src/commands/model/model.tsx +8 -23
- package/src/commands/peers/index.ts +11 -0
- package/src/commands/peers/peers.ts +24 -0
- package/src/commands/proactive/proactive.ts +64 -0
- package/src/commands/proactive.ts +12 -0
- package/src/commands/workflows/index.ts +12 -0
- package/src/commands/workflows/workflows.ts +36 -0
- package/src/commands.ts +4 -8
- package/src/components/Messages.tsx +1 -1
- package/src/components/ModelPicker.tsx +4 -34
- package/src/components/Spinner.tsx +54 -1
- package/src/components/StructuredDiff/colorDiff.ts +3 -1
- package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
- package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
- package/src/components/permissions/PermissionRequest.tsx +4 -2
- package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
- package/src/constants/prompts.ts +26 -0
- package/src/ink/terminal.ts +9 -1
- package/src/jobs/classifier.ts +103 -0
- package/src/memdir/paths.ts +13 -1
- package/src/proactive/index.ts +158 -0
- package/src/proactive/useProactive.ts +78 -0
- package/src/screens/REPL.tsx +28 -8
- package/src/services/api/claude.ts +6 -1
- package/src/services/compact/autoCompact.ts +18 -4
- package/src/services/compact/cachedMCConfig.ts +1 -0
- package/src/services/compact/cachedMicrocompact.ts +113 -0
- package/src/services/compact/compact.ts +22 -2
- package/src/services/compact/reactiveCompact.ts +251 -0
- package/src/services/compact/snipCompact.ts +282 -0
- package/src/services/compact/snipProjection.ts +50 -0
- package/src/services/contextCollapse/index.ts +322 -0
- package/src/services/contextCollapse/operations.ts +187 -0
- package/src/services/contextCollapse/persist.ts +40 -0
- package/src/services/extractMemories/extractMemories.ts +11 -2
- package/src/services/skillSearch/featureCheck.ts +12 -0
- package/src/services/skillSearch/localSearch.ts +98 -0
- package/src/services/skillSearch/prefetch.ts +125 -0
- package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
- package/src/services/skillSearch/remoteSkillState.ts +21 -0
- package/src/services/skillSearch/signals.ts +7 -0
- package/src/services/skillSearch/telemetry.ts +9 -0
- package/src/setup.ts +10 -5
- package/src/skills/bundled/dream.ts +31 -0
- package/src/skills/bundled/hunter.ts +45 -0
- package/src/skills/bundled/index.ts +10 -23
- package/src/skills/bundled/runSkillGenerator.ts +61 -0
- package/src/skills/mcpSkills.ts +101 -0
- package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
- package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
- package/src/tasks.ts +22 -8
- package/src/tools/AgentTool/runAgent.ts +11 -0
- package/src/tools/BriefTool/BriefTool.ts +2 -0
- package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
- package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
- package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
- package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
- package/src/tools/MCPTool/MCPTool.ts +6 -0
- package/src/tools/MonitorTool/MonitorTool.ts +273 -0
- package/src/tools/MonitorTool/UI.tsx +25 -0
- package/src/tools/MonitorTool/events.ts +84 -0
- package/src/tools/MonitorTool/prompt.ts +45 -0
- package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
- package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
- package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
- package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
- package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
- package/src/tools/SendUserFileTool/prompt.ts +7 -0
- package/src/tools/SleepTool/SleepTool.ts +126 -0
- package/src/tools/SnipTool/SnipTool.ts +90 -0
- package/src/tools/SnipTool/prompt.ts +5 -0
- package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
- package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
- package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
- package/src/tools/ToolSearchTool/prompt.ts +1 -1
- package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
- package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
- package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
- package/src/tools/WebBrowserTool/prompt.ts +16 -0
- package/src/tools/WorkflowTool/UI.tsx +27 -0
- package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
- package/src/tools/WorkflowTool/bundled/index.ts +88 -0
- package/src/tools/WorkflowTool/constants.ts +7 -0
- package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
- package/src/tools/WorkflowTool/prompt.ts +38 -0
- package/src/tools/WorkflowTool/runtime.ts +331 -0
- package/src/tools.ts +36 -31
- package/src/utils/betas.ts +5 -0
- package/src/utils/context.ts +9 -0
- package/src/utils/conversationRecovery.ts +1 -1
- package/src/utils/forkedAgent.ts +4 -0
- package/src/utils/logoV2Utils.ts +7 -3
- package/src/utils/messageQueueManager.ts +23 -9
- package/src/utils/model/model.ts +10 -9
- package/src/utils/model/modelOptions.ts +0 -21
- package/src/utils/model/validateModel.ts +0 -19
- package/src/utils/permissions/permissionSetup.ts +5 -0
- package/src/utils/permissions/permissions.ts +25 -0
- package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
- package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
- package/src/utils/permissions/yoloClassifier.ts +28 -6
- package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
- package/src/utils/systemThemeWatcher.ts +62 -0
- package/src/utils/udsClient.ts +115 -0
- package/src/utils/udsMessaging.ts +157 -0
- package/src/utils/yaml.ts +251 -5
- package/src/utils/model/providerModels.ts +0 -168
|
@@ -18,7 +18,6 @@ import { Byline } from './design-system/Byline.js';
|
|
|
18
18
|
import { KeyboardShortcutHint } from './design-system/KeyboardShortcutHint.js';
|
|
19
19
|
import { Pane } from './design-system/Pane.js';
|
|
20
20
|
import { effortLevelToSymbol } from './EffortIndicator.js';
|
|
21
|
-
import { isOpenAIBackend, fetchProviderModels, getOpenAIDefaultModel } from '../utils/model/providerModels.js';
|
|
22
21
|
export type Props = {
|
|
23
22
|
initial: string | null;
|
|
24
23
|
sessionModel?: ModelSetting;
|
|
@@ -75,38 +74,9 @@ export function ModelPicker(t0) {
|
|
|
75
74
|
t3 = $[3];
|
|
76
75
|
}
|
|
77
76
|
const modelOptions = t3;
|
|
78
|
-
|
|
79
|
-
const [providerModels, setProviderModels] = useState<Array<{value: string | null, label: string, description: string}>>([]);
|
|
80
|
-
const [providerLoading, setProviderLoading] = useState(isOpenAIBackend());
|
|
81
|
-
|
|
82
|
-
React.useEffect(() => {
|
|
83
|
-
if (!isOpenAIBackend()) return;
|
|
84
|
-
let cancelled = false;
|
|
85
|
-
(async () => {
|
|
86
|
-
try {
|
|
87
|
-
const models = await fetchProviderModels();
|
|
88
|
-
if (cancelled) return;
|
|
89
|
-
const defaultModel = getOpenAIDefaultModel();
|
|
90
|
-
const options = [
|
|
91
|
-
{ value: null, label: `Default (${defaultModel})`, description: 'Current default model' },
|
|
92
|
-
...models.map(m => ({ value: m.id, label: m.id, description: m.name !== m.id ? m.name : '' })),
|
|
93
|
-
];
|
|
94
|
-
setProviderModels(options);
|
|
95
|
-
} finally {
|
|
96
|
-
if (!cancelled) setProviderLoading(false);
|
|
97
|
-
}
|
|
98
|
-
})();
|
|
99
|
-
return () => { cancelled = true; };
|
|
100
|
-
}, []);
|
|
101
|
-
|
|
102
|
-
// When on OpenAI backend and provider models loaded, use them instead of Claude options
|
|
103
|
-
const effectiveModelOptions = isOpenAIBackend() && providerModels.length > 0
|
|
104
|
-
? providerModels
|
|
105
|
-
: modelOptions;
|
|
106
|
-
|
|
107
77
|
let t4;
|
|
108
78
|
bb0: {
|
|
109
|
-
if (initial !== null && !
|
|
79
|
+
if (initial !== null && !modelOptions.some(opt => opt.value === initial)) {
|
|
110
80
|
let t5;
|
|
111
81
|
if ($[4] !== initial) {
|
|
112
82
|
t5 = modelDisplayString(initial);
|
|
@@ -130,7 +100,7 @@ export function ModelPicker(t0) {
|
|
|
130
100
|
}
|
|
131
101
|
let t7;
|
|
132
102
|
if ($[9] !== modelOptions || $[10] !== t6) {
|
|
133
|
-
t7 = [...
|
|
103
|
+
t7 = [...modelOptions, t6];
|
|
134
104
|
$[9] = modelOptions;
|
|
135
105
|
$[10] = t6;
|
|
136
106
|
$[11] = t7;
|
|
@@ -140,7 +110,7 @@ export function ModelPicker(t0) {
|
|
|
140
110
|
t4 = t7;
|
|
141
111
|
break bb0;
|
|
142
112
|
}
|
|
143
|
-
t4 =
|
|
113
|
+
t4 = modelOptions;
|
|
144
114
|
}
|
|
145
115
|
const optionsWithInitial = t4;
|
|
146
116
|
let t5;
|
|
@@ -295,7 +265,7 @@ export function ModelPicker(t0) {
|
|
|
295
265
|
} else {
|
|
296
266
|
t15 = $[41];
|
|
297
267
|
}
|
|
298
|
-
const t16 = headerText ??
|
|
268
|
+
const t16 = headerText ?? "Switch between Claude models. Applies to this session and future Claude Code sessions. For other/previous model names, specify with --model.";
|
|
299
269
|
let t17;
|
|
300
270
|
if ($[42] !== t16) {
|
|
301
271
|
t17 = <Text dimColor={true}>{t16}</Text>;
|
|
@@ -39,8 +39,14 @@ import { getGlobalConfig } from '../utils/config.js';
|
|
|
39
39
|
export type { SpinnerMode } from './Spinner/index.js';
|
|
40
40
|
const DEFAULT_CHARACTERS = getDefaultCharacters();
|
|
41
41
|
const SPINNER_FRAMES = [...DEFAULT_CHARACTERS, ...[...DEFAULT_CHARACTERS].reverse()];
|
|
42
|
+
export type CompactProgressInfo = {
|
|
43
|
+
startedAt: number;
|
|
44
|
+
preCompactTokenCount: number;
|
|
45
|
+
};
|
|
42
46
|
type Props = {
|
|
43
47
|
mode: SpinnerMode;
|
|
48
|
+
/** Set while /compact or auto-compact is summarizing; shows a progress bar. */
|
|
49
|
+
compactProgress?: CompactProgressInfo | null;
|
|
44
50
|
loadingStartTimeRef: React.RefObject<number>;
|
|
45
51
|
totalPausedMsRef: React.RefObject<number>;
|
|
46
52
|
pauseStartTimeRef: React.RefObject<number | null>;
|
|
@@ -89,6 +95,7 @@ function SpinnerWithVerbInner({
|
|
|
89
95
|
overrideColor,
|
|
90
96
|
overrideShimmerColor,
|
|
91
97
|
overrideMessage,
|
|
98
|
+
compactProgress,
|
|
92
99
|
spinnerSuffix,
|
|
93
100
|
verbose,
|
|
94
101
|
hasActiveTools = false,
|
|
@@ -254,7 +261,9 @@ function SpinnerWithVerbInner({
|
|
|
254
261
|
// cause re-renders that refresh this in practice.
|
|
255
262
|
let contextTipsActive = false;
|
|
256
263
|
const tipsEnabled = settings.spinnerTipsEnabled !== false;
|
|
257
|
-
|
|
264
|
+
// While compacting, /clear is the relevant hint whatever the elapsed time:
|
|
265
|
+
// the user is waiting on context pressure right now.
|
|
266
|
+
const showClearTip = tipsEnabled && (elapsedSnapshot > 1_800_000 || !!compactProgress);
|
|
258
267
|
const showBtwTip = tipsEnabled && elapsedSnapshot > 30_000 && !getGlobalConfig().btwUseCount;
|
|
259
268
|
const effectiveTip = contextTipsActive ? undefined : showClearTip && !nextTask ? 'Use /clear to start fresh when switching topics and free up context' : showBtwTip && !nextTask ? "Use /btw to ask a quick side question without interrupting Claude's current work" : spinnerTip;
|
|
260
269
|
|
|
@@ -279,6 +288,7 @@ function SpinnerWithVerbInner({
|
|
|
279
288
|
}
|
|
280
289
|
return <Box flexDirection="column" width="100%" alignItems="flex-start">
|
|
281
290
|
<SpinnerAnimationRow mode={mode} reducedMotion={reducedMotion} hasActiveTools={hasActiveTools} responseLengthRef={responseLengthRef} message={message} messageColor={messageColor} shimmerColor={shimmerColor} overrideColor={overrideColor} loadingStartTimeRef={loadingStartTimeRef} totalPausedMsRef={totalPausedMsRef} pauseStartTimeRef={pauseStartTimeRef} spinnerSuffix={spinnerSuffix} verbose={verbose} columns={columns} hasRunningTeammates={hasRunningTeammates} teammateTokens={teammateTokens} foregroundedTeammate={foregroundedTeammate} leaderIsIdle={leaderIsIdle} thinkingStatus={thinkingStatus} effortSuffix={effortSuffix} />
|
|
291
|
+
{compactProgress && <CompactProgressBar info={compactProgress} responseLengthRef={responseLengthRef} columns={columns} />}
|
|
282
292
|
{showSpinnerTree && hasRunningTeammates ? <TeammateSpinnerTree selectedIndex={selectedIPAgentIndex} isInSelectionMode={viewSelectionMode === 'selecting-agent'} allIdle={allIdle} leaderVerb={leaderIsIdle ? undefined : leaderVerb} leaderIdleText={leaderIsIdle ? 'Idle' : undefined} leaderTokenCount={leaderTokenCount} /> : showExpandedTodos && tasksV2 && tasksV2.length > 0 ? <Box width="100%" flexDirection="column">
|
|
283
293
|
<MessageResponse>
|
|
284
294
|
<TaskListV2 tasks={tasksV2} />
|
|
@@ -300,6 +310,49 @@ function SpinnerWithVerbInner({
|
|
|
300
310
|
</Box>;
|
|
301
311
|
}
|
|
302
312
|
|
|
313
|
+
/**
|
|
314
|
+
* Progress bar shown under "Compacting conversation…". Compaction streams a
|
|
315
|
+
* summary whose length we can only estimate, so the ratio blends two signals:
|
|
316
|
+
* characters streamed so far against the expected summary size, and elapsed
|
|
317
|
+
* time against the expected prompt-processing time (so the bar moves while a
|
|
318
|
+
* large prompt is still being read and nothing has streamed yet). It never
|
|
319
|
+
* reaches 100% on its own; compact_end removes it.
|
|
320
|
+
*/
|
|
321
|
+
function CompactProgressBar({
|
|
322
|
+
info,
|
|
323
|
+
responseLengthRef,
|
|
324
|
+
columns
|
|
325
|
+
}: {
|
|
326
|
+
info: CompactProgressInfo;
|
|
327
|
+
responseLengthRef: React.RefObject<number>;
|
|
328
|
+
columns: number;
|
|
329
|
+
}): React.ReactNode {
|
|
330
|
+
// The clock only ticks while the ref'd element is visible, so it must be
|
|
331
|
+
// attached to the rendered Box.
|
|
332
|
+
// `time` is only used to re-render on the shared clock; its epoch is not
|
|
333
|
+
// wall time, so elapsed is computed from Date.now().
|
|
334
|
+
const [viewportRef, time] = useAnimationFrame(250);
|
|
335
|
+
void time;
|
|
336
|
+
const tokens = info.preCompactTokenCount;
|
|
337
|
+
// Measured locally: summaries run about 5% of the input (≈1k tokens for a
|
|
338
|
+
// 20k context), bounded to a sane range; 4 chars per token.
|
|
339
|
+
const expectedChars = Math.min(3_000, Math.max(700, tokens * 0.05)) * 4;
|
|
340
|
+
// Prompt processing: ~0.5ms/token on fast backends, floor of 3s.
|
|
341
|
+
const expectedPromptMs = Math.max(3_000, tokens * 0.5);
|
|
342
|
+
const streamed = responseLengthRef.current ?? 0;
|
|
343
|
+
const streamRatio = streamed / expectedChars;
|
|
344
|
+
const elapsed = Math.max(0, Date.now() - info.startedAt);
|
|
345
|
+
const timeRatio = Math.min(1, elapsed / expectedPromptMs) * 0.25;
|
|
346
|
+
const ratio = Math.min(0.99, Math.max(streamed > 0 ? 0.25 + streamRatio * 0.74 : 0, timeRatio));
|
|
347
|
+
const width = Math.max(10, Math.min(40, columns - 10));
|
|
348
|
+
const filled = Math.round(ratio * width);
|
|
349
|
+
const pct = Math.round(ratio * 100);
|
|
350
|
+
return <Box ref={viewportRef} flexDirection="row" paddingLeft={2}>
|
|
351
|
+
<Text color="claudeBlue_FOR_SYSTEM_SPINNER">{'\u25B0'.repeat(filled)}</Text>
|
|
352
|
+
<Text dimColor>{'\u25B1'.repeat(width - filled)} {pct}%</Text>
|
|
353
|
+
</Box>;
|
|
354
|
+
}
|
|
355
|
+
|
|
303
356
|
// Brief/assistant mode spinner: single status line. PromptInput drops its
|
|
304
357
|
// own marginTop when isBriefOnly is active, so this component owns the
|
|
305
358
|
// 2-row footprint between messages and input. Footprint is [blank, content]
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
// The fork ships the TypeScript port of color-diff (src/native-ts) instead of
|
|
2
|
+
// the native color-diff-napi module.
|
|
1
3
|
import {
|
|
2
4
|
ColorDiff,
|
|
3
5
|
ColorFile,
|
|
4
6
|
getSyntaxTheme as nativeGetSyntaxTheme,
|
|
5
7
|
type SyntaxTheme,
|
|
6
|
-
} from 'color-diff
|
|
8
|
+
} from '../../native-ts/color-diff/index.js'
|
|
7
9
|
import { isEnvDefinedFalsy } from '../../utils/envUtils.js'
|
|
8
10
|
|
|
9
11
|
export type ColorModuleUnavailableReason = 'env'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from '../../ink.js'
|
|
3
|
+
import type { SnipBoundaryMessage as SnipBoundary } from '../../services/compact/snipProjection.js'
|
|
4
|
+
|
|
5
|
+
export function SnipBoundaryMessage({
|
|
6
|
+
message,
|
|
7
|
+
}: {
|
|
8
|
+
message: SnipBoundary
|
|
9
|
+
}): React.ReactNode {
|
|
10
|
+
return (
|
|
11
|
+
<Box flexDirection="row" marginTop={1}>
|
|
12
|
+
<Text dimColor>✂ {message.content}</Text>
|
|
13
|
+
</Box>
|
|
14
|
+
)
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from '../../ink.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Renders an incoming <cross-session-message from="uds:..."> prompt as a
|
|
6
|
+
* labeled block instead of raw XML.
|
|
7
|
+
*/
|
|
8
|
+
export function UserCrossSessionMessage({
|
|
9
|
+
addMargin,
|
|
10
|
+
param,
|
|
11
|
+
}: {
|
|
12
|
+
addMargin: boolean
|
|
13
|
+
param: { text: string }
|
|
14
|
+
}): React.ReactNode {
|
|
15
|
+
const from = /from="([^"]*)"/.exec(param.text)?.[1]?.replace(/"/g, '"') ?? 'another session'
|
|
16
|
+
const body = param.text
|
|
17
|
+
.replace(/<cross-session-message[^>]*>/, '')
|
|
18
|
+
.replace(/<\/cross-session-message>/, '')
|
|
19
|
+
.trim()
|
|
20
|
+
return (
|
|
21
|
+
<Box flexDirection="column" marginTop={addMargin ? 1 : 0}>
|
|
22
|
+
<Text dimColor>✉ message from {from}</Text>
|
|
23
|
+
<Text>{body}</Text>
|
|
24
|
+
</Box>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
@@ -31,10 +31,12 @@ import { NotebookEditPermissionRequest } from './NotebookEditPermissionRequest/N
|
|
|
31
31
|
import { PowerShellPermissionRequest } from './PowerShellPermissionRequest/PowerShellPermissionRequest.js';
|
|
32
32
|
import { SkillPermissionRequest } from './SkillPermissionRequest/SkillPermissionRequest.js';
|
|
33
33
|
import { WebFetchPermissionRequest } from './WebFetchPermissionRequest/WebFetchPermissionRequest.js';
|
|
34
|
+
import { ReviewArtifactTool as ReviewArtifactToolImpl } from '../../tools/ReviewArtifactTool/ReviewArtifactTool.js';
|
|
35
|
+
import { ReviewArtifactPermissionRequest as ReviewArtifactPermissionRequestImpl } from './ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js';
|
|
34
36
|
|
|
35
37
|
/* eslint-disable @typescript-eslint/no-require-imports */
|
|
36
|
-
const ReviewArtifactTool = feature('REVIEW_ARTIFACT') ?
|
|
37
|
-
const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT') ?
|
|
38
|
+
const ReviewArtifactTool = feature('REVIEW_ARTIFACT') ? ReviewArtifactToolImpl : null;
|
|
39
|
+
const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT') ? ReviewArtifactPermissionRequestImpl : null;
|
|
38
40
|
const WorkflowTool = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowTool.js') as typeof import('../../tools/WorkflowTool/WorkflowTool.js')).WorkflowTool : null;
|
|
39
41
|
const WorkflowPermissionRequest = feature('WORKFLOW_SCRIPTS') ? (require('../../tools/WorkflowTool/WorkflowPermissionRequest.js') as typeof import('../../tools/WorkflowTool/WorkflowPermissionRequest.js')).WorkflowPermissionRequest : null;
|
|
40
42
|
const MonitorTool = feature('MONITOR_TOOL') ? (require('../../tools/MonitorTool/MonitorTool.js') as typeof import('../../tools/MonitorTool/MonitorTool.js')).MonitorTool : null;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import React, { useMemo } from 'react'
|
|
2
|
+
import { Box, Text } from '../../../ink.js'
|
|
3
|
+
import { resolveArtifactPath } from '../../../tools/ReviewArtifactTool/ReviewArtifactTool.js'
|
|
4
|
+
import { getCwd } from '../../../utils/cwd.js'
|
|
5
|
+
import { relative } from 'path'
|
|
6
|
+
import { type OptionWithDescription, Select } from '../../CustomSelect/select.js'
|
|
7
|
+
import { type UnaryEvent, usePermissionRequestLogging } from '../hooks.js'
|
|
8
|
+
import { PermissionDialog } from '../PermissionDialog.js'
|
|
9
|
+
import type { PermissionRequestProps } from '../PermissionRequest.js'
|
|
10
|
+
import { PermissionRuleExplanation } from '../PermissionRuleExplanation.js'
|
|
11
|
+
import { logUnaryPermissionEvent } from '../utils.js'
|
|
12
|
+
|
|
13
|
+
const PREVIEW_LINES = 18
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Approval dialog for ReviewArtifact: shows the title, target path and the
|
|
17
|
+
* first lines of the report before anything is written to disk.
|
|
18
|
+
*/
|
|
19
|
+
export function ReviewArtifactPermissionRequest({
|
|
20
|
+
toolUseConfirm,
|
|
21
|
+
onDone,
|
|
22
|
+
onReject,
|
|
23
|
+
workerBadge,
|
|
24
|
+
}: PermissionRequestProps): React.ReactNode {
|
|
25
|
+
const input = toolUseConfirm.input as {
|
|
26
|
+
title: string
|
|
27
|
+
content: string
|
|
28
|
+
path?: string
|
|
29
|
+
findings?: number
|
|
30
|
+
severity?: string
|
|
31
|
+
}
|
|
32
|
+
const target = relative(getCwd(), resolveArtifactPath(input))
|
|
33
|
+
const lines = input.content.split('\n')
|
|
34
|
+
const preview = lines.slice(0, PREVIEW_LINES)
|
|
35
|
+
const hidden = lines.length - preview.length
|
|
36
|
+
|
|
37
|
+
const unaryEvent = useMemo<UnaryEvent>(
|
|
38
|
+
() => ({ completion_type: 'tool_use_single', language_name: 'markdown' }),
|
|
39
|
+
[],
|
|
40
|
+
)
|
|
41
|
+
usePermissionRequestLogging(toolUseConfirm, unaryEvent)
|
|
42
|
+
|
|
43
|
+
const options = useMemo((): OptionWithDescription<string>[] => {
|
|
44
|
+
return [
|
|
45
|
+
{ label: 'Yes, write the report', value: 'yes' },
|
|
46
|
+
{
|
|
47
|
+
label: (
|
|
48
|
+
<Text>
|
|
49
|
+
No, and tell Claude what to change <Text bold>(esc)</Text>
|
|
50
|
+
</Text>
|
|
51
|
+
),
|
|
52
|
+
value: 'no',
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
}, [])
|
|
56
|
+
|
|
57
|
+
function onChange(newValue: string): void {
|
|
58
|
+
switch (newValue) {
|
|
59
|
+
case 'yes':
|
|
60
|
+
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'accept')
|
|
61
|
+
toolUseConfirm.onAllow(toolUseConfirm.input, [])
|
|
62
|
+
onDone()
|
|
63
|
+
break
|
|
64
|
+
case 'no':
|
|
65
|
+
logUnaryPermissionEvent('tool_use_single', toolUseConfirm, 'reject')
|
|
66
|
+
toolUseConfirm.onReject()
|
|
67
|
+
onReject()
|
|
68
|
+
onDone()
|
|
69
|
+
break
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<PermissionDialog title="Review artifact" workerBadge={workerBadge}>
|
|
75
|
+
<Box flexDirection="column" paddingX={2} paddingY={1}>
|
|
76
|
+
<Text bold>{input.title}</Text>
|
|
77
|
+
<Text dimColor>
|
|
78
|
+
{target}
|
|
79
|
+
{input.findings !== undefined ? ` · ${input.findings} finding${input.findings === 1 ? '' : 's'}` : ''}
|
|
80
|
+
{input.severity && input.severity !== 'none' ? ` · ${input.severity}` : ''}
|
|
81
|
+
</Text>
|
|
82
|
+
<Box flexDirection="column" marginTop={1} borderStyle="round" borderDimColor paddingX={1}>
|
|
83
|
+
{preview.map((line, i) => (
|
|
84
|
+
<Text key={i}>{line || ' '}</Text>
|
|
85
|
+
))}
|
|
86
|
+
{hidden > 0 && <Text dimColor>… {hidden} more lines</Text>}
|
|
87
|
+
</Box>
|
|
88
|
+
</Box>
|
|
89
|
+
<Box flexDirection="column">
|
|
90
|
+
<PermissionRuleExplanation permissionResult={toolUseConfirm.permissionResult} toolType="tool" />
|
|
91
|
+
<Text>Write this review report to the project?</Text>
|
|
92
|
+
<Select options={options} onChange={onChange} onCancel={() => onChange('no')} />
|
|
93
|
+
</Box>
|
|
94
|
+
</PermissionDialog>
|
|
95
|
+
)
|
|
96
|
+
}
|
package/src/constants/prompts.ts
CHANGED
|
@@ -400,7 +400,29 @@ function getSessionSpecificGuidanceSection(
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
// @[MODEL LAUNCH]: Remove this section when we launch numbat.
|
|
403
|
+
function getLocalModelCommunicationSection(): string {
|
|
404
|
+
return `# Communicating with the user
|
|
405
|
+
You are writing for a person watching a terminal, not logging to a console. They see your text, a one-line summary of each tool call, and the final answer; they do not see your thinking.
|
|
406
|
+
|
|
407
|
+
Flow of a turn:
|
|
408
|
+
- Before your first tool call, say in one short sentence what you are about to do (for example: "Looking at how the login form validates input."). Then call the tools.
|
|
409
|
+
- While working, give a brief update at key moments only: when you find the root cause, when you change direction, or after several tool calls without an update. One sentence each, no colon before a tool call.
|
|
410
|
+
- Finish with a short recap that stands on its own for someone who did not watch you work: what you found, what you changed (files, functions), and what is next or what you need from them. Lead with the outcome.
|
|
411
|
+
|
|
412
|
+
Style:
|
|
413
|
+
- Short, direct sentences in prose. No filler, no restating the request, no "Certainly" openers. Do not describe your own reasoning process.
|
|
414
|
+
- Use Markdown. Put every command, snippet, path list, or error text in a fenced code block with a language tag (\`\`\`bash, \`\`\`ts, \`\`\`json ...) so it renders highlighted. Use bullet lists for parallel items, bold for the first words of a bullet, and headers only for long answers.
|
|
415
|
+
- Refer to code locations as \`path/to/file.ts:123\` so they are clickable.
|
|
416
|
+
- Match the size of the answer to the question: a simple question gets one or two sentences, a review or investigation gets a structured write-up.
|
|
417
|
+
- Answer in the language the user writes in.
|
|
418
|
+
|
|
419
|
+
These instructions apply to user-facing text only, not to code or tool calls.`
|
|
420
|
+
}
|
|
421
|
+
|
|
403
422
|
function getOutputEfficiencySection(): string {
|
|
423
|
+
if (process.env.OPENAI_API_KEY && process.env.USER_TYPE !== 'ant') {
|
|
424
|
+
return getLocalModelCommunicationSection()
|
|
425
|
+
}
|
|
404
426
|
if (process.env.USER_TYPE === 'ant') {
|
|
405
427
|
return `# Communicating with the user
|
|
406
428
|
When sending user-facing text, you're writing for a person, not logging to a console. Assume users can't see most tool calls or thinking - only your text output. Before your first tool call, briefly state what you're about to do. While working, give short updates at key moments: when you find something load-bearing (a bug, a root cause), when changing direction, when you've made progress without an update.
|
|
@@ -823,6 +845,10 @@ function getFunctionResultClearingSection(model: string): string | null {
|
|
|
823
845
|
return null
|
|
824
846
|
}
|
|
825
847
|
const config = getCachedMCConfigForFRC()
|
|
848
|
+
// The cached-microcompact module is absent in this build (Anthropic API only)
|
|
849
|
+
if (!config) {
|
|
850
|
+
return null
|
|
851
|
+
}
|
|
826
852
|
const isModelSupported = config.supportedModels?.some(pattern =>
|
|
827
853
|
model.includes(pattern),
|
|
828
854
|
)
|
package/src/ink/terminal.ts
CHANGED
|
@@ -175,7 +175,15 @@ export function supportsExtendedKeys(): boolean {
|
|
|
175
175
|
* mid-stream. WT_SESSION catches WSL-in-Windows-Terminal where platform
|
|
176
176
|
* is linux but output still routes through conhost. */
|
|
177
177
|
export function hasCursorUpViewportYankBug(): boolean {
|
|
178
|
-
|
|
178
|
+
// Opting in keeps the upstream behaviour (no streamed text, messages shown
|
|
179
|
+
// only when the turn ends). Off by default: it hid all real-time output for
|
|
180
|
+
// WSL users in Windows Terminal, and current Windows Terminal builds do not
|
|
181
|
+
// yank the viewport on cursor-up.
|
|
182
|
+
if (process.env.CLAUDE_CODE_WT_CURSOR_WORKAROUND !== undefined) {
|
|
183
|
+
return process.env.CLAUDE_CODE_WT_CURSOR_WORKAROUND === '1' ||
|
|
184
|
+
process.env.CLAUDE_CODE_WT_CURSOR_WORKAROUND === 'true'
|
|
185
|
+
}
|
|
186
|
+
return process.platform === 'win32' && !process.env.WT_SESSION
|
|
179
187
|
}
|
|
180
188
|
|
|
181
189
|
// Computed once at module load — terminal capabilities don't change mid-session.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'fs'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
import type { AssistantMessage } from '../types/message.js'
|
|
4
|
+
import { logForDebugging } from '../utils/debug.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Background-job bookkeeping (TEMPLATES). A job runs in its own directory
|
|
8
|
+
* (`CLAUDE_JOB_DIR`); after every main-thread turn we classify how the turn
|
|
9
|
+
* ended and write `state.json` there so the job list and notifications can
|
|
10
|
+
* show status without parsing transcripts.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type JobStatus = 'running' | 'completed' | 'needs_input' | 'failed' | 'idle'
|
|
14
|
+
|
|
15
|
+
export type JobState = {
|
|
16
|
+
status: JobStatus
|
|
17
|
+
summary: string
|
|
18
|
+
turnCount: number
|
|
19
|
+
toolCalls: Record<string, number>
|
|
20
|
+
lastTools: string[]
|
|
21
|
+
errors: number
|
|
22
|
+
updatedAt: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const JOB_STATE_FILE = 'state.json'
|
|
26
|
+
export const JOB_ENV_KEY = 'CLAUDE_JOB_DIR'
|
|
27
|
+
|
|
28
|
+
function textOf(msg: AssistantMessage): string {
|
|
29
|
+
const content = msg.message.content
|
|
30
|
+
if (typeof content === 'string') return content
|
|
31
|
+
return content
|
|
32
|
+
.filter((b): b is Extract<typeof b, { type: 'text' }> => b.type === 'text')
|
|
33
|
+
.map(b => b.text)
|
|
34
|
+
.join('\n')
|
|
35
|
+
.trim()
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function looksLikeQuestion(text: string): boolean {
|
|
39
|
+
const tail = text.split('\n').filter(l => l.trim()).slice(-3).join(' ')
|
|
40
|
+
return /\?\s*$/.test(tail) || /\b(should I|do you want|would you like|let me know|which (one|option)|confirm)\b/i.test(tail)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function classifyTurn(assistantMessages: AssistantMessage[]): Omit<JobState, 'turnCount' | 'updatedAt'> {
|
|
44
|
+
const toolCalls: Record<string, number> = {}
|
|
45
|
+
const lastTools: string[] = []
|
|
46
|
+
let errors = 0
|
|
47
|
+
let lastText = ''
|
|
48
|
+
for (const m of assistantMessages) {
|
|
49
|
+
if (m.isApiErrorMessage) errors++
|
|
50
|
+
const content = m.message.content
|
|
51
|
+
if (Array.isArray(content)) {
|
|
52
|
+
for (const b of content) {
|
|
53
|
+
if (b.type === 'tool_use') {
|
|
54
|
+
toolCalls[b.name] = (toolCalls[b.name] ?? 0) + 1
|
|
55
|
+
lastTools.push(b.name)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
const t = textOf(m)
|
|
60
|
+
if (t) lastText = t
|
|
61
|
+
}
|
|
62
|
+
const last = assistantMessages.at(-1)
|
|
63
|
+
let status: JobStatus = 'completed'
|
|
64
|
+
if (!last) status = 'idle'
|
|
65
|
+
else if (last.isApiErrorMessage) status = 'failed'
|
|
66
|
+
else if (lastText && looksLikeQuestion(lastText)) status = 'needs_input'
|
|
67
|
+
else if (lastText && /\b(cannot|can't|unable to|blocked|failed)\b/i.test(lastText.slice(-400)) && errors > 0) status = 'failed'
|
|
68
|
+
return {
|
|
69
|
+
status,
|
|
70
|
+
summary: lastText.slice(0, 600),
|
|
71
|
+
toolCalls,
|
|
72
|
+
lastTools: lastTools.slice(-8),
|
|
73
|
+
errors,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function readJobState(jobDir: string): JobState | null {
|
|
78
|
+
try {
|
|
79
|
+
return JSON.parse(readFileSync(join(jobDir, JOB_STATE_FILE), 'utf8')) as JobState
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function classifyAndWriteState(
|
|
86
|
+
jobDir: string,
|
|
87
|
+
assistantMessages: AssistantMessage[],
|
|
88
|
+
): Promise<JobState> {
|
|
89
|
+
const previous = readJobState(jobDir)
|
|
90
|
+
const classified = classifyTurn(assistantMessages)
|
|
91
|
+
const state: JobState = {
|
|
92
|
+
...classified,
|
|
93
|
+
turnCount: (previous?.turnCount ?? 0) + 1,
|
|
94
|
+
updatedAt: new Date().toISOString(),
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
mkdirSync(jobDir, { recursive: true })
|
|
98
|
+
writeFileSync(join(jobDir, JOB_STATE_FILE), JSON.stringify(state, null, 2))
|
|
99
|
+
} catch (e) {
|
|
100
|
+
logForDebugging(`[job] cannot write ${jobDir}/${JOB_STATE_FILE}: ${String(e)}`)
|
|
101
|
+
}
|
|
102
|
+
return state
|
|
103
|
+
}
|
package/src/memdir/paths.ts
CHANGED
|
@@ -66,8 +66,20 @@ export function isAutoMemoryEnabled(): boolean {
|
|
|
66
66
|
* live inside this helper because feature() only tree-shakes when used
|
|
67
67
|
* directly in an `if` condition.
|
|
68
68
|
*/
|
|
69
|
+
/**
|
|
70
|
+
* Local builds have no remote flag service: background memory extraction is
|
|
71
|
+
* on whenever the OpenAI-compatible backend is used, unless disabled with
|
|
72
|
+
* CLAUDE_CODE_DISABLE_MEMORY_EXTRACTION=1.
|
|
73
|
+
*/
|
|
74
|
+
export function isExtractGateOpen(): boolean {
|
|
75
|
+
if (process.env.OPENAI_API_KEY) {
|
|
76
|
+
return !isEnvTruthy(process.env.CLAUDE_CODE_DISABLE_MEMORY_EXTRACTION)
|
|
77
|
+
}
|
|
78
|
+
return getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
export function isExtractModeActive(): boolean {
|
|
70
|
-
if (!
|
|
82
|
+
if (!isExtractGateOpen()) {
|
|
71
83
|
return false
|
|
72
84
|
}
|
|
73
85
|
return (
|