Skip to content

Commit bee6e6a

Browse files
mabry1985Automakerclaude
authored
feat(cli): long-turn recap card + /recap, /insight toggles + setup-first README (#137)
- Recap: appends a "※ where we left off" card after agent turns longer than recap.thresholdSeconds (300s) or recap.thresholdToolCalls (15). Modeled on cc-2.18's awaySummary, but triggered by turn duration/tool count rather than terminal blur. Generated by `generateRecap` (packages/core/src/recap/) using the configured model with a 1-3 sentence prompt; returns null on abort/error so it can never crash a turn. Wired into AppContainer via useRecap. - Settings: new top-level `recap` (enabled, thresholdSeconds, thresholdToolCalls) and `insight` (enabled) keys on the user-scope schema. Both default to enabled — no experimental gate. - Slash commands: new /recap with status|enable|disable subactions. /insight gains the same subactions; bare /insight still runs the report but errors with a hint when disabled. - Rendering: new MessageType.RECAP + HistoryItemRecap, rendered dim with the U+203B (※) marker via RecapMessage. - README: replace hand-written settings.json in the Quick Start with `proto setup` (the existing interactive wizard); keep the JSON example below as advanced/manual setup. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent ee5cd29 commit bee6e6a

13 files changed

Lines changed: 542 additions & 23 deletions

File tree

README.md

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,40 @@ cargo install beads_rust
5151

5252
## Quick Start
5353

54-
### 1. Configure your endpoint
54+
### 1. Run the setup wizard
5555

56-
proto connects to any OpenAI-compatible API. Create `~/.proto/settings.json`:
56+
```bash
57+
proto setup
58+
```
59+
60+
`proto setup` is an interactive wizard that picks a provider (OpenAI, Anthropic, Gemini, or any OpenAI-compatible endpoint), discovers the available models, optionally configures voice/STT, and writes everything to `~/.proto/settings.json` for you. Re-run it any time to switch providers or pick a different default model.
61+
62+
### 2. Set your API key
63+
64+
The wizard tells you which env var it expects (e.g. `OPENAI_API_KEY`). Set it once:
65+
66+
```bash
67+
export OPENAI_API_KEY=sk-your-key-here
68+
```
69+
70+
Or persist it in `~/.proto/.env`:
71+
72+
```
73+
OPENAI_API_KEY=sk-your-key-here
74+
```
75+
76+
### 3. Run proto
77+
78+
```bash
79+
proto # interactive mode
80+
proto -p "explain this codebase" # one-shot mode
81+
```
82+
83+
No auth screen — proto connects directly to your endpoint.
84+
85+
### Manual setup (advanced)
86+
87+
If you'd rather skip the wizard, drop a `~/.proto/settings.json` of your own:
5788

5889
```json
5990
{
@@ -74,25 +105,6 @@ proto connects to any OpenAI-compatible API. Create `~/.proto/settings.json`:
74105
}
75106
```
76107

77-
### 2. Set your API key
78-
79-
Create `~/.proto/.env`:
80-
81-
```
82-
MY_API_KEY=sk-your-key-here
83-
```
84-
85-
Or export it in your shell: `export MY_API_KEY=sk-your-key-here`
86-
87-
### 3. Run proto
88-
89-
```bash
90-
proto # interactive mode
91-
proto -p "explain this codebase" # one-shot mode
92-
```
93-
94-
No auth screen — proto connects directly to your endpoint.
95-
96108
### Example: Multiple models via a gateway
97109

98110
If you run a gateway like [LiteLLM](https://github.com/BerriAI/litellm) in front of multiple providers, register them all under `modelProviders.openai` and switch between them with `/model`:

packages/cli/src/config/settingsSchema.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,72 @@ const SETTINGS_SCHEMA = {
17241724
},
17251725
},
17261726

1727+
recap: {
1728+
type: 'object',
1729+
label: 'Recap',
1730+
category: 'Recap',
1731+
requiresRestart: false,
1732+
default: {},
1733+
description:
1734+
'After a long agent turn, append a short "where we left off" card to the transcript.',
1735+
showInDialog: false,
1736+
properties: {
1737+
enabled: {
1738+
type: 'boolean',
1739+
label: 'Recap Enabled',
1740+
category: 'Recap',
1741+
requiresRestart: false,
1742+
default: true,
1743+
description:
1744+
'When enabled, a 1-3 sentence recap is appended after agent turns that exceed thresholdSeconds or thresholdToolCalls. Toggle with /recap enable|disable.',
1745+
showInDialog: true,
1746+
},
1747+
thresholdSeconds: {
1748+
type: 'number',
1749+
label: 'Recap Duration Threshold (seconds)',
1750+
category: 'Recap',
1751+
requiresRestart: false,
1752+
default: 300,
1753+
description:
1754+
'Minimum agent-turn wall-clock duration (seconds) before a recap fires.',
1755+
showInDialog: true,
1756+
},
1757+
thresholdToolCalls: {
1758+
type: 'number',
1759+
label: 'Recap Tool-Call Threshold',
1760+
category: 'Recap',
1761+
requiresRestart: false,
1762+
default: 15,
1763+
description:
1764+
'Minimum tool-call count in a single turn before a recap fires.',
1765+
showInDialog: true,
1766+
},
1767+
},
1768+
},
1769+
1770+
insight: {
1771+
type: 'object',
1772+
label: 'Insight',
1773+
category: 'Insight',
1774+
requiresRestart: false,
1775+
default: {},
1776+
description:
1777+
'Personalized programming-insight reports generated from your chat history.',
1778+
showInDialog: false,
1779+
properties: {
1780+
enabled: {
1781+
type: 'boolean',
1782+
label: 'Insight Enabled',
1783+
category: 'Insight',
1784+
requiresRestart: false,
1785+
default: true,
1786+
description:
1787+
'When enabled, /insight will generate a personalized HTML report from your session history. Toggle with /insight enable|disable.',
1788+
showInDialog: true,
1789+
},
1790+
},
1791+
},
1792+
17271793
experimental: {
17281794
type: 'object',
17291795
label: 'Experimental',

packages/cli/src/services/BuiltinCommandLoader.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ import { setupCommand } from '../ui/commands/setupCommand.js';
5151
import { insightCommand } from '../ui/commands/insightCommand.js';
5252
import { teamCommand } from '../ui/commands/teamCommand.js';
5353
import { voiceCommand } from '../ui/commands/voiceCommand.js';
54+
import { recapCommand } from '../ui/commands/recapCommand.js';
5455

5556
/**
5657
* Loads the core, hard-coded slash commands that are an integral part
@@ -112,6 +113,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
112113
terminalSetupCommand,
113114
insightCommand,
114115
voiceCommand,
116+
recapCommand,
115117
];
116118

117119
return allDefinitions.filter((cmd): cmd is SlashCommand => cmd !== null);

packages/cli/src/ui/AppContainer.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { clearScreen } from '../utils/stdioHelpers.js';
7070
import { useTextBuffer } from './components/shared/text-buffer.js';
7171
import { useLogger } from './hooks/useLogger.js';
7272
import { useGeminiStream } from './hooks/useGeminiStream.js';
73+
import { useRecap } from './hooks/useRecap.js';
7374
import { useVim } from './hooks/vim.js';
7475
import { isBtwCommand } from './utils/commandUtils.js';
7576
import { type LoadedSettings, SettingScope } from '../config/settings.js';
@@ -768,6 +769,18 @@ export const AppContainer = (props: AppContainerProps) => {
768769
pendingGeminiHistoryItems,
769770
});
770771

772+
// Recap card after long agent turns (※ where we left off)
773+
useRecap({
774+
config,
775+
geminiClient,
776+
streamingState,
777+
history: historyManager.history,
778+
addItem: historyManager.addItem,
779+
enabled: settings.merged.recap?.enabled ?? true,
780+
thresholdSeconds: settings.merged.recap?.thresholdSeconds ?? 300,
781+
thresholdToolCalls: settings.merged.recap?.thresholdToolCalls ?? 15,
782+
});
783+
771784
// Callback for handling final submit (must be after addMessage from useMessageQueue)
772785
const handleFinalSubmit = useCallback(
773786
(submittedValue: string) => {

packages/cli/src/ui/commands/insightCommand.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,52 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type { CommandContext, SlashCommand } from './types.js';
7+
import type {
8+
CommandContext,
9+
SlashCommand,
10+
SlashCommandActionReturn,
11+
} from './types.js';
812
import { CommandKind } from './types.js';
913
import { MessageType } from '../types.js';
1014
import type { HistoryItemInsightProgress } from '../types.js';
1115
import { t } from '../../i18n/index.js';
1216
import { join } from 'path';
1317
import { StaticInsightGenerator } from '../../services/insight/generators/StaticInsightGenerator.js';
1418
import { createDebugLogger, Storage } from '@qwen-code/qwen-code-core';
19+
import { SettingScope } from '../../config/settings.js';
1520
import open from 'open';
1621

1722
const logger = createDebugLogger('DataProcessor');
1823

24+
function statusMessage(context: CommandContext): SlashCommandActionReturn {
25+
const enabled = context.services.settings.merged.insight?.enabled ?? true;
26+
return {
27+
type: 'message',
28+
messageType: 'info',
29+
content: enabled
30+
? 'Insight: enabled. Run /insight to generate a report.'
31+
: 'Insight: disabled. Run /insight enable to turn it back on.',
32+
};
33+
}
34+
35+
function setEnabled(
36+
context: CommandContext,
37+
value: boolean,
38+
): SlashCommandActionReturn {
39+
context.services.settings.setValue(
40+
SettingScope.User,
41+
'insight.enabled',
42+
value,
43+
);
44+
return {
45+
type: 'message',
46+
messageType: 'info',
47+
content: value
48+
? 'Insight enabled. Run /insight to generate a report.'
49+
: 'Insight disabled. Run /insight enable to turn it back on.',
50+
};
51+
}
52+
1953
export const insightCommand: SlashCommand = {
2054
name: 'insight',
2155
get description() {
@@ -24,7 +58,41 @@ export const insightCommand: SlashCommand = {
2458
);
2559
},
2660
kind: CommandKind.BUILT_IN,
61+
subCommands: [
62+
{
63+
name: 'status',
64+
description: 'Show insight enabled status',
65+
kind: CommandKind.BUILT_IN,
66+
action: statusMessage,
67+
},
68+
{
69+
name: 'enable',
70+
description: 'Enable /insight report generation',
71+
kind: CommandKind.BUILT_IN,
72+
action: (context: CommandContext): SlashCommandActionReturn =>
73+
setEnabled(context, true),
74+
},
75+
{
76+
name: 'disable',
77+
description: 'Disable /insight report generation',
78+
kind: CommandKind.BUILT_IN,
79+
action: (context: CommandContext): SlashCommandActionReturn =>
80+
setEnabled(context, false),
81+
},
82+
],
2783
action: async (context: CommandContext) => {
84+
if (context.services.settings.merged.insight?.enabled === false) {
85+
context.ui.addItem(
86+
{
87+
type: MessageType.INFO,
88+
text: t(
89+
'Insight is disabled. Run /insight enable to turn it back on.',
90+
),
91+
},
92+
Date.now(),
93+
);
94+
return;
95+
}
2896
try {
2997
context.ui.setDebugMessage(t('Generating insights...'));
3098

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/**
2+
* @license
3+
* Copyright 2025 protoLabs Studio
4+
* SPDX-License-Identifier: Apache-2.0
5+
*/
6+
7+
import type {
8+
CommandContext,
9+
SlashCommand,
10+
SlashCommandActionReturn,
11+
} from './types.js';
12+
import { CommandKind } from './types.js';
13+
import { SettingScope } from '../../config/settings.js';
14+
15+
const DEFAULT_THRESHOLD_SECONDS = 300;
16+
const DEFAULT_THRESHOLD_TOOL_CALLS = 15;
17+
18+
function statusMessage(context: CommandContext): SlashCommandActionReturn {
19+
const merged = context.services.settings.merged;
20+
const enabled = merged.recap?.enabled ?? true;
21+
const seconds = merged.recap?.thresholdSeconds ?? DEFAULT_THRESHOLD_SECONDS;
22+
const toolCalls =
23+
merged.recap?.thresholdToolCalls ?? DEFAULT_THRESHOLD_TOOL_CALLS;
24+
25+
return {
26+
type: 'message',
27+
messageType: 'info',
28+
content: [
29+
`Recap: ${enabled ? 'enabled' : 'disabled'}`,
30+
`Duration threshold: ${seconds}s`,
31+
`Tool-call threshold: ${toolCalls}`,
32+
enabled
33+
? 'A "where we left off" card will be appended after long agent turns.'
34+
: 'Run /recap enable to turn it back on.',
35+
].join('\n'),
36+
};
37+
}
38+
39+
function setEnabled(
40+
context: CommandContext,
41+
value: boolean,
42+
): SlashCommandActionReturn {
43+
context.services.settings.setValue(SettingScope.User, 'recap.enabled', value);
44+
return {
45+
type: 'message',
46+
messageType: 'info',
47+
content: value
48+
? 'Recap enabled. Long-running turns will produce a "where we left off" card.'
49+
: 'Recap disabled.',
50+
};
51+
}
52+
53+
export const recapCommand: SlashCommand = {
54+
name: 'recap',
55+
description:
56+
'Toggle the long-turn recap card (※ where we left off). Run with no args for status.',
57+
kind: CommandKind.BUILT_IN,
58+
subCommands: [
59+
{
60+
name: 'status',
61+
description: 'Show recap status and thresholds',
62+
kind: CommandKind.BUILT_IN,
63+
action: statusMessage,
64+
},
65+
{
66+
name: 'enable',
67+
description: 'Enable the long-turn recap card',
68+
kind: CommandKind.BUILT_IN,
69+
action: (context: CommandContext): SlashCommandActionReturn =>
70+
setEnabled(context, true),
71+
},
72+
{
73+
name: 'disable',
74+
description: 'Disable the long-turn recap card',
75+
kind: CommandKind.BUILT_IN,
76+
action: (context: CommandContext): SlashCommandActionReturn =>
77+
setEnabled(context, false),
78+
},
79+
],
80+
action: (context: CommandContext): SlashCommandActionReturn =>
81+
statusMessage(context),
82+
};

packages/cli/src/ui/components/HistoryItemDisplay.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { ContextUsage } from './views/ContextUsage.js';
4343
import { ArenaAgentCard, ArenaSessionCard } from './arena/ArenaCards.js';
4444
import { InsightProgressMessage } from './messages/InsightProgressMessage.js';
4545
import { BtwMessage } from './messages/BtwMessage.js';
46+
import { RecapMessage } from './messages/RecapMessage.js';
4647

4748
interface HistoryItemDisplayProps {
4849
item: HistoryItem;
@@ -230,6 +231,9 @@ const HistoryItemDisplayComponent: React.FC<HistoryItemDisplayProps> = ({
230231
{itemForDisplay.type === 'btw' && itemForDisplay.btw && (
231232
<BtwMessage btw={itemForDisplay.btw} />
232233
)}
234+
{itemForDisplay.type === 'recap' && (
235+
<RecapMessage text={itemForDisplay.text} />
236+
)}
233237
</Box>
234238
);
235239
};

0 commit comments

Comments
 (0)