@redplanethq/corebrain 2.8.30 → 2.8.32
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/dist/assets/assets/AppIcon.icns +0 -0
- package/dist/commands/gateway/register.js +1 -1
- package/dist/commands/gateway/register.js.map +1 -1
- package/dist/server/api/server.d.ts.map +1 -1
- package/dist/server/api/server.js +5 -0
- package/dist/server/api/server.js.map +1 -1
- package/dist/server/api/skills/index.d.ts +9 -0
- package/dist/server/api/skills/index.d.ts.map +1 -0
- package/dist/server/api/skills/index.js +12 -0
- package/dist/server/api/skills/index.js.map +1 -0
- package/dist/server/skills/install.d.ts +35 -0
- package/dist/server/skills/install.d.ts.map +1 -0
- package/dist/server/skills/install.js +182 -0
- package/dist/server/skills/install.js.map +1 -0
- package/dist/server/skills/skill-store.d.ts +12 -0
- package/dist/server/skills/skill-store.d.ts.map +1 -0
- package/dist/server/skills/skill-store.js +76 -0
- package/dist/server/skills/skill-store.js.map +1 -0
- package/dist/server/tools/skills-tools.d.ts +14 -0
- package/dist/server/tools/skills-tools.d.ts.map +1 -0
- package/dist/server/tools/skills-tools.js +100 -0
- package/dist/server/tools/skills-tools.js.map +1 -0
- package/dist/tui/chat.d.ts.map +1 -1
- package/dist/tui/chat.js +464 -73
- package/dist/tui/chat.js.map +1 -1
- package/dist/tui/components/approval-bar.d.ts +1 -1
- package/dist/tui/components/approval-bar.d.ts.map +1 -1
- package/dist/tui/components/approval-bar.js +1 -1
- package/dist/tui/components/approval-bar.js.map +1 -1
- package/dist/tui/components/approval-panel.d.ts +1 -1
- package/dist/tui/components/approval-panel.d.ts.map +1 -1
- package/dist/tui/components/approval-panel.js +1 -1
- package/dist/tui/components/approval-panel.js.map +1 -1
- package/dist/tui/components/context-bar.d.ts +26 -0
- package/dist/tui/components/context-bar.d.ts.map +1 -0
- package/dist/tui/components/context-bar.js +45 -0
- package/dist/tui/components/context-bar.js.map +1 -0
- package/dist/tui/components/conversation-selector.d.ts +24 -6
- package/dist/tui/components/conversation-selector.d.ts.map +1 -1
- package/dist/tui/components/conversation-selector.js +187 -117
- package/dist/tui/components/conversation-selector.js.map +1 -1
- package/dist/tui/components/integrations-view.d.ts +1 -1
- package/dist/tui/components/integrations-view.d.ts.map +1 -1
- package/dist/tui/components/integrations-view.js +1 -1
- package/dist/tui/components/integrations-view.js.map +1 -1
- package/dist/tui/components/scratchpad-panel.d.ts +28 -0
- package/dist/tui/components/scratchpad-panel.d.ts.map +1 -0
- package/dist/tui/components/scratchpad-panel.js +117 -0
- package/dist/tui/components/scratchpad-panel.js.map +1 -0
- package/dist/tui/components/split-pane.d.ts +23 -0
- package/dist/tui/components/split-pane.d.ts.map +1 -0
- package/dist/tui/components/split-pane.js +59 -0
- package/dist/tui/components/split-pane.js.map +1 -0
- package/dist/tui/components/status-line.d.ts +35 -6
- package/dist/tui/components/status-line.d.ts.map +1 -1
- package/dist/tui/components/status-line.js +149 -19
- package/dist/tui/components/status-line.js.map +1 -1
- package/dist/tui/components/task-detail.d.ts +1 -1
- package/dist/tui/components/task-detail.d.ts.map +1 -1
- package/dist/tui/components/task-detail.js +1 -1
- package/dist/tui/components/task-detail.js.map +1 -1
- package/dist/tui/components/task-list.d.ts +1 -1
- package/dist/tui/components/task-list.d.ts.map +1 -1
- package/dist/tui/components/task-list.js +5 -6
- package/dist/tui/components/task-list.js.map +1 -1
- package/dist/tui/components/task-runs.d.ts +32 -0
- package/dist/tui/components/task-runs.d.ts.map +1 -0
- package/dist/tui/components/task-runs.js +146 -0
- package/dist/tui/components/task-runs.js.map +1 -0
- package/dist/tui/components/tool-call-item.d.ts +1 -1
- package/dist/tui/components/tool-call-item.d.ts.map +1 -1
- package/dist/tui/components/tool-call-item.js +44 -13
- package/dist/tui/components/tool-call-item.js.map +1 -1
- package/dist/tui/components/widgets-view.d.ts +1 -1
- package/dist/tui/components/widgets-view.d.ts.map +1 -1
- package/dist/tui/components/widgets-view.js +1 -1
- package/dist/tui/components/widgets-view.js.map +1 -1
- package/dist/tui/hooks/use-conversation.d.ts.map +1 -1
- package/dist/tui/hooks/use-conversation.js +44 -8
- package/dist/tui/hooks/use-conversation.js.map +1 -1
- package/dist/tui/utils/avatar.d.ts +1 -1
- package/dist/tui/utils/avatar.d.ts.map +1 -1
- package/dist/tui/utils/avatar.js +1 -1
- package/dist/tui/utils/avatar.js.map +1 -1
- package/dist/tui/utils/bundle-loader.d.ts.map +1 -1
- package/dist/tui/utils/bundle-loader.js +8 -1
- package/dist/tui/utils/bundle-loader.js.map +1 -1
- package/dist/tui/utils/integration-loader.d.ts +1 -1
- package/dist/tui/utils/integration-loader.d.ts.map +1 -1
- package/dist/tui/utils/stream.d.ts +75 -13
- package/dist/tui/utils/stream.d.ts.map +1 -1
- package/dist/tui/utils/stream.js +148 -13
- package/dist/tui/utils/stream.js.map +1 -1
- package/dist/utils/env-bootstrap.js +1 -1
- package/dist/utils/env-bootstrap.js.map +1 -1
- package/dist/utils/setup/docker.js +1 -1
- package/dist/utils/setup/docker.js.map +1 -1
- package/dist/utils/setup/railway.js +1 -1
- package/dist/utils/setup/railway.js.map +1 -1
- package/package.json +5 -4
- package/dist/tui/components/dashboard-view.d.ts +0 -20
- package/dist/tui/components/dashboard-view.d.ts.map +0 -1
- package/dist/tui/components/dashboard-view.js +0 -147
- package/dist/tui/components/dashboard-view.js.map +0 -1
- package/dist/tui/components/reminder-list.d.ts +0 -28
- package/dist/tui/components/reminder-list.d.ts.map +0 -1
- package/dist/tui/components/reminder-list.js +0 -261
- package/dist/tui/components/reminder-list.js.map +0 -1
package/dist/tui/chat.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import { TUI, Text, Editor, ProcessTerminal, Markdown, Loader, Spacer, Container, CombinedAutocompleteProvider, matchesKey, Key, } from '@
|
|
1
|
+
import { TUI, Text, Editor, ProcessTerminal, Markdown, Loader, Spacer, Container, CombinedAutocompleteProvider, matchesKey, Key, } from '@earendil-works/pi-tui';
|
|
2
2
|
import { buildAvatar } from './utils/avatar.js';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { StatusLine } from './components/status-line.js';
|
|
5
5
|
import { editorTheme, markdownTheme } from './themes.js';
|
|
6
6
|
import { createConversation } from './hooks/use-conversation.js';
|
|
7
7
|
import { ConversationSelector } from './components/conversation-selector.js';
|
|
8
|
-
import { ReminderList } from './components/reminder-list.js';
|
|
9
8
|
import { TaskList } from './components/task-list.js';
|
|
10
9
|
import { TaskDetail } from './components/task-detail.js';
|
|
11
10
|
import { IntegrationsView } from './components/integrations-view.js';
|
|
12
11
|
import { WidgetsView } from './components/widgets-view.js';
|
|
13
|
-
import { DashboardView } from './components/dashboard-view.js';
|
|
14
12
|
import { loadWidgetBundle } from './utils/widget-loader.js';
|
|
15
13
|
import { getPreferences } from '../config/preferences.js';
|
|
16
|
-
import { fetchConversationHistory, fetchWorkspace, fetchIntegrationAccounts, createTaskApi } from './utils/stream.js';
|
|
14
|
+
import { fetchConversationHistory, fetchWorkspace, fetchIntegrationAccounts, createTaskApi, isRecurringTask, summariseCatchup, fetchCredits, NoCreditsError } from './utils/stream.js';
|
|
17
15
|
import { getToolDisplayName } from './utils/tool-names.js';
|
|
18
16
|
import { ApprovalPanel } from './components/approval-panel.js';
|
|
17
|
+
import { ContextBar } from './components/context-bar.js';
|
|
18
|
+
import { SplitPane } from './components/split-pane.js';
|
|
19
|
+
import { ScratchpadPanel } from './components/scratchpad-panel.js';
|
|
20
|
+
import { TaskRuns } from './components/task-runs.js';
|
|
19
21
|
export function startTuiApp(baseUrl, apiKey, version) {
|
|
20
22
|
const terminal = new ProcessTerminal();
|
|
21
23
|
const tui = new TUI(terminal);
|
|
@@ -28,20 +30,25 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
28
30
|
tui.addChild(new Spacer(1));
|
|
29
31
|
// ── Messages area ─────────────────────────────────────────────────────────
|
|
30
32
|
const messagesContainer = new Container();
|
|
31
|
-
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
// SplitPane wraps messages so we can slot a scratchpad panel on the left
|
|
34
|
+
// half when `/today` is active. Without a left child it renders the right
|
|
35
|
+
// child at full width, i.e. behaves exactly like the plain container.
|
|
36
|
+
const messagesSplit = new SplitPane(messagesContainer);
|
|
37
|
+
tui.addChild(messagesSplit);
|
|
38
|
+
// ── Context bar (shown above editor when a task/scratchpad is active) ────
|
|
39
|
+
const contextBar = new ContextBar();
|
|
40
|
+
tui.addChild(contextBar);
|
|
35
41
|
// ── Editor ────────────────────────────────────────────────────────────────
|
|
36
42
|
const editor = new Editor(tui, editorTheme);
|
|
37
43
|
editor.setAutocompleteProvider(new CombinedAutocompleteProvider([
|
|
44
|
+
{ name: 'new-task', description: 'Create a new task from the next message' },
|
|
45
|
+
{ name: 'today', description: 'Open today’s scratchpad' },
|
|
46
|
+
{ name: 'catchup', description: 'Summarise unread inbox items' },
|
|
38
47
|
{ name: 'clear', description: 'Clear conversation and start fresh' },
|
|
39
48
|
{ name: 'resume', description: 'Resume a previous conversation' },
|
|
40
|
-
{ name: 'reminders', description: 'View your reminders' },
|
|
41
49
|
{ name: 'tasks', description: 'View and manage your tasks' },
|
|
42
50
|
{ name: 'integrations', description: 'View and connect integrations' },
|
|
43
51
|
{ name: 'widgets', description: 'Configure widgets (below-input & overview)' },
|
|
44
|
-
{ name: 'dashboard', description: 'Show overview widgets in TUI' },
|
|
45
52
|
{
|
|
46
53
|
name: 'incognito',
|
|
47
54
|
description: 'Toggle incognito mode (new conversations only)',
|
|
@@ -49,8 +56,28 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
49
56
|
{ name: 'exit', description: 'Exit CORE' },
|
|
50
57
|
], process.cwd()));
|
|
51
58
|
tui.addChild(editor);
|
|
52
|
-
// ── Status line
|
|
53
|
-
const statusLine = new StatusLine();
|
|
59
|
+
// ── Status line — badges + tasks + widget + G + C (focusable, 2 rows)
|
|
60
|
+
const statusLine = new StatusLine(baseUrl, apiKey, () => tui.requestRender());
|
|
61
|
+
statusLine.onBlur = () => {
|
|
62
|
+
tui.setFocus(editor);
|
|
63
|
+
tui.requestRender();
|
|
64
|
+
};
|
|
65
|
+
statusLine.onActivate = (zone) => {
|
|
66
|
+
tui.setFocus(editor);
|
|
67
|
+
if (zone === 'tasks') {
|
|
68
|
+
showTaskList();
|
|
69
|
+
}
|
|
70
|
+
else if (zone === 'catchup') {
|
|
71
|
+
runCatchup();
|
|
72
|
+
}
|
|
73
|
+
else if (zone === 'gateways') {
|
|
74
|
+
addToMessages(new Text(chalk.dim('Manage gateways: run ') +
|
|
75
|
+
chalk.white('corebrain gateway list') +
|
|
76
|
+
chalk.dim(' outside the chat.'), 1, 0));
|
|
77
|
+
addToMessages(new Spacer(1));
|
|
78
|
+
tui.requestRender();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
54
81
|
tui.addChild(statusLine);
|
|
55
82
|
async function loadBelowInputWidget() {
|
|
56
83
|
statusLine.setWidget(null);
|
|
@@ -108,6 +135,44 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
108
135
|
let autoApproveAll = false;
|
|
109
136
|
let currentTask = null;
|
|
110
137
|
let taskInfoOverlay = null;
|
|
138
|
+
// Pending catchup summary that will seed a new chat when the user presses `c`.
|
|
139
|
+
let pendingCatchupSummary = null;
|
|
140
|
+
// Container holding the catchup summary + hints while catchup view is active.
|
|
141
|
+
let catchupViewContainer = null;
|
|
142
|
+
// Active scratchpad side-panel (set while `/today` is in effect).
|
|
143
|
+
let scratchpadPanel = null;
|
|
144
|
+
// Cached credit balance for the workspace. `null` = not fetched yet.
|
|
145
|
+
// The webapp's `/api/v1/conversation` and `/api/v1/conversation/create`
|
|
146
|
+
// endpoints both server-gate the same check; we mirror it client-side so
|
|
147
|
+
// the input border can turn red immediately instead of waiting for the
|
|
148
|
+
// user to type + submit and see a 402.
|
|
149
|
+
let creditsInfo = null;
|
|
150
|
+
// Which context is driving the "normal" border colour right now. Kept
|
|
151
|
+
// separate from `contextBar.getContext()` because scratchpad mode hides
|
|
152
|
+
// the bar visually but still wants a yellow border.
|
|
153
|
+
let activeBorderKind = null;
|
|
154
|
+
function outOfCredits() {
|
|
155
|
+
if (!creditsInfo)
|
|
156
|
+
return false;
|
|
157
|
+
if (!creditsInfo.billingEnabled)
|
|
158
|
+
return false;
|
|
159
|
+
if (creditsInfo.byok)
|
|
160
|
+
return false;
|
|
161
|
+
return creditsInfo.available <= 0;
|
|
162
|
+
}
|
|
163
|
+
function refreshBorder() {
|
|
164
|
+
editor.borderColor = outOfCredits()
|
|
165
|
+
? chalk.red
|
|
166
|
+
: ContextBar.borderColor(activeBorderKind);
|
|
167
|
+
tui.requestRender();
|
|
168
|
+
}
|
|
169
|
+
async function refreshCredits() {
|
|
170
|
+
const info = await fetchCredits(baseUrl, apiKey);
|
|
171
|
+
if (info) {
|
|
172
|
+
creditsInfo = info;
|
|
173
|
+
refreshBorder();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
111
176
|
const conversation = createConversation(baseUrl, apiKey);
|
|
112
177
|
let avatarComponent = buildAvatar(butlerName, workspaceAccent);
|
|
113
178
|
avatarContainer.addChild(avatarComponent);
|
|
@@ -132,6 +197,12 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
132
197
|
}
|
|
133
198
|
})
|
|
134
199
|
.catch(() => { });
|
|
200
|
+
// Initial credit probe + 60s poll so a top-up done in the webapp reflects
|
|
201
|
+
// back in the CLI within a minute (mirrors the webapp's polling cadence).
|
|
202
|
+
refreshCredits().catch(() => { });
|
|
203
|
+
setInterval(() => {
|
|
204
|
+
refreshCredits().catch(() => { });
|
|
205
|
+
}, 60_000);
|
|
135
206
|
const loader = new Loader(tui, s => chalk.cyan(s), s => chalk.gray(s), 'Thinking...');
|
|
136
207
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
137
208
|
function addToMessages(component) {
|
|
@@ -140,15 +211,23 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
140
211
|
}
|
|
141
212
|
function insertBeforeLoader(component) {
|
|
142
213
|
conversationComponents.push(component);
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
214
|
+
// Insert new content ABOVE the loader when it's visible, otherwise
|
|
215
|
+
// just append. Do NOT re-add the loader here — that used to bypass
|
|
216
|
+
// the loaderVisible flag and stranded the loader in the tree after
|
|
217
|
+
// removeLoader() had already run, which the caller couldn't clear.
|
|
218
|
+
const loaderInTree = loaderVisible;
|
|
219
|
+
if (loaderInTree) {
|
|
220
|
+
try {
|
|
221
|
+
messagesContainer.removeChild(loader);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// ignore
|
|
225
|
+
}
|
|
149
226
|
}
|
|
150
227
|
messagesContainer.addChild(component);
|
|
151
|
-
|
|
228
|
+
if (loaderInTree) {
|
|
229
|
+
messagesContainer.addChild(loader);
|
|
230
|
+
}
|
|
152
231
|
}
|
|
153
232
|
function toggleIncognito() {
|
|
154
233
|
if (conversation.conversationId !== null) {
|
|
@@ -160,17 +239,27 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
160
239
|
statusLine.setIncognito(conversation.incognito);
|
|
161
240
|
tui.requestRender();
|
|
162
241
|
}
|
|
242
|
+
function applyContext(info) {
|
|
243
|
+
contextBar.setContext(info);
|
|
244
|
+
activeBorderKind = info?.kind ?? null;
|
|
245
|
+
refreshBorder();
|
|
246
|
+
}
|
|
163
247
|
function setTaskContext(task) {
|
|
164
248
|
currentTask = task;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const title = chalk.bold.white(task.title || 'Untitled');
|
|
169
|
-
const status = chalk.cyan(`(${task.status})`);
|
|
170
|
-
const hint = chalk.dim(' ctrl+o info');
|
|
171
|
-
taskTitleContainer.addChild(new Text(`${id}${title} ${status}${hint}`, 1, 0));
|
|
249
|
+
if (!task) {
|
|
250
|
+
applyContext(null);
|
|
251
|
+
return;
|
|
172
252
|
}
|
|
173
|
-
|
|
253
|
+
const pill = task.displayId ?? 'TASK';
|
|
254
|
+
const hint = isRecurringTask(task)
|
|
255
|
+
? 'ctrl+o info · ctrl+r runs'
|
|
256
|
+
: 'ctrl+o info';
|
|
257
|
+
applyContext({
|
|
258
|
+
kind: 'task',
|
|
259
|
+
pill,
|
|
260
|
+
subtitle: `${task.title || 'Untitled'} (${task.status})`,
|
|
261
|
+
hint,
|
|
262
|
+
});
|
|
174
263
|
}
|
|
175
264
|
function clearConversation() {
|
|
176
265
|
requestId++; // invalidate any in-flight callbacks
|
|
@@ -234,10 +323,6 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
234
323
|
showResumeSelector();
|
|
235
324
|
return;
|
|
236
325
|
}
|
|
237
|
-
if (trimmed === '/reminders') {
|
|
238
|
-
showReminderList();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
326
|
if (trimmed === '/tasks') {
|
|
242
327
|
showTaskList();
|
|
243
328
|
return;
|
|
@@ -250,8 +335,16 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
250
335
|
showWidgetsView();
|
|
251
336
|
return;
|
|
252
337
|
}
|
|
253
|
-
if (trimmed === '/
|
|
254
|
-
|
|
338
|
+
if (trimmed === '/new-task') {
|
|
339
|
+
setNewTaskMode(true);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if (trimmed === '/catchup') {
|
|
343
|
+
runCatchup();
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
if (trimmed === '/today') {
|
|
347
|
+
openScratchpad();
|
|
255
348
|
return;
|
|
256
349
|
}
|
|
257
350
|
if (trimmed === '/incognito') {
|
|
@@ -259,6 +352,7 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
259
352
|
return;
|
|
260
353
|
}
|
|
261
354
|
if (trimmed === '/exit') {
|
|
355
|
+
statusLine.dispose();
|
|
262
356
|
tui.stop();
|
|
263
357
|
process.stdout.write('\n');
|
|
264
358
|
process.exit(0);
|
|
@@ -268,13 +362,15 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
268
362
|
runMessage(trimmed);
|
|
269
363
|
};
|
|
270
364
|
function hideMainUI() {
|
|
365
|
+
if (statusLine.focused)
|
|
366
|
+
tui.setFocus(editor);
|
|
271
367
|
try {
|
|
272
368
|
tui.removeChild(statusLine);
|
|
273
369
|
}
|
|
274
370
|
catch { /* ignore */ }
|
|
275
|
-
tui.removeChild(
|
|
371
|
+
tui.removeChild(messagesSplit);
|
|
276
372
|
try {
|
|
277
|
-
tui.removeChild(
|
|
373
|
+
tui.removeChild(contextBar);
|
|
278
374
|
}
|
|
279
375
|
catch { /* ignore */ }
|
|
280
376
|
tui.removeChild(editor);
|
|
@@ -282,8 +378,8 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
282
378
|
}
|
|
283
379
|
function restoreMainUI() {
|
|
284
380
|
overlayActive = false;
|
|
285
|
-
tui.addChild(
|
|
286
|
-
tui.addChild(
|
|
381
|
+
tui.addChild(messagesSplit);
|
|
382
|
+
tui.addChild(contextBar);
|
|
287
383
|
tui.addChild(editor);
|
|
288
384
|
tui.addChild(statusLine);
|
|
289
385
|
tui.setFocus(editor);
|
|
@@ -338,17 +434,27 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
338
434
|
: chalk.green('●');
|
|
339
435
|
const header = `${dot} ${chalk.bold(displayName)}${argSummary ? chalk.dim(' (' + argSummary + ')') : ''}`;
|
|
340
436
|
addToMessages(new Text(header, 1, 0));
|
|
341
|
-
// Result preview
|
|
437
|
+
// Result preview — up to 2 non-empty lines, HTML stripped, prefixed
|
|
438
|
+
// with `│` / `└─` to match the streaming ToolCallItem style.
|
|
342
439
|
const resultStr = stringifyToolOutput(part.output);
|
|
343
440
|
if (resultStr) {
|
|
344
|
-
const
|
|
441
|
+
const allLines = resultStr
|
|
345
442
|
.split('\n')
|
|
346
|
-
.
|
|
347
|
-
.
|
|
348
|
-
|
|
349
|
-
|
|
443
|
+
.map(cleanPreviewLine)
|
|
444
|
+
.filter(l => l.length > 0);
|
|
445
|
+
const previewLines = allLines.slice(0, 2);
|
|
446
|
+
const extra = allLines.length - previewLines.length;
|
|
447
|
+
previewLines.forEach((line, idx) => {
|
|
448
|
+
const isLast = idx === previewLines.length - 1 && extra === 0;
|
|
449
|
+
const prefix = isLast ? ' └ ' : ' │ ';
|
|
450
|
+
addToMessages(new Text(chalk.dim(prefix + line), 1, 0));
|
|
451
|
+
});
|
|
452
|
+
if (extra > 0) {
|
|
453
|
+
addToMessages(new Text(chalk.dim(` └ +${extra} more lines`), 1, 0));
|
|
350
454
|
}
|
|
351
455
|
}
|
|
456
|
+
// Breathing room so consecutive tool calls don't run together.
|
|
457
|
+
addToMessages(new Spacer(1));
|
|
352
458
|
any = true;
|
|
353
459
|
continue;
|
|
354
460
|
}
|
|
@@ -357,6 +463,26 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
357
463
|
}
|
|
358
464
|
return any;
|
|
359
465
|
}
|
|
466
|
+
/**
|
|
467
|
+
* Turn one line of a tool's raw output into something that scans in the
|
|
468
|
+
* preview strip: HTML tags stripped, whitespace collapsed, capped to a
|
|
469
|
+
* readable width. Preserves tag *content* so `<p>hello</p>` becomes `hello`
|
|
470
|
+
* (not empty), which is important for HTML-heavy tools like get_scratchpad.
|
|
471
|
+
*/
|
|
472
|
+
function cleanPreviewLine(raw) {
|
|
473
|
+
const stripped = raw
|
|
474
|
+
.replace(/<[^>]+>/g, ' ')
|
|
475
|
+
.replace(/ /gi, ' ')
|
|
476
|
+
.replace(/&/gi, '&')
|
|
477
|
+
.replace(/</gi, '<')
|
|
478
|
+
.replace(/>/gi, '>')
|
|
479
|
+
.replace(/"/gi, '"')
|
|
480
|
+
.replace(/'/gi, "'");
|
|
481
|
+
const collapsed = stripped.replace(/\s+/g, ' ').trim();
|
|
482
|
+
if (collapsed.length <= 120)
|
|
483
|
+
return collapsed;
|
|
484
|
+
return collapsed.slice(0, 120) + '…';
|
|
485
|
+
}
|
|
360
486
|
function summarizeArgs(input) {
|
|
361
487
|
if (!input || typeof input !== 'object')
|
|
362
488
|
return '';
|
|
@@ -429,16 +555,6 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
429
555
|
loadConversationHistoryIntoMessages(conv.id);
|
|
430
556
|
};
|
|
431
557
|
}
|
|
432
|
-
function showReminderList() {
|
|
433
|
-
hideMainUI();
|
|
434
|
-
const list = new ReminderList(baseUrl, apiKey, tui, () => tui.requestRender());
|
|
435
|
-
tui.addChild(list);
|
|
436
|
-
tui.setFocus(list);
|
|
437
|
-
list.onCancel = () => {
|
|
438
|
-
tui.removeChild(list);
|
|
439
|
-
restoreMainUI();
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
558
|
function showTaskList() {
|
|
443
559
|
hideMainUI();
|
|
444
560
|
const list = new TaskList(baseUrl, apiKey, tui, () => tui.requestRender());
|
|
@@ -507,6 +623,30 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
507
623
|
restoreMainUI();
|
|
508
624
|
};
|
|
509
625
|
}
|
|
626
|
+
function showTaskRunsOverlay(task) {
|
|
627
|
+
hideMainUI();
|
|
628
|
+
const runsView = new TaskRuns(baseUrl, apiKey, task, tui, () => tui.requestRender());
|
|
629
|
+
tui.addChild(runsView);
|
|
630
|
+
tui.setFocus(runsView);
|
|
631
|
+
runsView.onCancel = () => {
|
|
632
|
+
try {
|
|
633
|
+
tui.removeChild(runsView);
|
|
634
|
+
}
|
|
635
|
+
catch { /* ignore */ }
|
|
636
|
+
restoreMainUI();
|
|
637
|
+
};
|
|
638
|
+
runsView.onOpenRun = run => {
|
|
639
|
+
try {
|
|
640
|
+
tui.removeChild(runsView);
|
|
641
|
+
}
|
|
642
|
+
catch { /* ignore */ }
|
|
643
|
+
// Swap the active conversation to the picked run. `openTaskConversation`
|
|
644
|
+
// already handles clearing state, setting the task context, and
|
|
645
|
+
// loading history — reuse it so the flow matches picking a run from
|
|
646
|
+
// `/tasks`.
|
|
647
|
+
openTaskConversation(task, run.id);
|
|
648
|
+
};
|
|
649
|
+
}
|
|
510
650
|
function showIntegrationsView() {
|
|
511
651
|
hideMainUI();
|
|
512
652
|
const view = new IntegrationsView(baseUrl, apiKey, tui, () => tui.requestRender());
|
|
@@ -528,17 +668,135 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
528
668
|
loadBelowInputWidget().then(() => restoreMainUI()).catch(() => restoreMainUI());
|
|
529
669
|
};
|
|
530
670
|
}
|
|
531
|
-
|
|
671
|
+
// ── Scratchpad (/today) ───────────────────────────────────────────────────
|
|
672
|
+
function updateScratchpadContextBar() {
|
|
673
|
+
if (!scratchpadPanel) {
|
|
674
|
+
// If a task was active, keep its context; otherwise clear.
|
|
675
|
+
if (currentTask)
|
|
676
|
+
setTaskContext(currentTask);
|
|
677
|
+
else
|
|
678
|
+
applyContext(null);
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// The ScratchpadPanel already shows the date + nav hints, so an extra
|
|
682
|
+
// context-bar row is just repetition. Keep the visual bar empty but
|
|
683
|
+
// drive the border colour off `activeBorderKind = 'scratchpad'`.
|
|
684
|
+
contextBar.setContext(null);
|
|
685
|
+
activeBorderKind = 'scratchpad';
|
|
686
|
+
refreshBorder();
|
|
687
|
+
}
|
|
688
|
+
function openScratchpad() {
|
|
689
|
+
if (scratchpadPanel) {
|
|
690
|
+
const currentDate = scratchpadPanel.getDate();
|
|
691
|
+
const today = new Date();
|
|
692
|
+
const onToday = currentDate.getUTCFullYear() === today.getUTCFullYear() &&
|
|
693
|
+
currentDate.getUTCMonth() === today.getUTCMonth() &&
|
|
694
|
+
currentDate.getUTCDate() === today.getUTCDate();
|
|
695
|
+
if (onToday) {
|
|
696
|
+
// Toggle off — /today acts as a "close scratchpad" too.
|
|
697
|
+
closeScratchpad();
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
scratchpadPanel.setDate(today);
|
|
701
|
+
updateScratchpadContextBar();
|
|
702
|
+
tui.requestRender();
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
scratchpadPanel = new ScratchpadPanel(baseUrl, apiKey, () => tui.requestRender());
|
|
706
|
+
scratchpadPanel.onDateChange = () => updateScratchpadContextBar();
|
|
707
|
+
messagesSplit.setLeft(scratchpadPanel);
|
|
708
|
+
updateScratchpadContextBar();
|
|
709
|
+
tui.requestRender();
|
|
710
|
+
}
|
|
711
|
+
function closeScratchpad() {
|
|
712
|
+
if (!scratchpadPanel)
|
|
713
|
+
return;
|
|
714
|
+
scratchpadPanel = null;
|
|
715
|
+
messagesSplit.setLeft(null);
|
|
716
|
+
updateScratchpadContextBar();
|
|
717
|
+
tui.requestRender();
|
|
718
|
+
}
|
|
719
|
+
// ── Catchup ───────────────────────────────────────────────────────────────
|
|
720
|
+
function showCatchupView(summary, count) {
|
|
532
721
|
hideMainUI();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
722
|
+
pendingCatchupSummary = summary;
|
|
723
|
+
const container = new Container();
|
|
724
|
+
catchupViewContainer = container;
|
|
725
|
+
container.addChild(new Spacer(1));
|
|
726
|
+
container.addChild(new Text(chalk.bold.cyan('Catchup') +
|
|
727
|
+
chalk.dim(` · ${count} item${count === 1 ? '' : 's'}`), 1, 0));
|
|
728
|
+
container.addChild(new Spacer(1));
|
|
729
|
+
container.addChild(new Markdown(summary, 1, 0, markdownTheme));
|
|
730
|
+
container.addChild(new Spacer(2));
|
|
731
|
+
container.addChild(new Text(chalk.bold.white(' c') +
|
|
732
|
+
chalk.dim(' continue in a new chat') +
|
|
733
|
+
chalk.dim(' ') +
|
|
734
|
+
chalk.bold.white('esc') +
|
|
735
|
+
chalk.dim(' close'), 1, 0));
|
|
736
|
+
container.addChild(new Spacer(1));
|
|
737
|
+
tui.addChild(container);
|
|
738
|
+
tui.setFocus(container);
|
|
739
|
+
tui.requestRender();
|
|
740
|
+
}
|
|
741
|
+
function closeCatchupView() {
|
|
742
|
+
if (!catchupViewContainer)
|
|
743
|
+
return;
|
|
744
|
+
try {
|
|
745
|
+
tui.removeChild(catchupViewContainer);
|
|
746
|
+
}
|
|
747
|
+
catch { /* ignore */ }
|
|
748
|
+
catchupViewContainer = null;
|
|
749
|
+
pendingCatchupSummary = null;
|
|
750
|
+
restoreMainUI();
|
|
751
|
+
}
|
|
752
|
+
function showCatchupEmptyToast() {
|
|
753
|
+
addToMessages(new Text(chalk.dim('Nothing to catch up on.'), 1, 0));
|
|
754
|
+
addToMessages(new Spacer(1));
|
|
755
|
+
tui.requestRender();
|
|
756
|
+
}
|
|
757
|
+
function runCatchup() {
|
|
758
|
+
if (isProcessing || catchupViewContainer)
|
|
759
|
+
return;
|
|
760
|
+
pendingCatchupSummary = null;
|
|
761
|
+
showLoader();
|
|
762
|
+
summariseCatchup(baseUrl, apiKey)
|
|
763
|
+
.then(({ summary, count }) => {
|
|
764
|
+
removeLoader();
|
|
765
|
+
if (count === 0 || !summary) {
|
|
766
|
+
showCatchupEmptyToast();
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
showCatchupView(summary, count);
|
|
770
|
+
})
|
|
771
|
+
.catch((err) => {
|
|
772
|
+
removeLoader();
|
|
773
|
+
addToMessages(new Text(chalk.red('Catchup failed: ') + chalk.dim(err.message), 1, 0));
|
|
774
|
+
addToMessages(new Spacer(1));
|
|
775
|
+
tui.requestRender();
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
function continueCatchupInNewChat() {
|
|
779
|
+
if (!pendingCatchupSummary)
|
|
780
|
+
return;
|
|
781
|
+
const seed = pendingCatchupSummary;
|
|
782
|
+
if (catchupViewContainer) {
|
|
783
|
+
try {
|
|
784
|
+
tui.removeChild(catchupViewContainer);
|
|
785
|
+
}
|
|
786
|
+
catch { /* ignore */ }
|
|
787
|
+
catchupViewContainer = null;
|
|
538
788
|
restoreMainUI();
|
|
539
|
-
}
|
|
789
|
+
}
|
|
790
|
+
pendingCatchupSummary = null;
|
|
791
|
+
clearConversation();
|
|
792
|
+
editor.setText(seed + '\n\n');
|
|
793
|
+
tui.setFocus(editor);
|
|
794
|
+
tui.requestRender();
|
|
540
795
|
}
|
|
796
|
+
let loaderVisible = false;
|
|
541
797
|
function removeLoader() {
|
|
798
|
+
if (!loaderVisible)
|
|
799
|
+
return;
|
|
542
800
|
const idx = conversationComponents.lastIndexOf(loader);
|
|
543
801
|
if (idx !== -1)
|
|
544
802
|
conversationComponents.splice(idx, 1);
|
|
@@ -549,18 +807,40 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
549
807
|
// ignore
|
|
550
808
|
}
|
|
551
809
|
loader.stop();
|
|
810
|
+
loaderVisible = false;
|
|
552
811
|
}
|
|
553
812
|
function showLoader() {
|
|
813
|
+
if (loaderVisible) {
|
|
814
|
+
// Already in the tree — just restart the animation in case a
|
|
815
|
+
// previous stop() cleared the interval.
|
|
816
|
+
loader.stop();
|
|
817
|
+
loader.start();
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
554
820
|
conversationComponents.push(loader);
|
|
555
821
|
messagesContainer.addChild(loader);
|
|
556
|
-
loader.stop();
|
|
822
|
+
loader.stop();
|
|
557
823
|
loader.start();
|
|
824
|
+
loaderVisible = true;
|
|
825
|
+
}
|
|
826
|
+
function showOutOfCreditsToast() {
|
|
827
|
+
addToMessages(new Text(chalk.red('You’re out of credits. ') +
|
|
828
|
+
chalk.dim('Top up at ') +
|
|
829
|
+
chalk.white(baseUrl + '/settings/billing'), 1, 0));
|
|
830
|
+
addToMessages(new Spacer(1));
|
|
558
831
|
}
|
|
559
832
|
function runMessage(message) {
|
|
833
|
+
if (outOfCredits()) {
|
|
834
|
+
showOutOfCreditsToast();
|
|
835
|
+
tui.requestRender();
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
560
838
|
isProcessing = true;
|
|
561
839
|
editor.disableSubmit = true;
|
|
562
840
|
const myRequestId = ++requestId;
|
|
563
|
-
// User bubble
|
|
841
|
+
// User bubble — leading blank line so consecutive turns don't run
|
|
842
|
+
// into the previous assistant reply.
|
|
843
|
+
addToMessages(new Spacer(1));
|
|
564
844
|
addToMessages(new Text(chalk.bgHex('#3a3a3a').white(' \u276f ' + message + ' '), 0, 0));
|
|
565
845
|
addToMessages(new Spacer(1));
|
|
566
846
|
showLoader();
|
|
@@ -568,6 +848,42 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
568
848
|
let accumulated = '';
|
|
569
849
|
let markdownInserted = false;
|
|
570
850
|
let hadOutput = false;
|
|
851
|
+
let hadErrorMessage = false;
|
|
852
|
+
let finishFired = false;
|
|
853
|
+
// Safety net: if the server never sends `finish` and stops emitting
|
|
854
|
+
// events for this many ms after any output, treat the turn as done.
|
|
855
|
+
// Otherwise the editor stays disabled forever and the user is stuck.
|
|
856
|
+
const IDLE_TIMEOUT_MS = 5_000;
|
|
857
|
+
let idleTimer = null;
|
|
858
|
+
function clearIdleTimer() {
|
|
859
|
+
if (idleTimer) {
|
|
860
|
+
clearTimeout(idleTimer);
|
|
861
|
+
idleTimer = null;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function fireFinish() {
|
|
865
|
+
if (requestId !== myRequestId)
|
|
866
|
+
return;
|
|
867
|
+
clearIdleTimer();
|
|
868
|
+
removeLoader();
|
|
869
|
+
if (!finishFired) {
|
|
870
|
+
finishFired = true;
|
|
871
|
+
if (!hadOutput && !hadErrorMessage) {
|
|
872
|
+
addToMessages(new Text(chalk.gray('(no response)'), 1, 0));
|
|
873
|
+
addToMessages(new Spacer(1));
|
|
874
|
+
}
|
|
875
|
+
scratchpadPanel?.refresh().catch(() => { });
|
|
876
|
+
}
|
|
877
|
+
isProcessing = false;
|
|
878
|
+
editor.disableSubmit = false;
|
|
879
|
+
tui.requestRender();
|
|
880
|
+
}
|
|
881
|
+
function armIdleTimer() {
|
|
882
|
+
clearIdleTimer();
|
|
883
|
+
if (!hadOutput)
|
|
884
|
+
return; // wait until at least one event before arming
|
|
885
|
+
idleTimer = setTimeout(fireFinish, IDLE_TIMEOUT_MS);
|
|
886
|
+
}
|
|
571
887
|
const callbacks = {
|
|
572
888
|
onTextDelta(delta) {
|
|
573
889
|
if (requestId !== myRequestId)
|
|
@@ -578,8 +894,11 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
578
894
|
insertBeforeLoader(responseMd);
|
|
579
895
|
markdownInserted = true;
|
|
580
896
|
}
|
|
897
|
+
// Re-show the loader if it was hidden after a previous step.
|
|
898
|
+
showLoader();
|
|
581
899
|
responseMd.setText(accumulated);
|
|
582
900
|
tui.requestRender();
|
|
901
|
+
armIdleTimer();
|
|
583
902
|
},
|
|
584
903
|
onToolStart(_id, _name, item) {
|
|
585
904
|
if (requestId !== myRequestId)
|
|
@@ -587,33 +906,35 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
587
906
|
hadOutput = true;
|
|
588
907
|
allToolItems.push(item);
|
|
589
908
|
insertBeforeLoader(item);
|
|
909
|
+
showLoader();
|
|
590
910
|
tui.requestRender();
|
|
911
|
+
armIdleTimer();
|
|
591
912
|
},
|
|
592
913
|
onRerender() {
|
|
593
914
|
if (requestId !== myRequestId)
|
|
594
915
|
return;
|
|
595
916
|
tui.requestRender();
|
|
917
|
+
armIdleTimer();
|
|
596
918
|
},
|
|
597
919
|
onStepFinish() {
|
|
598
920
|
if (requestId !== myRequestId)
|
|
599
921
|
return;
|
|
922
|
+
// The visible turn is done — server-side bookkeeping
|
|
923
|
+
// (persistence, token accounting) can still delay `finish`
|
|
924
|
+
// by seconds. Drop the spinner so the UI reads as "reply
|
|
925
|
+
// delivered". It'll be re-shown by the next text/tool event
|
|
926
|
+
// if another step runs.
|
|
927
|
+
removeLoader();
|
|
600
928
|
tui.requestRender();
|
|
929
|
+
armIdleTimer();
|
|
601
930
|
},
|
|
602
931
|
onFinish() {
|
|
603
|
-
|
|
604
|
-
return;
|
|
605
|
-
removeLoader();
|
|
606
|
-
if (!hadOutput) {
|
|
607
|
-
addToMessages(new Text(chalk.gray('(no response)'), 1, 0));
|
|
608
|
-
}
|
|
609
|
-
addToMessages(new Spacer(1));
|
|
610
|
-
isProcessing = false;
|
|
611
|
-
editor.disableSubmit = false;
|
|
612
|
-
tui.requestRender();
|
|
932
|
+
fireFinish();
|
|
613
933
|
},
|
|
614
934
|
onAbort() {
|
|
615
935
|
if (requestId !== myRequestId)
|
|
616
936
|
return;
|
|
937
|
+
clearIdleTimer();
|
|
617
938
|
removeLoader();
|
|
618
939
|
// Remove pending approval panel if present
|
|
619
940
|
if (pendingApprovalPanel) {
|
|
@@ -637,8 +958,31 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
637
958
|
onError(err) {
|
|
638
959
|
if (requestId !== myRequestId)
|
|
639
960
|
return;
|
|
961
|
+
clearIdleTimer();
|
|
640
962
|
removeLoader();
|
|
641
|
-
|
|
963
|
+
hadErrorMessage = true;
|
|
964
|
+
const code = err.code;
|
|
965
|
+
const isNoCredits = err instanceof NoCreditsError || code === 'no_credits';
|
|
966
|
+
if (isNoCredits) {
|
|
967
|
+
// Server told us we're out — flip local state so the border
|
|
968
|
+
// turns red immediately and future submits short-circuit.
|
|
969
|
+
if (creditsInfo)
|
|
970
|
+
creditsInfo.available = 0;
|
|
971
|
+
refreshBorder();
|
|
972
|
+
addToMessages(new Text(chalk.red('You’re out of credits. ') +
|
|
973
|
+
chalk.dim('Top up at ') +
|
|
974
|
+
chalk.white(baseUrl + '/settings/billing'), 1, 0));
|
|
975
|
+
}
|
|
976
|
+
else {
|
|
977
|
+
// Empty message often means the server emitted a bare SSE
|
|
978
|
+
// `error` event with no payload — surface something
|
|
979
|
+
// actionable instead of a lone "Error:" line.
|
|
980
|
+
const message = err.message?.trim() || 'Stream ended unexpectedly (no error message from server).';
|
|
981
|
+
addToMessages(new Text(chalk.red('Error: ') + chalk.gray(message), 1, 0));
|
|
982
|
+
// Any stream failure could be a credit gate that raced past
|
|
983
|
+
// our poll — refresh so the border reflects reality.
|
|
984
|
+
refreshCredits().catch(() => { });
|
|
985
|
+
}
|
|
642
986
|
addToMessages(new Spacer(1));
|
|
643
987
|
isProcessing = false;
|
|
644
988
|
editor.disableSubmit = false;
|
|
@@ -703,6 +1047,7 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
703
1047
|
// ── Global input ──────────────────────────────────────────────────────────
|
|
704
1048
|
tui.addInputListener(data => {
|
|
705
1049
|
if (data === '\x03') {
|
|
1050
|
+
statusLine.dispose();
|
|
706
1051
|
tui.stop();
|
|
707
1052
|
process.stdout.write('\n');
|
|
708
1053
|
process.exit(0);
|
|
@@ -777,6 +1122,43 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
777
1122
|
setNewTaskMode(false);
|
|
778
1123
|
return { consume: true };
|
|
779
1124
|
}
|
|
1125
|
+
// ── Down arrow on empty editor → focus status line zone selector ───
|
|
1126
|
+
if (!overlayActive &&
|
|
1127
|
+
!isProcessing &&
|
|
1128
|
+
!newTaskMode &&
|
|
1129
|
+
!statusLine.focused &&
|
|
1130
|
+
matchesKey(data, Key.down) &&
|
|
1131
|
+
editor.getText().length === 0) {
|
|
1132
|
+
statusLine.setSelectedZone('tasks');
|
|
1133
|
+
tui.setFocus(statusLine);
|
|
1134
|
+
tui.requestRender();
|
|
1135
|
+
return { consume: true };
|
|
1136
|
+
}
|
|
1137
|
+
// ── Catchup view input (only when the modal view is up) ────────────
|
|
1138
|
+
if (catchupViewContainer) {
|
|
1139
|
+
if (data === 'c' || data === 'C') {
|
|
1140
|
+
continueCatchupInNewChat();
|
|
1141
|
+
return { consume: true };
|
|
1142
|
+
}
|
|
1143
|
+
if (matchesKey(data, Key.escape)) {
|
|
1144
|
+
closeCatchupView();
|
|
1145
|
+
return { consume: true };
|
|
1146
|
+
}
|
|
1147
|
+
// Block other keys while the catchup modal is up — no editor,
|
|
1148
|
+
// no accidental slash-commands.
|
|
1149
|
+
return { consume: true };
|
|
1150
|
+
}
|
|
1151
|
+
// ── Scratchpad day nav (Ctrl+P prev · Ctrl+N next) ─────────────────
|
|
1152
|
+
if (scratchpadPanel && !overlayActive && !isProcessing && !statusLine.focused) {
|
|
1153
|
+
if (matchesKey(data, Key.ctrl('p'))) {
|
|
1154
|
+
scratchpadPanel.shiftDays(-1);
|
|
1155
|
+
return { consume: true };
|
|
1156
|
+
}
|
|
1157
|
+
if (matchesKey(data, Key.ctrl('n'))) {
|
|
1158
|
+
scratchpadPanel.shiftDays(1);
|
|
1159
|
+
return { consume: true };
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
780
1162
|
if (matchesKey(data, Key.ctrl('o'))) {
|
|
781
1163
|
if (taskInfoOverlay) {
|
|
782
1164
|
taskInfoOverlay.onCancel?.();
|
|
@@ -795,6 +1177,15 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
795
1177
|
}
|
|
796
1178
|
return;
|
|
797
1179
|
}
|
|
1180
|
+
// ── Ctrl+R → show runs of the current recurring task ──────────────
|
|
1181
|
+
if (!overlayActive &&
|
|
1182
|
+
!isProcessing &&
|
|
1183
|
+
currentTask &&
|
|
1184
|
+
isRecurringTask(currentTask) &&
|
|
1185
|
+
matchesKey(data, Key.ctrl('r'))) {
|
|
1186
|
+
showTaskRunsOverlay(currentTask);
|
|
1187
|
+
return { consume: true };
|
|
1188
|
+
}
|
|
798
1189
|
if (!overlayActive && matchesKey(data, Key.ctrl('i'))) {
|
|
799
1190
|
toggleIncognito();
|
|
800
1191
|
return;
|