@redplanethq/corebrain 2.8.30 → 2.8.31
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/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 +390 -57
- 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/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) {
|
|
@@ -160,17 +231,27 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
160
231
|
statusLine.setIncognito(conversation.incognito);
|
|
161
232
|
tui.requestRender();
|
|
162
233
|
}
|
|
234
|
+
function applyContext(info) {
|
|
235
|
+
contextBar.setContext(info);
|
|
236
|
+
activeBorderKind = info?.kind ?? null;
|
|
237
|
+
refreshBorder();
|
|
238
|
+
}
|
|
163
239
|
function setTaskContext(task) {
|
|
164
240
|
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));
|
|
241
|
+
if (!task) {
|
|
242
|
+
applyContext(null);
|
|
243
|
+
return;
|
|
172
244
|
}
|
|
173
|
-
|
|
245
|
+
const pill = task.displayId ?? 'TASK';
|
|
246
|
+
const hint = isRecurringTask(task)
|
|
247
|
+
? 'ctrl+o info · ctrl+r runs'
|
|
248
|
+
: 'ctrl+o info';
|
|
249
|
+
applyContext({
|
|
250
|
+
kind: 'task',
|
|
251
|
+
pill,
|
|
252
|
+
subtitle: `${task.title || 'Untitled'} (${task.status})`,
|
|
253
|
+
hint,
|
|
254
|
+
});
|
|
174
255
|
}
|
|
175
256
|
function clearConversation() {
|
|
176
257
|
requestId++; // invalidate any in-flight callbacks
|
|
@@ -234,10 +315,6 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
234
315
|
showResumeSelector();
|
|
235
316
|
return;
|
|
236
317
|
}
|
|
237
|
-
if (trimmed === '/reminders') {
|
|
238
|
-
showReminderList();
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
318
|
if (trimmed === '/tasks') {
|
|
242
319
|
showTaskList();
|
|
243
320
|
return;
|
|
@@ -250,8 +327,16 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
250
327
|
showWidgetsView();
|
|
251
328
|
return;
|
|
252
329
|
}
|
|
253
|
-
if (trimmed === '/
|
|
254
|
-
|
|
330
|
+
if (trimmed === '/new-task') {
|
|
331
|
+
setNewTaskMode(true);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (trimmed === '/catchup') {
|
|
335
|
+
runCatchup();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (trimmed === '/today') {
|
|
339
|
+
openScratchpad();
|
|
255
340
|
return;
|
|
256
341
|
}
|
|
257
342
|
if (trimmed === '/incognito') {
|
|
@@ -259,6 +344,7 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
259
344
|
return;
|
|
260
345
|
}
|
|
261
346
|
if (trimmed === '/exit') {
|
|
347
|
+
statusLine.dispose();
|
|
262
348
|
tui.stop();
|
|
263
349
|
process.stdout.write('\n');
|
|
264
350
|
process.exit(0);
|
|
@@ -268,13 +354,15 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
268
354
|
runMessage(trimmed);
|
|
269
355
|
};
|
|
270
356
|
function hideMainUI() {
|
|
357
|
+
if (statusLine.focused)
|
|
358
|
+
tui.setFocus(editor);
|
|
271
359
|
try {
|
|
272
360
|
tui.removeChild(statusLine);
|
|
273
361
|
}
|
|
274
362
|
catch { /* ignore */ }
|
|
275
|
-
tui.removeChild(
|
|
363
|
+
tui.removeChild(messagesSplit);
|
|
276
364
|
try {
|
|
277
|
-
tui.removeChild(
|
|
365
|
+
tui.removeChild(contextBar);
|
|
278
366
|
}
|
|
279
367
|
catch { /* ignore */ }
|
|
280
368
|
tui.removeChild(editor);
|
|
@@ -282,8 +370,8 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
282
370
|
}
|
|
283
371
|
function restoreMainUI() {
|
|
284
372
|
overlayActive = false;
|
|
285
|
-
tui.addChild(
|
|
286
|
-
tui.addChild(
|
|
373
|
+
tui.addChild(messagesSplit);
|
|
374
|
+
tui.addChild(contextBar);
|
|
287
375
|
tui.addChild(editor);
|
|
288
376
|
tui.addChild(statusLine);
|
|
289
377
|
tui.setFocus(editor);
|
|
@@ -338,17 +426,27 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
338
426
|
: chalk.green('●');
|
|
339
427
|
const header = `${dot} ${chalk.bold(displayName)}${argSummary ? chalk.dim(' (' + argSummary + ')') : ''}`;
|
|
340
428
|
addToMessages(new Text(header, 1, 0));
|
|
341
|
-
// Result preview
|
|
429
|
+
// Result preview — up to 2 non-empty lines, HTML stripped, prefixed
|
|
430
|
+
// with `│` / `└─` to match the streaming ToolCallItem style.
|
|
342
431
|
const resultStr = stringifyToolOutput(part.output);
|
|
343
432
|
if (resultStr) {
|
|
344
|
-
const
|
|
433
|
+
const allLines = resultStr
|
|
345
434
|
.split('\n')
|
|
346
|
-
.
|
|
347
|
-
.
|
|
348
|
-
|
|
349
|
-
|
|
435
|
+
.map(cleanPreviewLine)
|
|
436
|
+
.filter(l => l.length > 0);
|
|
437
|
+
const previewLines = allLines.slice(0, 2);
|
|
438
|
+
const extra = allLines.length - previewLines.length;
|
|
439
|
+
previewLines.forEach((line, idx) => {
|
|
440
|
+
const isLast = idx === previewLines.length - 1 && extra === 0;
|
|
441
|
+
const prefix = isLast ? ' └ ' : ' │ ';
|
|
442
|
+
addToMessages(new Text(chalk.dim(prefix + line), 1, 0));
|
|
443
|
+
});
|
|
444
|
+
if (extra > 0) {
|
|
445
|
+
addToMessages(new Text(chalk.dim(` └ +${extra} more lines`), 1, 0));
|
|
350
446
|
}
|
|
351
447
|
}
|
|
448
|
+
// Breathing room so consecutive tool calls don't run together.
|
|
449
|
+
addToMessages(new Spacer(1));
|
|
352
450
|
any = true;
|
|
353
451
|
continue;
|
|
354
452
|
}
|
|
@@ -357,6 +455,26 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
357
455
|
}
|
|
358
456
|
return any;
|
|
359
457
|
}
|
|
458
|
+
/**
|
|
459
|
+
* Turn one line of a tool's raw output into something that scans in the
|
|
460
|
+
* preview strip: HTML tags stripped, whitespace collapsed, capped to a
|
|
461
|
+
* readable width. Preserves tag *content* so `<p>hello</p>` becomes `hello`
|
|
462
|
+
* (not empty), which is important for HTML-heavy tools like get_scratchpad.
|
|
463
|
+
*/
|
|
464
|
+
function cleanPreviewLine(raw) {
|
|
465
|
+
const stripped = raw
|
|
466
|
+
.replace(/<[^>]+>/g, ' ')
|
|
467
|
+
.replace(/ /gi, ' ')
|
|
468
|
+
.replace(/&/gi, '&')
|
|
469
|
+
.replace(/</gi, '<')
|
|
470
|
+
.replace(/>/gi, '>')
|
|
471
|
+
.replace(/"/gi, '"')
|
|
472
|
+
.replace(/'/gi, "'");
|
|
473
|
+
const collapsed = stripped.replace(/\s+/g, ' ').trim();
|
|
474
|
+
if (collapsed.length <= 120)
|
|
475
|
+
return collapsed;
|
|
476
|
+
return collapsed.slice(0, 120) + '…';
|
|
477
|
+
}
|
|
360
478
|
function summarizeArgs(input) {
|
|
361
479
|
if (!input || typeof input !== 'object')
|
|
362
480
|
return '';
|
|
@@ -429,16 +547,6 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
429
547
|
loadConversationHistoryIntoMessages(conv.id);
|
|
430
548
|
};
|
|
431
549
|
}
|
|
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
550
|
function showTaskList() {
|
|
443
551
|
hideMainUI();
|
|
444
552
|
const list = new TaskList(baseUrl, apiKey, tui, () => tui.requestRender());
|
|
@@ -507,6 +615,30 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
507
615
|
restoreMainUI();
|
|
508
616
|
};
|
|
509
617
|
}
|
|
618
|
+
function showTaskRunsOverlay(task) {
|
|
619
|
+
hideMainUI();
|
|
620
|
+
const runsView = new TaskRuns(baseUrl, apiKey, task, tui, () => tui.requestRender());
|
|
621
|
+
tui.addChild(runsView);
|
|
622
|
+
tui.setFocus(runsView);
|
|
623
|
+
runsView.onCancel = () => {
|
|
624
|
+
try {
|
|
625
|
+
tui.removeChild(runsView);
|
|
626
|
+
}
|
|
627
|
+
catch { /* ignore */ }
|
|
628
|
+
restoreMainUI();
|
|
629
|
+
};
|
|
630
|
+
runsView.onOpenRun = run => {
|
|
631
|
+
try {
|
|
632
|
+
tui.removeChild(runsView);
|
|
633
|
+
}
|
|
634
|
+
catch { /* ignore */ }
|
|
635
|
+
// Swap the active conversation to the picked run. `openTaskConversation`
|
|
636
|
+
// already handles clearing state, setting the task context, and
|
|
637
|
+
// loading history — reuse it so the flow matches picking a run from
|
|
638
|
+
// `/tasks`.
|
|
639
|
+
openTaskConversation(task, run.id);
|
|
640
|
+
};
|
|
641
|
+
}
|
|
510
642
|
function showIntegrationsView() {
|
|
511
643
|
hideMainUI();
|
|
512
644
|
const view = new IntegrationsView(baseUrl, apiKey, tui, () => tui.requestRender());
|
|
@@ -528,15 +660,130 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
528
660
|
loadBelowInputWidget().then(() => restoreMainUI()).catch(() => restoreMainUI());
|
|
529
661
|
};
|
|
530
662
|
}
|
|
531
|
-
|
|
663
|
+
// ── Scratchpad (/today) ───────────────────────────────────────────────────
|
|
664
|
+
function updateScratchpadContextBar() {
|
|
665
|
+
if (!scratchpadPanel) {
|
|
666
|
+
// If a task was active, keep its context; otherwise clear.
|
|
667
|
+
if (currentTask)
|
|
668
|
+
setTaskContext(currentTask);
|
|
669
|
+
else
|
|
670
|
+
applyContext(null);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
// The ScratchpadPanel already shows the date + nav hints, so an extra
|
|
674
|
+
// context-bar row is just repetition. Keep the visual bar empty but
|
|
675
|
+
// drive the border colour off `activeBorderKind = 'scratchpad'`.
|
|
676
|
+
contextBar.setContext(null);
|
|
677
|
+
activeBorderKind = 'scratchpad';
|
|
678
|
+
refreshBorder();
|
|
679
|
+
}
|
|
680
|
+
function openScratchpad() {
|
|
681
|
+
if (scratchpadPanel) {
|
|
682
|
+
const currentDate = scratchpadPanel.getDate();
|
|
683
|
+
const today = new Date();
|
|
684
|
+
const onToday = currentDate.getUTCFullYear() === today.getUTCFullYear() &&
|
|
685
|
+
currentDate.getUTCMonth() === today.getUTCMonth() &&
|
|
686
|
+
currentDate.getUTCDate() === today.getUTCDate();
|
|
687
|
+
if (onToday) {
|
|
688
|
+
// Toggle off — /today acts as a "close scratchpad" too.
|
|
689
|
+
closeScratchpad();
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
scratchpadPanel.setDate(today);
|
|
693
|
+
updateScratchpadContextBar();
|
|
694
|
+
tui.requestRender();
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
scratchpadPanel = new ScratchpadPanel(baseUrl, apiKey, () => tui.requestRender());
|
|
698
|
+
scratchpadPanel.onDateChange = () => updateScratchpadContextBar();
|
|
699
|
+
messagesSplit.setLeft(scratchpadPanel);
|
|
700
|
+
updateScratchpadContextBar();
|
|
701
|
+
tui.requestRender();
|
|
702
|
+
}
|
|
703
|
+
function closeScratchpad() {
|
|
704
|
+
if (!scratchpadPanel)
|
|
705
|
+
return;
|
|
706
|
+
scratchpadPanel = null;
|
|
707
|
+
messagesSplit.setLeft(null);
|
|
708
|
+
updateScratchpadContextBar();
|
|
709
|
+
tui.requestRender();
|
|
710
|
+
}
|
|
711
|
+
// ── Catchup ───────────────────────────────────────────────────────────────
|
|
712
|
+
function showCatchupView(summary, count) {
|
|
532
713
|
hideMainUI();
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
714
|
+
pendingCatchupSummary = summary;
|
|
715
|
+
const container = new Container();
|
|
716
|
+
catchupViewContainer = container;
|
|
717
|
+
container.addChild(new Spacer(1));
|
|
718
|
+
container.addChild(new Text(chalk.bold.cyan('Catchup') +
|
|
719
|
+
chalk.dim(` · ${count} item${count === 1 ? '' : 's'}`), 1, 0));
|
|
720
|
+
container.addChild(new Spacer(1));
|
|
721
|
+
container.addChild(new Markdown(summary, 1, 0, markdownTheme));
|
|
722
|
+
container.addChild(new Spacer(2));
|
|
723
|
+
container.addChild(new Text(chalk.bold.white(' c') +
|
|
724
|
+
chalk.dim(' continue in a new chat') +
|
|
725
|
+
chalk.dim(' ') +
|
|
726
|
+
chalk.bold.white('esc') +
|
|
727
|
+
chalk.dim(' close'), 1, 0));
|
|
728
|
+
container.addChild(new Spacer(1));
|
|
729
|
+
tui.addChild(container);
|
|
730
|
+
tui.setFocus(container);
|
|
731
|
+
tui.requestRender();
|
|
732
|
+
}
|
|
733
|
+
function closeCatchupView() {
|
|
734
|
+
if (!catchupViewContainer)
|
|
735
|
+
return;
|
|
736
|
+
try {
|
|
737
|
+
tui.removeChild(catchupViewContainer);
|
|
738
|
+
}
|
|
739
|
+
catch { /* ignore */ }
|
|
740
|
+
catchupViewContainer = null;
|
|
741
|
+
pendingCatchupSummary = null;
|
|
742
|
+
restoreMainUI();
|
|
743
|
+
}
|
|
744
|
+
function showCatchupEmptyToast() {
|
|
745
|
+
addToMessages(new Text(chalk.dim('Nothing to catch up on.'), 1, 0));
|
|
746
|
+
addToMessages(new Spacer(1));
|
|
747
|
+
tui.requestRender();
|
|
748
|
+
}
|
|
749
|
+
function runCatchup() {
|
|
750
|
+
if (isProcessing || catchupViewContainer)
|
|
751
|
+
return;
|
|
752
|
+
pendingCatchupSummary = null;
|
|
753
|
+
showLoader();
|
|
754
|
+
summariseCatchup(baseUrl, apiKey)
|
|
755
|
+
.then(({ summary, count }) => {
|
|
756
|
+
removeLoader();
|
|
757
|
+
if (count === 0 || !summary) {
|
|
758
|
+
showCatchupEmptyToast();
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
showCatchupView(summary, count);
|
|
762
|
+
})
|
|
763
|
+
.catch((err) => {
|
|
764
|
+
removeLoader();
|
|
765
|
+
addToMessages(new Text(chalk.red('Catchup failed: ') + chalk.dim(err.message), 1, 0));
|
|
766
|
+
addToMessages(new Spacer(1));
|
|
767
|
+
tui.requestRender();
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
function continueCatchupInNewChat() {
|
|
771
|
+
if (!pendingCatchupSummary)
|
|
772
|
+
return;
|
|
773
|
+
const seed = pendingCatchupSummary;
|
|
774
|
+
if (catchupViewContainer) {
|
|
775
|
+
try {
|
|
776
|
+
tui.removeChild(catchupViewContainer);
|
|
777
|
+
}
|
|
778
|
+
catch { /* ignore */ }
|
|
779
|
+
catchupViewContainer = null;
|
|
538
780
|
restoreMainUI();
|
|
539
|
-
}
|
|
781
|
+
}
|
|
782
|
+
pendingCatchupSummary = null;
|
|
783
|
+
clearConversation();
|
|
784
|
+
editor.setText(seed + '\n\n');
|
|
785
|
+
tui.setFocus(editor);
|
|
786
|
+
tui.requestRender();
|
|
540
787
|
}
|
|
541
788
|
function removeLoader() {
|
|
542
789
|
const idx = conversationComponents.lastIndexOf(loader);
|
|
@@ -556,11 +803,24 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
556
803
|
loader.stop(); // clear any existing interval before restarting
|
|
557
804
|
loader.start();
|
|
558
805
|
}
|
|
806
|
+
function showOutOfCreditsToast() {
|
|
807
|
+
addToMessages(new Text(chalk.red('You’re out of credits. ') +
|
|
808
|
+
chalk.dim('Top up at ') +
|
|
809
|
+
chalk.white(baseUrl + '/settings/billing'), 1, 0));
|
|
810
|
+
addToMessages(new Spacer(1));
|
|
811
|
+
}
|
|
559
812
|
function runMessage(message) {
|
|
813
|
+
if (outOfCredits()) {
|
|
814
|
+
showOutOfCreditsToast();
|
|
815
|
+
tui.requestRender();
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
560
818
|
isProcessing = true;
|
|
561
819
|
editor.disableSubmit = true;
|
|
562
820
|
const myRequestId = ++requestId;
|
|
563
|
-
// User bubble
|
|
821
|
+
// User bubble — leading blank line so consecutive turns don't run
|
|
822
|
+
// into the previous assistant reply.
|
|
823
|
+
addToMessages(new Spacer(1));
|
|
564
824
|
addToMessages(new Text(chalk.bgHex('#3a3a3a').white(' \u276f ' + message + ' '), 0, 0));
|
|
565
825
|
addToMessages(new Spacer(1));
|
|
566
826
|
showLoader();
|
|
@@ -568,6 +828,7 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
568
828
|
let accumulated = '';
|
|
569
829
|
let markdownInserted = false;
|
|
570
830
|
let hadOutput = false;
|
|
831
|
+
let hadErrorMessage = false;
|
|
571
832
|
const callbacks = {
|
|
572
833
|
onTextDelta(delta) {
|
|
573
834
|
if (requestId !== myRequestId)
|
|
@@ -603,12 +864,15 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
603
864
|
if (requestId !== myRequestId)
|
|
604
865
|
return;
|
|
605
866
|
removeLoader();
|
|
606
|
-
if (!hadOutput) {
|
|
867
|
+
if (!hadOutput && !hadErrorMessage) {
|
|
607
868
|
addToMessages(new Text(chalk.gray('(no response)'), 1, 0));
|
|
869
|
+
addToMessages(new Spacer(1));
|
|
608
870
|
}
|
|
609
|
-
addToMessages(new Spacer(1));
|
|
610
871
|
isProcessing = false;
|
|
611
872
|
editor.disableSubmit = false;
|
|
873
|
+
// If the scratchpad panel is open the agent may have called
|
|
874
|
+
// `update_scratchpad` — refresh so the panel reflects new content.
|
|
875
|
+
scratchpadPanel?.refresh().catch(() => { });
|
|
612
876
|
tui.requestRender();
|
|
613
877
|
},
|
|
614
878
|
onAbort() {
|
|
@@ -638,7 +902,29 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
638
902
|
if (requestId !== myRequestId)
|
|
639
903
|
return;
|
|
640
904
|
removeLoader();
|
|
641
|
-
|
|
905
|
+
hadErrorMessage = true;
|
|
906
|
+
const code = err.code;
|
|
907
|
+
const isNoCredits = err instanceof NoCreditsError || code === 'no_credits';
|
|
908
|
+
if (isNoCredits) {
|
|
909
|
+
// Server told us we're out — flip local state so the border
|
|
910
|
+
// turns red immediately and future submits short-circuit.
|
|
911
|
+
if (creditsInfo)
|
|
912
|
+
creditsInfo.available = 0;
|
|
913
|
+
refreshBorder();
|
|
914
|
+
addToMessages(new Text(chalk.red('You’re out of credits. ') +
|
|
915
|
+
chalk.dim('Top up at ') +
|
|
916
|
+
chalk.white(baseUrl + '/settings/billing'), 1, 0));
|
|
917
|
+
}
|
|
918
|
+
else {
|
|
919
|
+
// Empty message often means the server emitted a bare SSE
|
|
920
|
+
// `error` event with no payload — surface something
|
|
921
|
+
// actionable instead of a lone "Error:" line.
|
|
922
|
+
const message = err.message?.trim() || 'Stream ended unexpectedly (no error message from server).';
|
|
923
|
+
addToMessages(new Text(chalk.red('Error: ') + chalk.gray(message), 1, 0));
|
|
924
|
+
// Any stream failure could be a credit gate that raced past
|
|
925
|
+
// our poll — refresh so the border reflects reality.
|
|
926
|
+
refreshCredits().catch(() => { });
|
|
927
|
+
}
|
|
642
928
|
addToMessages(new Spacer(1));
|
|
643
929
|
isProcessing = false;
|
|
644
930
|
editor.disableSubmit = false;
|
|
@@ -703,6 +989,7 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
703
989
|
// ── Global input ──────────────────────────────────────────────────────────
|
|
704
990
|
tui.addInputListener(data => {
|
|
705
991
|
if (data === '\x03') {
|
|
992
|
+
statusLine.dispose();
|
|
706
993
|
tui.stop();
|
|
707
994
|
process.stdout.write('\n');
|
|
708
995
|
process.exit(0);
|
|
@@ -777,6 +1064,43 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
777
1064
|
setNewTaskMode(false);
|
|
778
1065
|
return { consume: true };
|
|
779
1066
|
}
|
|
1067
|
+
// ── Down arrow on empty editor → focus status line zone selector ───
|
|
1068
|
+
if (!overlayActive &&
|
|
1069
|
+
!isProcessing &&
|
|
1070
|
+
!newTaskMode &&
|
|
1071
|
+
!statusLine.focused &&
|
|
1072
|
+
matchesKey(data, Key.down) &&
|
|
1073
|
+
editor.getText().length === 0) {
|
|
1074
|
+
statusLine.setSelectedZone('tasks');
|
|
1075
|
+
tui.setFocus(statusLine);
|
|
1076
|
+
tui.requestRender();
|
|
1077
|
+
return { consume: true };
|
|
1078
|
+
}
|
|
1079
|
+
// ── Catchup view input (only when the modal view is up) ────────────
|
|
1080
|
+
if (catchupViewContainer) {
|
|
1081
|
+
if (data === 'c' || data === 'C') {
|
|
1082
|
+
continueCatchupInNewChat();
|
|
1083
|
+
return { consume: true };
|
|
1084
|
+
}
|
|
1085
|
+
if (matchesKey(data, Key.escape)) {
|
|
1086
|
+
closeCatchupView();
|
|
1087
|
+
return { consume: true };
|
|
1088
|
+
}
|
|
1089
|
+
// Block other keys while the catchup modal is up — no editor,
|
|
1090
|
+
// no accidental slash-commands.
|
|
1091
|
+
return { consume: true };
|
|
1092
|
+
}
|
|
1093
|
+
// ── Scratchpad day nav (Ctrl+P prev · Ctrl+N next) ─────────────────
|
|
1094
|
+
if (scratchpadPanel && !overlayActive && !isProcessing && !statusLine.focused) {
|
|
1095
|
+
if (matchesKey(data, Key.ctrl('p'))) {
|
|
1096
|
+
scratchpadPanel.shiftDays(-1);
|
|
1097
|
+
return { consume: true };
|
|
1098
|
+
}
|
|
1099
|
+
if (matchesKey(data, Key.ctrl('n'))) {
|
|
1100
|
+
scratchpadPanel.shiftDays(1);
|
|
1101
|
+
return { consume: true };
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
780
1104
|
if (matchesKey(data, Key.ctrl('o'))) {
|
|
781
1105
|
if (taskInfoOverlay) {
|
|
782
1106
|
taskInfoOverlay.onCancel?.();
|
|
@@ -795,6 +1119,15 @@ export function startTuiApp(baseUrl, apiKey, version) {
|
|
|
795
1119
|
}
|
|
796
1120
|
return;
|
|
797
1121
|
}
|
|
1122
|
+
// ── Ctrl+R → show runs of the current recurring task ──────────────
|
|
1123
|
+
if (!overlayActive &&
|
|
1124
|
+
!isProcessing &&
|
|
1125
|
+
currentTask &&
|
|
1126
|
+
isRecurringTask(currentTask) &&
|
|
1127
|
+
matchesKey(data, Key.ctrl('r'))) {
|
|
1128
|
+
showTaskRunsOverlay(currentTask);
|
|
1129
|
+
return { consume: true };
|
|
1130
|
+
}
|
|
798
1131
|
if (!overlayActive && matchesKey(data, Key.ctrl('i'))) {
|
|
799
1132
|
toggleIncognito();
|
|
800
1133
|
return;
|