@wangzhizhi/remi 0.0.1-alpha
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/README.md +9 -0
- package/dist/doctor.js +108 -0
- package/dist/git.js +41 -0
- package/dist/help.js +27 -0
- package/dist/i18n.js +422 -0
- package/dist/index.js +97 -0
- package/dist/initPrompt.js +17 -0
- package/dist/model.js +116 -0
- package/dist/modelSelection.js +34 -0
- package/dist/permissionDisplay.js +46 -0
- package/dist/permissions.js +206 -0
- package/dist/repl.js +346 -0
- package/dist/resume.js +3 -0
- package/dist/setup.js +62 -0
- package/dist/statusline.js +59 -0
- package/dist/style.js +48 -0
- package/dist/syntaxTheme.js +39 -0
- package/dist/tui/RemiApp.js +1756 -0
- package/dist/tui/commands.js +427 -0
- package/dist/tui/index.js +42 -0
- package/dist/tui/renderers/Header.js +28 -0
- package/dist/tui/renderers/MessageList.js +1176 -0
- package/dist/tui/renderers/PromptBox.js +118 -0
- package/dist/tui/renderers/StatusLine.js +124 -0
- package/dist/tui/renderers/WorkingIndicator.js +70 -0
- package/dist/tui/slashCommandHighlight.js +8 -0
- package/dist/tui/theme.js +13 -0
- package/dist/tui/types.js +1 -0
- package/dist/usage.js +66 -0
- package/dist/version.js +5 -0
- package/node_modules/@remi/compact/dist/index.js +389 -0
- package/node_modules/@remi/compact/package.json +8 -0
- package/node_modules/@remi/config/dist/index.js +426 -0
- package/node_modules/@remi/config/package.json +8 -0
- package/node_modules/@remi/core/dist/contextBuilder.js +344 -0
- package/node_modules/@remi/core/dist/directoryOverview.js +359 -0
- package/node_modules/@remi/core/dist/index.js +2843 -0
- package/node_modules/@remi/core/dist/projectInstructions.js +123 -0
- package/node_modules/@remi/core/dist/responseStyles.js +98 -0
- package/node_modules/@remi/core/package.json +8 -0
- package/node_modules/@remi/llm/dist/index.js +804 -0
- package/node_modules/@remi/llm/package.json +8 -0
- package/node_modules/@remi/memory/dist/index.js +312 -0
- package/node_modules/@remi/memory/package.json +8 -0
- package/node_modules/@remi/permissions/dist/index.js +90 -0
- package/node_modules/@remi/permissions/package.json +8 -0
- package/node_modules/@remi/sessions/dist/index.js +370 -0
- package/node_modules/@remi/sessions/package.json +8 -0
- package/node_modules/@remi/skills/dist/index.js +273 -0
- package/node_modules/@remi/skills/package.json +8 -0
- package/node_modules/@remi/terminal-markdown/dist/index.js +1412 -0
- package/node_modules/@remi/terminal-markdown/package.json +8 -0
- package/node_modules/@remi/tools/dist/index.js +3875 -0
- package/node_modules/@remi/tools/package.json +8 -0
- package/package.json +48 -0
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { displayWidth } from '@remi/terminal-markdown';
|
|
4
|
+
import { remiDarkTheme } from '../theme.js';
|
|
5
|
+
import { executableSlashCommandPrefix } from '../slashCommandHighlight.js';
|
|
6
|
+
const maxVisibleCommandHints = 8;
|
|
7
|
+
function rule(width) {
|
|
8
|
+
return '─'.repeat(Math.max(24, width - 2));
|
|
9
|
+
}
|
|
10
|
+
function promptLabel(mode) {
|
|
11
|
+
return mode === 'prompt' ? '>' : `${mode}>`;
|
|
12
|
+
}
|
|
13
|
+
function commandMatches(input, command) {
|
|
14
|
+
if (/\s/.test(input.value)) {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
const query = input.value.trim();
|
|
18
|
+
return query === '/' || command.name.startsWith(query);
|
|
19
|
+
}
|
|
20
|
+
function visibleCommandHints(input) {
|
|
21
|
+
if (!input.value.startsWith('/')) {
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
return (input.commandHints ?? []).filter(command => commandMatches(input, command));
|
|
25
|
+
}
|
|
26
|
+
export function PromptBox({ input, width = 100 }) {
|
|
27
|
+
const commandHints = visibleCommandHints(input);
|
|
28
|
+
const label = promptLabel(input.mode);
|
|
29
|
+
const inputLines = wrapComposerInput(input, width, label);
|
|
30
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Text, { color: remiDarkTheme.border, children: rule(width) }), _jsx(Box, { minHeight: 3, paddingX: 1, paddingY: 1, backgroundColor: remiDarkTheme.composerBackground, flexDirection: "column", children: inputLines.map((line, index) => (_jsx(ComposerLineView, { line: line, label: index === 0 ? label : ' '.repeat(displayWidth(label)), showPrompt: index === 0 }, `${index}-${line.segments.map(segment => `${segment.kind}:${segment.text}`).join('|')}`))) }), input.queuedCommands.length > 0 ? (_jsxs(Text, { color: remiDarkTheme.muted, children: ["queued ", input.queuedCommands.length] })) : null, commandHints.length > 0 ? (_jsx(CommandHints, { commands: commandHints, selectedIndex: input.selectedCommandIndex ?? 0 })) : null, _jsx(Text, { color: remiDarkTheme.border, children: rule(width) })] }));
|
|
31
|
+
}
|
|
32
|
+
function clampCursorPosition(position, length) {
|
|
33
|
+
return Math.max(0, Math.min(length, position));
|
|
34
|
+
}
|
|
35
|
+
function wrapComposerInput(input, width, label) {
|
|
36
|
+
const labelWidth = displayWidth(label);
|
|
37
|
+
const contentWidth = Math.max(8, width - 2 - labelWidth - 1);
|
|
38
|
+
const segments = input.value.length > 0 ? inputValueSegments(input) : placeholderSegments(input.placeholder);
|
|
39
|
+
return wrapSegments(segments, contentWidth);
|
|
40
|
+
}
|
|
41
|
+
function inputValueSegments(input) {
|
|
42
|
+
const chars = Array.from(input.value);
|
|
43
|
+
const cursorPosition = clampCursorPosition(input.cursorPosition ?? chars.length, chars.length);
|
|
44
|
+
const slashCommand = executableSlashCommandPrefix(input.value, input.commandHints ?? []);
|
|
45
|
+
const segments = [];
|
|
46
|
+
for (let index = 0; index < chars.length; index += 1) {
|
|
47
|
+
const char = chars[index] ?? '';
|
|
48
|
+
const kind = slashCommand && index < Array.from(slashCommand.command).length ? 'slash-command' : 'text';
|
|
49
|
+
segments.push({ kind: index === cursorPosition ? 'cursor' : kind, text: char });
|
|
50
|
+
}
|
|
51
|
+
if (cursorPosition === chars.length) {
|
|
52
|
+
segments.push({ kind: 'cursor', text: ' ' });
|
|
53
|
+
}
|
|
54
|
+
return segments;
|
|
55
|
+
}
|
|
56
|
+
function placeholderSegments(placeholder) {
|
|
57
|
+
return [{ kind: 'cursor', text: ' ' }, ...Array.from(placeholder).map(char => ({ kind: 'placeholder', text: char }))];
|
|
58
|
+
}
|
|
59
|
+
function wrapSegments(segments, width) {
|
|
60
|
+
const lines = [];
|
|
61
|
+
let current = [];
|
|
62
|
+
let currentWidth = 0;
|
|
63
|
+
for (const segment of segments) {
|
|
64
|
+
const segmentWidth = Math.max(1, displayWidth(segment.text));
|
|
65
|
+
if (current.length > 0 && currentWidth + segmentWidth > width) {
|
|
66
|
+
lines.push({ segments: current });
|
|
67
|
+
current = [];
|
|
68
|
+
currentWidth = 0;
|
|
69
|
+
}
|
|
70
|
+
current.push(segment);
|
|
71
|
+
currentWidth += segmentWidth;
|
|
72
|
+
}
|
|
73
|
+
lines.push({ segments: current });
|
|
74
|
+
return lines;
|
|
75
|
+
}
|
|
76
|
+
function ComposerLineView({ line, label, showPrompt }) {
|
|
77
|
+
return (_jsxs(Box, { children: [_jsx(Text, { color: showPrompt ? remiDarkTheme.accent : remiDarkTheme.muted, backgroundColor: remiDarkTheme.composerBackground, bold: showPrompt, children: label }), _jsx(Text, { color: remiDarkTheme.muted, backgroundColor: remiDarkTheme.composerBackground, children: " " }), line.segments.map((segment, index) => (_jsx(ComposerSegmentView, { segment: segment }, `${index}-${segment.kind}-${segment.text}`)))] }));
|
|
78
|
+
}
|
|
79
|
+
function ComposerSegmentView({ segment }) {
|
|
80
|
+
if (segment.kind === 'cursor') {
|
|
81
|
+
return _jsx(Cursor, { char: segment.text });
|
|
82
|
+
}
|
|
83
|
+
if (segment.kind === 'placeholder') {
|
|
84
|
+
return _jsx(Text, { color: remiDarkTheme.muted, backgroundColor: remiDarkTheme.composerBackground, children: segment.text });
|
|
85
|
+
}
|
|
86
|
+
if (segment.kind === 'slash-command') {
|
|
87
|
+
return _jsx(Text, { color: remiDarkTheme.accent, backgroundColor: remiDarkTheme.composerBackground, children: segment.text });
|
|
88
|
+
}
|
|
89
|
+
return _jsx(Text, { color: remiDarkTheme.text, backgroundColor: remiDarkTheme.composerBackground, children: segment.text });
|
|
90
|
+
}
|
|
91
|
+
function Cursor({ char }) {
|
|
92
|
+
return (_jsx(Text, { color: remiDarkTheme.text, backgroundColor: remiDarkTheme.composerBackground, children: cursorGlyph(char) }));
|
|
93
|
+
}
|
|
94
|
+
function cursorGlyph(char) {
|
|
95
|
+
return '█'.repeat(Math.max(1, displayWidth(char ?? ' ')));
|
|
96
|
+
}
|
|
97
|
+
function CommandHints({ commands, selectedIndex }) {
|
|
98
|
+
const normalizedSelectedIndex = normalizeHintIndex(selectedIndex, commands.length);
|
|
99
|
+
const windowStart = commandHintWindowStart(normalizedSelectedIndex, commands.length);
|
|
100
|
+
const visibleCommands = commands.slice(windowStart, windowStart + maxVisibleCommandHints);
|
|
101
|
+
return (_jsx(Box, { flexDirection: "column", marginLeft: 3, children: visibleCommands.map((command, index) => {
|
|
102
|
+
const commandIndex = windowStart + index;
|
|
103
|
+
const isSelected = commandIndex === normalizedSelectedIndex;
|
|
104
|
+
return (_jsxs(Box, { children: [_jsx(Box, { width: 16, children: _jsx(Text, { color: isSelected ? remiDarkTheme.accent : remiDarkTheme.muted, children: command.name }) }), _jsx(Text, { color: isSelected ? remiDarkTheme.accent : remiDarkTheme.muted, wrap: "truncate-end", children: command.description })] }, command.name));
|
|
105
|
+
}) }));
|
|
106
|
+
}
|
|
107
|
+
function commandHintWindowStart(selectedIndex, total) {
|
|
108
|
+
if (total <= maxVisibleCommandHints) {
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
return Math.min(Math.max(0, selectedIndex - maxVisibleCommandHints + 1), total - maxVisibleCommandHints);
|
|
112
|
+
}
|
|
113
|
+
function normalizeHintIndex(index, length) {
|
|
114
|
+
if (length <= 0) {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
return ((index % length) + length) % length;
|
|
118
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { t } from '../../i18n.js';
|
|
5
|
+
import { defaultStatusLineItems } from '../../statusline.js';
|
|
6
|
+
import { formatCompactTokenCount, normalizedTokenUsageTotal, totalInputTokens } from '../../usage.js';
|
|
7
|
+
import { remiDarkTheme } from '../theme.js';
|
|
8
|
+
export function compactPath(path, maxLength) {
|
|
9
|
+
const displayPath = path.replace(homedir(), '~');
|
|
10
|
+
if (displayPath.length <= maxLength) {
|
|
11
|
+
return displayPath;
|
|
12
|
+
}
|
|
13
|
+
const parts = displayPath.split('/').filter(Boolean);
|
|
14
|
+
const tail = parts.slice(-2).join('/');
|
|
15
|
+
const compacted = displayPath.startsWith('~/') ? `~/${tail}` : `.../${tail}`;
|
|
16
|
+
if (compacted.length <= maxLength) {
|
|
17
|
+
return compacted;
|
|
18
|
+
}
|
|
19
|
+
return compacted.slice(0, maxLength);
|
|
20
|
+
}
|
|
21
|
+
export function permissionColor(mode) {
|
|
22
|
+
if (mode === 'bypass') {
|
|
23
|
+
return remiDarkTheme.danger;
|
|
24
|
+
}
|
|
25
|
+
if (mode === 'auto') {
|
|
26
|
+
return remiDarkTheme.warning;
|
|
27
|
+
}
|
|
28
|
+
return remiDarkTheme.success;
|
|
29
|
+
}
|
|
30
|
+
export function formatStatusLine(status, width = 100) {
|
|
31
|
+
return statusLineFields(status, width)
|
|
32
|
+
.map(field => field.text)
|
|
33
|
+
.join(' · ');
|
|
34
|
+
}
|
|
35
|
+
function statusLineFields(status, width = 100) {
|
|
36
|
+
const contentWidth = Math.max(40, width - 2);
|
|
37
|
+
const items = status.statusLineItems ?? defaultStatusLineItems;
|
|
38
|
+
const fields = items.flatMap(item => createStatusLineField(item, status, contentWidth));
|
|
39
|
+
const accepted = [];
|
|
40
|
+
let length = 0;
|
|
41
|
+
for (const field of fields) {
|
|
42
|
+
const nextLength = length + field.text.length + (accepted.length > 0 ? 3 : 0);
|
|
43
|
+
if (nextLength <= contentWidth) {
|
|
44
|
+
accepted.push(field);
|
|
45
|
+
length = nextLength;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (accepted.length === 0) {
|
|
49
|
+
accepted.push({ ...field, text: field.text.slice(0, contentWidth) });
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
return accepted;
|
|
54
|
+
}
|
|
55
|
+
function createStatusLineField(item, status, width) {
|
|
56
|
+
if (item === 'model') {
|
|
57
|
+
return [mutedField(status.model)];
|
|
58
|
+
}
|
|
59
|
+
if (item === 'cwd') {
|
|
60
|
+
return [mutedField(compactPath(status.cwd, Math.max(12, Math.floor(width / 2))))];
|
|
61
|
+
}
|
|
62
|
+
if (item === 'git-branch') {
|
|
63
|
+
return status.git?.branch ? [mutedField(status.git.branch)] : [];
|
|
64
|
+
}
|
|
65
|
+
if (item === 'branch-changes') {
|
|
66
|
+
if (!status.git) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
const added = status.git?.added ?? 0;
|
|
70
|
+
const deleted = status.git?.deleted ?? 0;
|
|
71
|
+
return [mutedField(`+${added} -${deleted}`)];
|
|
72
|
+
}
|
|
73
|
+
if (item === 'context-remaining') {
|
|
74
|
+
const remaining = resolveContextRemainingPercent(status);
|
|
75
|
+
return remaining === undefined ? [] : [mutedField(`${t(status.language, 'statusline.context')} ${contextRemainingBar(remaining)} ${remaining}%`)];
|
|
76
|
+
}
|
|
77
|
+
if (item === 'used-tokens') {
|
|
78
|
+
const totalTokens = status.usage ? normalizedTokenUsageTotal(status.usage) : 0;
|
|
79
|
+
return [mutedField(`${formatCompactTokenCount(totalTokens)} ${t(status.language, 'statusline.used')}`)];
|
|
80
|
+
}
|
|
81
|
+
if (item === 'total-input-tokens') {
|
|
82
|
+
const inputTokens = status.usage ? totalInputTokens(status.usage) : 0;
|
|
83
|
+
return [mutedField(`${formatCompactTokenCount(inputTokens)} ${t(status.language, 'statusline.input')}`)];
|
|
84
|
+
}
|
|
85
|
+
if (item === 'total-output-tokens') {
|
|
86
|
+
const outputTokens = status.usage?.outputTokens ?? 0;
|
|
87
|
+
return [mutedField(`${formatCompactTokenCount(outputTokens)} ${t(status.language, 'statusline.output')}`)];
|
|
88
|
+
}
|
|
89
|
+
if (item === 'run-state') {
|
|
90
|
+
if (!status.runState) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const key = status.runState === 'Working' ? 'run.working' : status.runState === 'Thinking' ? 'run.thinking' : 'run.ready';
|
|
94
|
+
return [mutedField(t(status.language, key))];
|
|
95
|
+
}
|
|
96
|
+
if (item === 'permissions') {
|
|
97
|
+
return status.permissionProfile ? [mutedField(status.permissionProfile)] : [];
|
|
98
|
+
}
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
function resolveContextRemainingPercent(status) {
|
|
102
|
+
if (status.contextRemainingPercent !== undefined) {
|
|
103
|
+
return clampPercent(status.contextRemainingPercent);
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
function contextRemainingBar(remainingPercent) {
|
|
108
|
+
const segments = 5;
|
|
109
|
+
const filled = Math.max(0, Math.min(segments, Math.round((remainingPercent / 100) * segments)));
|
|
110
|
+
return `${'█'.repeat(filled)}${'░'.repeat(segments - filled)}`;
|
|
111
|
+
}
|
|
112
|
+
function clampPercent(value) {
|
|
113
|
+
if (!Number.isFinite(value)) {
|
|
114
|
+
return 0;
|
|
115
|
+
}
|
|
116
|
+
return Math.max(0, Math.min(100, Math.round(value)));
|
|
117
|
+
}
|
|
118
|
+
function mutedField(text) {
|
|
119
|
+
return { text, color: remiDarkTheme.muted };
|
|
120
|
+
}
|
|
121
|
+
export function StatusLine({ status, width = 100 }) {
|
|
122
|
+
const fields = statusLineFields(status, width);
|
|
123
|
+
return (_jsx(Box, { flexShrink: 0, children: fields.map((field, index) => (_jsxs(Text, { children: [index > 0 ? _jsx(Text, { color: remiDarkTheme.muted, children: " \u00B7 " }) : null, _jsx(Text, { color: field.color, wrap: "truncate-end", children: field.text })] }, `${field.text}-${index}`))) }));
|
|
124
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Box, Text } from 'ink';
|
|
4
|
+
import { t } from '../../i18n.js';
|
|
5
|
+
import { remiDarkTheme } from '../theme.js';
|
|
6
|
+
export const activityIntervalMs = 80;
|
|
7
|
+
export const activityDotsTickDivisor = 3;
|
|
8
|
+
const flowingHighlightWidth = 4;
|
|
9
|
+
const flowingPauseWidth = 3;
|
|
10
|
+
export const activityBaseColor = '#7dd3fc';
|
|
11
|
+
export const activityHighlightColor = 'white';
|
|
12
|
+
export const activityGlyphs = ['◜', '◠', '◝', '◞', '◡', '◟'];
|
|
13
|
+
const activityTrailColor = '#bae6fd';
|
|
14
|
+
const flowingTrailColors = [activityHighlightColor, activityTrailColor, activityBaseColor, activityBaseColor];
|
|
15
|
+
export function WorkingIndicator({ startedAt, visible, language, label, interruptText, marginTop = 1 }) {
|
|
16
|
+
const tick = useActivityTick(visible, startedAt);
|
|
17
|
+
if (!visible || startedAt === undefined) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const elapsedSeconds = Math.max(1, Math.ceil((Date.now() - startedAt) / 1000));
|
|
21
|
+
const displayLabel = label ?? t(language, 'working.label');
|
|
22
|
+
const displayInterrupt = interruptText ?? t(language, 'working.interrupt');
|
|
23
|
+
return (_jsxs(Box, { flexShrink: 0, marginTop: marginTop, children: [_jsx(ActivityText, { label: displayLabel, tick: tick }), _jsx(Text, { color: remiDarkTheme.muted, children: ` (${elapsedSeconds}s · ${displayInterrupt})` })] }));
|
|
24
|
+
}
|
|
25
|
+
export function ActivityText({ label, tick }) {
|
|
26
|
+
const baseLabel = stripTrailingDots(label);
|
|
27
|
+
return (_jsxs(Text, { children: [_jsx(ActivityGlyph, { tick: tick }), _jsx(Text, { color: activityBaseColor, children: " " }), _jsx(FlowingText, { text: baseLabel, tick: tick }), _jsx(AnimatedDots, { tick: tick })] }));
|
|
28
|
+
}
|
|
29
|
+
export function ActivityGlyph({ tick }) {
|
|
30
|
+
const color = flowingTextColor(0, tick % Math.max(1, flowingHighlightWidth), flowingHighlightWidth);
|
|
31
|
+
return _jsx(Text, { color: color, children: activityGlyphForTick(tick) });
|
|
32
|
+
}
|
|
33
|
+
export function AnimatedDots({ tick }) {
|
|
34
|
+
return _jsx(Text, { color: activityBaseColor, children: activityDotsForTick(tick) });
|
|
35
|
+
}
|
|
36
|
+
export function FlowingText({ text, tick }) {
|
|
37
|
+
const chars = useMemo(() => Array.from(text), [text]);
|
|
38
|
+
const cycle = Math.max(1, chars.length + flowingPauseWidth);
|
|
39
|
+
const head = tick % cycle;
|
|
40
|
+
return (_jsx(_Fragment, { children: chars.map((char, index) => (_jsx(Text, { color: flowingTextColor(index, head, cycle), children: char }, `${index}-${char}`))) }));
|
|
41
|
+
}
|
|
42
|
+
export function useActivityTick(active = true, resetKey) {
|
|
43
|
+
const [tick, setTick] = useState(0);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!active) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
setTick(0);
|
|
49
|
+
const timer = setInterval(() => setTick(current => current + 1), activityIntervalMs);
|
|
50
|
+
return () => clearInterval(timer);
|
|
51
|
+
}, [active, resetKey]);
|
|
52
|
+
return tick;
|
|
53
|
+
}
|
|
54
|
+
export function activityDotsForTick(tick) {
|
|
55
|
+
const dotTick = Math.floor(Math.abs(tick) / activityDotsTickDivisor);
|
|
56
|
+
return '.'.repeat((dotTick % 3) + 1).padEnd(3, ' ');
|
|
57
|
+
}
|
|
58
|
+
export function activityGlyphForTick(tick) {
|
|
59
|
+
return activityGlyphs[Math.abs(tick) % activityGlyphs.length] ?? activityGlyphs[0];
|
|
60
|
+
}
|
|
61
|
+
function stripTrailingDots(label) {
|
|
62
|
+
return label.replace(/\.+$/, '');
|
|
63
|
+
}
|
|
64
|
+
function flowingTextColor(index, head, cycle) {
|
|
65
|
+
const distance = (head - index + cycle) % cycle;
|
|
66
|
+
if (distance >= 0 && distance < flowingHighlightWidth) {
|
|
67
|
+
return flowingTrailColors[distance] ?? remiDarkTheme.text;
|
|
68
|
+
}
|
|
69
|
+
return activityBaseColor;
|
|
70
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function executableSlashCommandPrefix(text, slashCommandHints) {
|
|
2
|
+
const match = /^(\S+)([\s\S]*)$/.exec(text);
|
|
3
|
+
const command = match?.[1];
|
|
4
|
+
if (!command || !command.startsWith('/')) {
|
|
5
|
+
return undefined;
|
|
6
|
+
}
|
|
7
|
+
return slashCommandHints.some(hint => hint.name === command) ? { command, rest: match?.[2] ?? '' } : undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const remiDarkTheme = {
|
|
2
|
+
text: 'white',
|
|
3
|
+
muted: 'gray',
|
|
4
|
+
accent: 'cyan',
|
|
5
|
+
success: 'green',
|
|
6
|
+
warning: 'yellow',
|
|
7
|
+
danger: 'red',
|
|
8
|
+
border: 'gray',
|
|
9
|
+
composerBackground: '#2b3036',
|
|
10
|
+
selection: 'blue',
|
|
11
|
+
tool: 'magenta',
|
|
12
|
+
model: 'cyan',
|
|
13
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/usage.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { t } from './i18n.js';
|
|
2
|
+
export function createEmptyTokenUsage() {
|
|
3
|
+
return {
|
|
4
|
+
inputTokens: 0,
|
|
5
|
+
outputTokens: 0,
|
|
6
|
+
totalTokens: 0,
|
|
7
|
+
cachedInputTokens: 0,
|
|
8
|
+
reasoningOutputTokens: 0,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function addTokenUsage(current, usage) {
|
|
12
|
+
return {
|
|
13
|
+
inputTokens: current.inputTokens + usage.inputTokens,
|
|
14
|
+
outputTokens: current.outputTokens + usage.outputTokens,
|
|
15
|
+
totalTokens: normalizedTokenUsageTotal(current) + normalizedTokenUsageTotal(usage),
|
|
16
|
+
cachedInputTokens: (current.cachedInputTokens ?? 0) + (usage.cachedInputTokens ?? 0),
|
|
17
|
+
reasoningOutputTokens: (current.reasoningOutputTokens ?? 0) + (usage.reasoningOutputTokens ?? 0),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function sumSessionTokenUsage(events) {
|
|
21
|
+
return events.reduce((total, event) => {
|
|
22
|
+
if (event.type !== 'assistant' || !event.usage) {
|
|
23
|
+
return total;
|
|
24
|
+
}
|
|
25
|
+
return addTokenUsage(total, event.usage);
|
|
26
|
+
}, createEmptyTokenUsage());
|
|
27
|
+
}
|
|
28
|
+
export function formatTokenUsage(usage, language) {
|
|
29
|
+
const cached = usage.cachedInputTokens && usage.cachedInputTokens > 0 ? ` (+ ${formatNumber(usage.cachedInputTokens)} ${t(language, 'usage.cached')})` : '';
|
|
30
|
+
const reasoning = usage.reasoningOutputTokens && usage.reasoningOutputTokens > 0 ? ` (${t(language, 'usage.reasoning')} ${formatNumber(usage.reasoningOutputTokens)})` : '';
|
|
31
|
+
return [
|
|
32
|
+
t(language, 'usage.prefix'),
|
|
33
|
+
`total=${formatNumber(normalizedTokenUsageTotal(usage))}`,
|
|
34
|
+
`input=${formatNumber(usage.inputTokens)}${cached}`,
|
|
35
|
+
`output=${formatNumber(usage.outputTokens)}${reasoning}`,
|
|
36
|
+
].join(' ');
|
|
37
|
+
}
|
|
38
|
+
export function totalInputTokens(usage) {
|
|
39
|
+
return usage.inputTokens + (usage.cachedInputTokens ?? 0);
|
|
40
|
+
}
|
|
41
|
+
export function contextRemainingPercentFromUsage(usage, contextWindowTokens) {
|
|
42
|
+
if (!contextWindowTokens || contextWindowTokens <= 0) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const usedPercent = Math.max(0, Math.min(100, Math.round((totalInputTokens(usage) / contextWindowTokens) * 100)));
|
|
46
|
+
return 100 - usedPercent;
|
|
47
|
+
}
|
|
48
|
+
export function normalizedTokenUsageTotal(usage) {
|
|
49
|
+
return Math.max(usage.totalTokens, totalInputTokens(usage) + usage.outputTokens);
|
|
50
|
+
}
|
|
51
|
+
export function formatCompactTokenCount(value) {
|
|
52
|
+
if (value >= 1_000_000) {
|
|
53
|
+
return `${formatCompactDecimal(value / 1_000_000)}M`;
|
|
54
|
+
}
|
|
55
|
+
if (value >= 1_000) {
|
|
56
|
+
return `${formatCompactDecimal(value / 1_000)}K`;
|
|
57
|
+
}
|
|
58
|
+
return String(value);
|
|
59
|
+
}
|
|
60
|
+
function formatNumber(value) {
|
|
61
|
+
return new Intl.NumberFormat('en-US').format(value);
|
|
62
|
+
}
|
|
63
|
+
function formatCompactDecimal(value) {
|
|
64
|
+
const rounded = value >= 10 ? Math.round(value) : Math.round(value * 10) / 10;
|
|
65
|
+
return Number.isInteger(rounded) ? String(rounded) : String(rounded);
|
|
66
|
+
}
|