@xelauvas/xela-cli 0.3.0 → 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.
Files changed (136) hide show
  1. package/local/README.md +112 -0
  2. package/local/start-qwen.sh +47 -0
  3. package/local/start-tunnel.sh +36 -0
  4. package/local/xela-local.sh +139 -0
  5. package/package.json +19 -5
  6. package/run.sh +33 -0
  7. package/src/Tool.ts +1 -1
  8. package/src/_shims/_generated_stubs/_universal_stub.mjs +168 -0
  9. package/src/_shims/_generated_stubs/stub_346f96741420f62caece15b196e233c6.mjs +19 -0
  10. package/src/_shims/_generated_stubs/stub_4b79af2a6ac1af5a53a55fd666b2bbb4.mjs +3 -0
  11. package/src/_shims/_generated_stubs/stub_94345ddeadacc184ff895597ec5abf71.mjs +6 -0
  12. package/src/_shims/_generated_stubs/stub_99fa0bc272643c549d8a2ec6d578436f.mjs +3 -0
  13. package/src/_shims/_generated_stubs/stub_e3451b131fcaf272a2a75270b9a427a3.mjs +9 -0
  14. package/src/_shims/_generated_stubs/text_stub.mjs +1 -0
  15. package/src/_shims/bun_bundle.js +79 -4
  16. package/src/_shims/cjs_stub.cjs +32 -10
  17. package/src/_shims/loader.js +104 -5
  18. package/src/_shims/openai_adapter.ts +446 -230
  19. package/src/_shims/register.js +16 -3
  20. package/src/assistant/gate.ts +15 -0
  21. package/src/assistant/index.ts +89 -0
  22. package/src/bootstrap/state.ts +6 -1
  23. package/src/bridge/peerSessions.ts +18 -0
  24. package/src/cli/print.ts +5 -0
  25. package/src/commands/assistant/assistant.ts +17 -0
  26. package/src/commands/assistant/index.ts +11 -0
  27. package/src/commands/brief.ts +3 -1
  28. package/src/commands/buddy/buddy.ts +88 -0
  29. package/src/commands/buddy/index.ts +12 -0
  30. package/src/commands/model/model.tsx +8 -23
  31. package/src/commands/peers/index.ts +11 -0
  32. package/src/commands/peers/peers.ts +24 -0
  33. package/src/commands/proactive/proactive.ts +64 -0
  34. package/src/commands/proactive.ts +12 -0
  35. package/src/commands/workflows/index.ts +12 -0
  36. package/src/commands/workflows/workflows.ts +36 -0
  37. package/src/commands.ts +4 -8
  38. package/src/components/Messages.tsx +1 -1
  39. package/src/components/ModelPicker.tsx +4 -34
  40. package/src/components/Spinner.tsx +54 -1
  41. package/src/components/StructuredDiff/colorDiff.ts +3 -1
  42. package/src/components/messages/SnipBoundaryMessage.tsx +15 -0
  43. package/src/components/messages/UserCrossSessionMessage.tsx +26 -0
  44. package/src/components/permissions/PermissionRequest.tsx +4 -2
  45. package/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.tsx +96 -0
  46. package/src/constants/prompts.ts +26 -0
  47. package/src/ink/terminal.ts +9 -1
  48. package/src/jobs/classifier.ts +103 -0
  49. package/src/memdir/paths.ts +13 -1
  50. package/src/proactive/index.ts +158 -0
  51. package/src/proactive/useProactive.ts +78 -0
  52. package/src/screens/REPL.tsx +28 -8
  53. package/src/services/api/claude.ts +6 -1
  54. package/src/services/compact/autoCompact.ts +18 -4
  55. package/src/services/compact/cachedMCConfig.ts +1 -0
  56. package/src/services/compact/cachedMicrocompact.ts +113 -0
  57. package/src/services/compact/compact.ts +22 -2
  58. package/src/services/compact/reactiveCompact.ts +251 -0
  59. package/src/services/compact/snipCompact.ts +282 -0
  60. package/src/services/compact/snipProjection.ts +50 -0
  61. package/src/services/contextCollapse/index.ts +322 -0
  62. package/src/services/contextCollapse/operations.ts +187 -0
  63. package/src/services/contextCollapse/persist.ts +40 -0
  64. package/src/services/extractMemories/extractMemories.ts +11 -2
  65. package/src/services/skillSearch/featureCheck.ts +12 -0
  66. package/src/services/skillSearch/localSearch.ts +98 -0
  67. package/src/services/skillSearch/prefetch.ts +125 -0
  68. package/src/services/skillSearch/remoteSkillLoader.ts +9 -0
  69. package/src/services/skillSearch/remoteSkillState.ts +21 -0
  70. package/src/services/skillSearch/signals.ts +7 -0
  71. package/src/services/skillSearch/telemetry.ts +9 -0
  72. package/src/setup.ts +10 -5
  73. package/src/skills/bundled/dream.ts +31 -0
  74. package/src/skills/bundled/hunter.ts +45 -0
  75. package/src/skills/bundled/index.ts +10 -23
  76. package/src/skills/bundled/runSkillGenerator.ts +61 -0
  77. package/src/skills/mcpSkills.ts +101 -0
  78. package/src/tasks/LocalWorkflowTask/LocalWorkflowTask.ts +301 -0
  79. package/src/tasks/MonitorMcpTask/MonitorMcpTask.ts +273 -0
  80. package/src/tasks.ts +22 -8
  81. package/src/tools/AgentTool/runAgent.ts +11 -0
  82. package/src/tools/BriefTool/BriefTool.ts +2 -0
  83. package/src/tools/CtxInspectTool/CtxInspectTool.ts +102 -0
  84. package/src/tools/DiscoverSkillsTool/DiscoverSkillsTool.ts +78 -0
  85. package/src/tools/DiscoverSkillsTool/prompt.ts +3 -0
  86. package/src/tools/ListPeersTool/ListPeersTool.ts +89 -0
  87. package/src/tools/MCPTool/MCPTool.ts +6 -0
  88. package/src/tools/MonitorTool/MonitorTool.ts +273 -0
  89. package/src/tools/MonitorTool/UI.tsx +25 -0
  90. package/src/tools/MonitorTool/events.ts +84 -0
  91. package/src/tools/MonitorTool/prompt.ts +45 -0
  92. package/src/tools/OverflowTestTool/OverflowTestTool.ts +73 -0
  93. package/src/tools/PushNotificationTool/PushNotificationTool.ts +119 -0
  94. package/src/tools/ReviewArtifactTool/ReviewArtifactTool.ts +93 -0
  95. package/src/tools/ReviewArtifactTool/prompt.ts +5 -0
  96. package/src/tools/SendUserFileTool/SendUserFileTool.ts +87 -0
  97. package/src/tools/SendUserFileTool/prompt.ts +7 -0
  98. package/src/tools/SleepTool/SleepTool.ts +126 -0
  99. package/src/tools/SnipTool/SnipTool.ts +90 -0
  100. package/src/tools/SnipTool/prompt.ts +5 -0
  101. package/src/tools/SubscribePRTool/SubscribePRTool.ts +115 -0
  102. package/src/tools/TerminalCaptureTool/TerminalCaptureTool.ts +129 -0
  103. package/src/tools/TerminalCaptureTool/prompt.ts +8 -0
  104. package/src/tools/ToolSearchTool/prompt.ts +1 -1
  105. package/src/tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.ts +99 -0
  106. package/src/tools/WebBrowserTool/WebBrowserPanel.tsx +10 -0
  107. package/src/tools/WebBrowserTool/WebBrowserTool.ts +190 -0
  108. package/src/tools/WebBrowserTool/prompt.ts +16 -0
  109. package/src/tools/WorkflowTool/UI.tsx +27 -0
  110. package/src/tools/WorkflowTool/WorkflowTool.ts +401 -0
  111. package/src/tools/WorkflowTool/bundled/index.ts +88 -0
  112. package/src/tools/WorkflowTool/constants.ts +7 -0
  113. package/src/tools/WorkflowTool/createWorkflowCommand.ts +84 -0
  114. package/src/tools/WorkflowTool/prompt.ts +38 -0
  115. package/src/tools/WorkflowTool/runtime.ts +331 -0
  116. package/src/tools.ts +36 -31
  117. package/src/utils/betas.ts +5 -0
  118. package/src/utils/context.ts +9 -0
  119. package/src/utils/conversationRecovery.ts +1 -1
  120. package/src/utils/forkedAgent.ts +4 -0
  121. package/src/utils/logoV2Utils.ts +7 -3
  122. package/src/utils/messageQueueManager.ts +23 -9
  123. package/src/utils/model/model.ts +10 -9
  124. package/src/utils/model/modelOptions.ts +0 -21
  125. package/src/utils/model/validateModel.ts +0 -19
  126. package/src/utils/permissions/permissionSetup.ts +5 -0
  127. package/src/utils/permissions/permissions.ts +25 -0
  128. package/src/utils/permissions/yolo-classifier-prompts/auto_mode_system_prompt.txt +38 -0
  129. package/src/utils/permissions/yolo-classifier-prompts/permissions_external.txt +25 -0
  130. package/src/utils/permissions/yoloClassifier.ts +28 -6
  131. package/src/utils/plugins/officialMarketplaceStartupCheck.ts +9 -0
  132. package/src/utils/systemThemeWatcher.ts +62 -0
  133. package/src/utils/udsClient.ts +115 -0
  134. package/src/utils/udsMessaging.ts +157 -0
  135. package/src/utils/yaml.ts +251 -5
  136. 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 && !effectiveModelOptions.some(opt => opt.value === initial)) {
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 = [...effectiveModelOptions, t6];
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 = effectiveModelOptions;
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 ?? (providerLoading ? 'Loading models from provider...' : isOpenAIBackend() ? 'Switch between provider models. Select a model or use /model <name> directly.' : "Switch between Claude models. Applies to this session and future Claude Code sessions. For other/previous model names, specify with --model.");
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
- const showClearTip = tipsEnabled && elapsedSnapshot > 1_800_000;
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-napi'
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(/&quot;/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') ? (require('../../tools/ReviewArtifactTool/ReviewArtifactTool.js') as typeof import('../../tools/ReviewArtifactTool/ReviewArtifactTool.js')).ReviewArtifactTool : null;
37
- const ReviewArtifactPermissionRequest = feature('REVIEW_ARTIFACT') ? (require('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js') as typeof import('./ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.js')).ReviewArtifactPermissionRequest : null;
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
+ }
@@ -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
  )
@@ -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
- return process.platform === 'win32' || !!process.env.WT_SESSION
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
+ }
@@ -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 (!getFeatureValue_CACHED_MAY_BE_STALE('tengu_passport_quail', false)) {
82
+ if (!isExtractGateOpen()) {
71
83
  return false
72
84
  }
73
85
  return (