@aragon-agent/cli 0.6.11 → 0.6.13
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/CHANGELOG.md +10 -1
- package/README.md +97 -20
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +151 -60
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +199 -0
- package/dist/agent/queued-messages.d.ts +20 -0
- package/dist/agent/queued-messages.js +46 -0
- package/dist/agent/reducer.d.ts +21 -2
- package/dist/agent/reducer.js +65 -37
- package/dist/cli.js +15 -1
- package/dist/commands/builtins.js +83 -42
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +4 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +13 -11
- package/dist/compaction/context-command.js +5 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +6 -1
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +2 -1
- package/dist/config/load.js +15 -8
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/enter-sequences.d.ts +11 -10
- package/dist/input/enter-sequences.js +29 -27
- package/dist/input/stdin-filter.js +93 -28
- package/dist/logging/install.d.ts +3 -0
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/session/persist.d.ts +6 -7
- package/dist/session/persist.js +6 -9
- package/dist/session/store.d.ts +4 -1
- package/dist/session/validate-session.d.ts +3 -0
- package/dist/session/validate-session.js +207 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +161 -226
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +18 -84
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/PromptInput.d.ts +6 -2
- package/dist/ui/PromptInput.js +91 -105
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +24 -0
- package/dist/ui/QueueStatusRow.js +51 -0
- package/dist/ui/StatusBar.d.ts +15 -149
- package/dist/ui/StatusBar.js +27 -112
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/composer-input.d.ts +28 -0
- package/dist/ui/composer-input.js +46 -0
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +9 -0
- package/dist/ui/editor-reducer.js +31 -15
- package/dist/ui/enter-frames.d.ts +15 -31
- package/dist/ui/enter-frames.js +14 -33
- package/dist/ui/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +109 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +11 -3
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +58 -20
- package/dist/ui/transcript-text.js +20 -12
- package/dist/ui/use-wheel-routing.js +2 -1
- package/package.json +2 -2
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { ModelProfile, ModelProfilesConfig, ModelRole } from '../../config/model-profiles.js';
|
|
3
|
+
import type { Theme } from '../theme.js';
|
|
4
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
5
|
+
export interface ProfileFrameProps {
|
|
6
|
+
theme: Theme;
|
|
7
|
+
caps: TermCapabilities;
|
|
8
|
+
cols: number;
|
|
9
|
+
maxRows: number;
|
|
10
|
+
scrollOffset: number;
|
|
11
|
+
onScrollClamp?: (offset: number) => void;
|
|
12
|
+
}
|
|
13
|
+
/** Move focus into view while accepting App's independent wheel/page scrolling. */
|
|
14
|
+
export declare function useProfileScroll(props: ProfileFrameProps, focus: number): number;
|
|
15
|
+
/** Cut by terminal columns, preserving surrogate pairs and wide names. */
|
|
16
|
+
export declare function clipProfileText(value: string, columns: number): string;
|
|
17
|
+
export declare function profileEndpointSummary(baseUrl: string | null): string;
|
|
18
|
+
interface ModelProfilePickerProps extends ProfileFrameProps {
|
|
19
|
+
profiles: ModelProfilesConfig;
|
|
20
|
+
savedProfiles?: ModelProfilesConfig;
|
|
21
|
+
role: ModelRole;
|
|
22
|
+
manager?: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
apiKeys: Record<string, string | undefined>;
|
|
25
|
+
customProfile?: ModelProfile;
|
|
26
|
+
onSelect: (id: string | null) => void;
|
|
27
|
+
onCreate: () => void;
|
|
28
|
+
onEdit: (profile: ModelProfile) => void;
|
|
29
|
+
onDuplicate: (id: string) => void;
|
|
30
|
+
onDelete: (id: string) => void;
|
|
31
|
+
onCancel: () => void;
|
|
32
|
+
}
|
|
33
|
+
/** Profile selection and explicit management actions share one input owner. */
|
|
34
|
+
export declare function ModelProfilePicker(props: ModelProfilePickerProps): React.ReactElement;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { Text, useInput } from 'ink';
|
|
4
|
+
import stringWidth from 'string-width';
|
|
5
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
6
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
7
|
+
import { stripPasteFrames } from '../paste-frames.js';
|
|
8
|
+
import { stripEnterFrames } from '../enter-frames.js';
|
|
9
|
+
import { profileUsers } from './model-profile-state.js';
|
|
10
|
+
/** Move focus into view while accepting App's independent wheel/page scrolling. */
|
|
11
|
+
export function useProfileScroll(props, focus) {
|
|
12
|
+
const [offset, setOffset] = useState(0);
|
|
13
|
+
useEffect(() => { props.onScrollClamp?.(0); }, []);
|
|
14
|
+
useEffect(() => { setOffset(props.scrollOffset); }, [props.scrollOffset]);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
const height = Math.max(1, props.maxRows - (props.cols >= 48 ? 5 : 3));
|
|
17
|
+
const next = focus < offset ? focus : focus >= offset + height ? focus - height + 1 : offset;
|
|
18
|
+
if (next !== offset) {
|
|
19
|
+
setOffset(next);
|
|
20
|
+
props.onScrollClamp?.(next);
|
|
21
|
+
}
|
|
22
|
+
}, [focus, props.maxRows, props.cols]);
|
|
23
|
+
return offset;
|
|
24
|
+
}
|
|
25
|
+
/** Cut by terminal columns, preserving surrogate pairs and wide names. */
|
|
26
|
+
export function clipProfileText(value, columns) {
|
|
27
|
+
let result = '';
|
|
28
|
+
for (const point of value) {
|
|
29
|
+
if (stringWidth(result + point) > Math.max(0, columns))
|
|
30
|
+
break;
|
|
31
|
+
result += point;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
}
|
|
35
|
+
function uniqueId(profile, entries) {
|
|
36
|
+
let length = Math.min(8, profile.id.length);
|
|
37
|
+
while (length < profile.id.length && entries.some((other) => other.id !== profile.id
|
|
38
|
+
&& other.id.slice(0, length) === profile.id.slice(0, length)))
|
|
39
|
+
length++;
|
|
40
|
+
return profile.id.slice(0, length);
|
|
41
|
+
}
|
|
42
|
+
export function profileEndpointSummary(baseUrl) {
|
|
43
|
+
if (!baseUrl)
|
|
44
|
+
return 'Default URL';
|
|
45
|
+
try {
|
|
46
|
+
const url = new URL(baseUrl);
|
|
47
|
+
return url.username || url.password || url.search || url.hash ? 'Invalid URL' : url.host;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return 'Invalid URL';
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function host(profile) {
|
|
54
|
+
return profileEndpointSummary(profile.baseUrl);
|
|
55
|
+
}
|
|
56
|
+
function usePickerState(props) {
|
|
57
|
+
const [index, setIndex] = useState(0);
|
|
58
|
+
const [selectedId, setSelectedId] = useState(props.profiles.entries[0]?.id);
|
|
59
|
+
const [query, setQuery] = useState('');
|
|
60
|
+
const [searching, setSearching] = useState(false);
|
|
61
|
+
const searchable = props.profiles.entries.length > 8;
|
|
62
|
+
const entries = props.profiles.entries.filter((entry) => `${entry.name} ${entry.provider} ${entry.model}`.toLocaleLowerCase()
|
|
63
|
+
.includes(query.toLocaleLowerCase()));
|
|
64
|
+
const actions = props.manager ? ['New', 'Edit', 'Duplicate', 'Delete'] : ['Create profile'];
|
|
65
|
+
const prefix = props.manager ? 0 : 1;
|
|
66
|
+
const count = entries.length + prefix + actions.length;
|
|
67
|
+
const cursor = Math.min(index, count - 1);
|
|
68
|
+
const focused = entries[cursor - prefix];
|
|
69
|
+
const selected = props.profiles.entries.find((entry) => entry.id === selectedId);
|
|
70
|
+
const offset = useProfileScroll(props, cursor + (searchable ? 1 : 0));
|
|
71
|
+
useEffect(() => { if (focused)
|
|
72
|
+
setSelectedId(focused.id); }, [focused?.id]);
|
|
73
|
+
return { setIndex, query, setQuery, searching, setSearching, searchable, entries,
|
|
74
|
+
actions, prefix, count, cursor, focused, selected, offset };
|
|
75
|
+
}
|
|
76
|
+
function usePickerInput(props, state) {
|
|
77
|
+
const { cursor, focused, selected, actions, prefix, entries, count, searching, searchable, setIndex, setQuery, setSearching } = state;
|
|
78
|
+
function activate() {
|
|
79
|
+
if (!props.manager && cursor === 0) {
|
|
80
|
+
props.onSelect(null);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (focused) {
|
|
84
|
+
if (props.manager)
|
|
85
|
+
setIndex(prefix + entries.length + 1);
|
|
86
|
+
else
|
|
87
|
+
props.onSelect(focused.id);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const action = actions[cursor - prefix - entries.length];
|
|
91
|
+
if (action === 'New' || action === 'Create profile')
|
|
92
|
+
props.onCreate();
|
|
93
|
+
if (action === 'Edit' && selected)
|
|
94
|
+
props.onEdit(selected);
|
|
95
|
+
if (action === 'Duplicate' && selected)
|
|
96
|
+
props.onDuplicate(selected.id);
|
|
97
|
+
if (action === 'Delete' && selected)
|
|
98
|
+
props.onDelete(selected.id);
|
|
99
|
+
}
|
|
100
|
+
useInput((input, key) => {
|
|
101
|
+
if (key.escape) {
|
|
102
|
+
if (searching)
|
|
103
|
+
setSearching(false);
|
|
104
|
+
else
|
|
105
|
+
props.onCancel();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (searching) {
|
|
109
|
+
if (key.return || key.downArrow || key.tab) {
|
|
110
|
+
setSearching(false);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
if (key.backspace || key.delete)
|
|
114
|
+
setQuery((value) => [...value].slice(0, -1).join(''));
|
|
115
|
+
else if (input && !key.ctrl && !key.meta) {
|
|
116
|
+
setQuery((value) => value + stripPasteFrames(stripEnterFrames(input)));
|
|
117
|
+
}
|
|
118
|
+
setIndex(0);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (key.upArrow || (key.tab && key.shift))
|
|
122
|
+
setIndex((cursor - 1 + count) % count);
|
|
123
|
+
else if (key.downArrow || key.tab)
|
|
124
|
+
setIndex((cursor + 1) % count);
|
|
125
|
+
else if (key.return)
|
|
126
|
+
activate();
|
|
127
|
+
else if (input === '/' && searchable)
|
|
128
|
+
setSearching(true);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function profilePickerLabel(props, entry) {
|
|
132
|
+
const saved = profileUsers(props.savedProfiles, entry.id);
|
|
133
|
+
const checked = props.profiles[`${props.role}Id`] === entry.id ? '[Selected] ' : '';
|
|
134
|
+
const duplicate = props.profiles.entries.some((other) => other.id !== entry.id
|
|
135
|
+
&& other.name === entry.name);
|
|
136
|
+
const identity = duplicate ? `#${uniqueId(entry, props.profiles.entries)} ` : '';
|
|
137
|
+
const detail = duplicate ? ` ${host(entry)}` : '';
|
|
138
|
+
return `${identity}${checked}${entry.name} ${entry.provider}:${entry.model}${detail}`
|
|
139
|
+
+ (saved ? ` [${saved}]` : '');
|
|
140
|
+
}
|
|
141
|
+
function buildPickerRows(props, state) {
|
|
142
|
+
const { entries, actions, cursor, searching, searchable, query, focused, selected } = state;
|
|
143
|
+
const glyphs = pickGlyphs(props.caps);
|
|
144
|
+
const lines = [];
|
|
145
|
+
if (!props.manager)
|
|
146
|
+
lines.push('Current custom');
|
|
147
|
+
lines.push(...entries.map((entry) => profilePickerLabel(props, entry)));
|
|
148
|
+
lines.push(...actions);
|
|
149
|
+
const rows = lines.map((line, row) => _jsxs(Text, { wrap: "truncate", color: row === cursor ? props.theme.accent : props.theme.muted, children: [`${row === cursor && !searching ? glyphs.caret : ' '} `, clipProfileText(line, props.cols - 8)] }, row));
|
|
150
|
+
if (searchable)
|
|
151
|
+
rows.unshift(_jsxs(Text, { wrap: "truncate", color: props.theme.primary, children: [searching ? `${glyphs.caret} ` : ' ', "Search: ", query || '(/ to search)'] }, "search"));
|
|
152
|
+
const detail = !props.manager && cursor === 0 ? props.customProfile : focused ?? selected;
|
|
153
|
+
if (detail) {
|
|
154
|
+
rows.push(_jsxs(Text, { wrap: "truncate", color: props.theme.muted, children: [detail.provider, ":", detail.model || '(not set)', " ", host(detail), detail === props.customProfile ? '' : ` #${detail.id}`] }, "detail"));
|
|
155
|
+
rows.push(_jsx(Text, { wrap: "truncate", color: props.theme.muted, children: detail.apiKey ? 'Profile key: Configured'
|
|
156
|
+
: props.apiKeys[detail.provider] ? 'Shared / environment: Configured' : 'Missing API key' }, "key"));
|
|
157
|
+
}
|
|
158
|
+
return rows;
|
|
159
|
+
}
|
|
160
|
+
/** Profile selection and explicit management actions share one input owner. */
|
|
161
|
+
export function ModelProfilePicker(props) {
|
|
162
|
+
const state = usePickerState(props);
|
|
163
|
+
usePickerInput(props, state);
|
|
164
|
+
return _jsx(OverlayFrame, { ...props, title: props.error ?? (props.manager ? 'Manage profiles'
|
|
165
|
+
: `${props.role === 'main' ? 'Main' : 'Fast'} profile`), rows: buildPickerRows(props, state), scrollOffset: state.searching ? 0 : state.offset, hint: state.searching ? 'Type search | Enter done | Esc back'
|
|
166
|
+
: 'Enter select/action | Esc back' });
|
|
167
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { PendingSteering } from '../../agent/queued-messages.js';
|
|
3
|
+
import type { Theme } from '../theme.js';
|
|
4
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
5
|
+
export interface QueueTextRow {
|
|
6
|
+
queueId: string;
|
|
7
|
+
offset: number;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
/** Preserve visible whitespace and display terminal controls as harmless escapes. */
|
|
11
|
+
export declare function projectQueueRows(pending: readonly PendingSteering[], columns: number): QueueTextRow[];
|
|
12
|
+
/** Anchor by stable message ID and source offset, including long soft-wrapped lines. */
|
|
13
|
+
export declare function restoreQueueAnchor(before: readonly QueueTextRow[], after: readonly QueueTextRow[], scroll: number): number;
|
|
14
|
+
export declare function QueueOverlay(props: {
|
|
15
|
+
pending: readonly PendingSteering[];
|
|
16
|
+
cols: number;
|
|
17
|
+
maxRows: number;
|
|
18
|
+
scrollOffset: number;
|
|
19
|
+
onScrollClamp: (offset: number) => void;
|
|
20
|
+
theme: Theme;
|
|
21
|
+
caps: TermCapabilities;
|
|
22
|
+
}): React.ReactElement;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import stringWidth from 'string-width';
|
|
5
|
+
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
6
|
+
import { graphemes } from '../editor-navigation.js';
|
|
7
|
+
/** Preserve visible whitespace and display terminal controls as harmless escapes. */
|
|
8
|
+
export function projectQueueRows(pending, columns) {
|
|
9
|
+
const width = Math.max(1, columns);
|
|
10
|
+
const rows = [];
|
|
11
|
+
for (const [index, item] of pending.entries()) {
|
|
12
|
+
rows.push({ queueId: item.queueId, offset: -1, text: `Queue ${index + 1}` });
|
|
13
|
+
let text = '';
|
|
14
|
+
let cells = 0;
|
|
15
|
+
let offset = 0;
|
|
16
|
+
const flush = () => {
|
|
17
|
+
rows.push({ queueId: item.queueId, offset, text });
|
|
18
|
+
text = '';
|
|
19
|
+
cells = 0;
|
|
20
|
+
};
|
|
21
|
+
for (const part of graphemes(item.text)) {
|
|
22
|
+
if (part.text === '\n' || part.text === '\r\n') {
|
|
23
|
+
flush();
|
|
24
|
+
offset = part.end;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const safe = part.text === '\t' ? ' '.repeat(4 - cells % 4)
|
|
28
|
+
: part.text.replace(/[\x00-\x1f\x7f-\x9f]/g, (char) => `\\x${char.charCodeAt(0).toString(16).padStart(2, '0')}`);
|
|
29
|
+
const units = safe === part.text ? [safe] : Array.from(safe);
|
|
30
|
+
for (const unit of units) {
|
|
31
|
+
const size = stringWidth(unit);
|
|
32
|
+
if (cells + size > width && text) {
|
|
33
|
+
flush();
|
|
34
|
+
offset = part.index;
|
|
35
|
+
}
|
|
36
|
+
text += unit;
|
|
37
|
+
cells += size;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
flush();
|
|
41
|
+
}
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
44
|
+
/** Anchor by stable message ID and source offset, including long soft-wrapped lines. */
|
|
45
|
+
export function restoreQueueAnchor(before, after, scroll) {
|
|
46
|
+
const anchor = before[Math.min(scroll, before.length - 1)];
|
|
47
|
+
if (!anchor || !after.length)
|
|
48
|
+
return 0;
|
|
49
|
+
let target = -1;
|
|
50
|
+
for (let i = 0; i < after.length; i++) {
|
|
51
|
+
if (after[i].queueId === anchor.queueId && after[i].offset <= anchor.offset)
|
|
52
|
+
target = i;
|
|
53
|
+
}
|
|
54
|
+
if (target >= 0)
|
|
55
|
+
return target;
|
|
56
|
+
const next = before.slice(scroll).find((row) => after.some((item) => item.queueId === row.queueId));
|
|
57
|
+
if (next)
|
|
58
|
+
return after.findIndex((row) => row.queueId === next.queueId);
|
|
59
|
+
const previous = [...before.slice(0, scroll)].reverse()
|
|
60
|
+
.find((row) => after.some((item) => item.queueId === row.queueId));
|
|
61
|
+
return previous ? after.findIndex((row) => row.queueId === previous.queueId) : 0;
|
|
62
|
+
}
|
|
63
|
+
export function QueueOverlay(props) {
|
|
64
|
+
const { pending, cols, scrollOffset, onScrollClamp, theme } = props;
|
|
65
|
+
const projected = useMemo(() => projectQueueRows(pending, cols - (cols >= 48 ? 4 : 2)), [pending, cols]);
|
|
66
|
+
const previous = useRef({ rows: projected, scroll: scrollOffset });
|
|
67
|
+
const anchored = previous.current.rows === projected ? scrollOffset
|
|
68
|
+
: restoreQueueAnchor(previous.current.rows, projected, previous.current.scroll);
|
|
69
|
+
const bodyRows = Math.max(1, Math.floor(props.maxRows) - (cols >= 48 ? 5 : 3));
|
|
70
|
+
const offset = Math.max(0, Math.min(anchored, projected.length - bodyRows));
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
previous.current = { rows: projected, scroll: offset };
|
|
73
|
+
if (offset !== scrollOffset)
|
|
74
|
+
onScrollClamp(offset);
|
|
75
|
+
}, [projected, offset, scrollOffset, onScrollClamp]);
|
|
76
|
+
const rows = useMemo(() => projected.length ? projected.map((row, index) => _jsx(Text, { wrap: "truncate", color: theme.primary, children: row.text || ' ' }, `${row.queueId}:${index}`))
|
|
77
|
+
: [_jsx(Text, { children: '\u961f\u5217\u5df2\u5904\u7406\u5b8c\u6bd5' }, "empty")], [projected, theme]);
|
|
78
|
+
return _jsx(OverlayFrame, { ...props, title: `Queue: \u5f85\u5904\u7406 ${pending.length}`, hint: "PgUp/PgDn | Up/Down | Esc", rows: rows, scrollOffset: offset });
|
|
79
|
+
}
|
|
@@ -13,6 +13,7 @@ import { type CompactionConfig, type FastConfig } from '../../config/schema.js';
|
|
|
13
13
|
import { type LogLevelName } from '../../logging/levels.js';
|
|
14
14
|
import type { Theme } from '../theme.js';
|
|
15
15
|
import type { TermCapabilities } from '../capabilities.js';
|
|
16
|
+
import type { ModelSettingsDraft, ModelSettingsPatch, ModelSettingsSaveResult } from '../../config/model-profile-store.js';
|
|
16
17
|
export interface SettingsValues {
|
|
17
18
|
provider: string;
|
|
18
19
|
model: string;
|
|
@@ -97,7 +98,11 @@ interface SettingsScreenProps {
|
|
|
97
98
|
onScrollClamp?: (offset: number) => void;
|
|
98
99
|
theme: Theme;
|
|
99
100
|
caps: TermCapabilities;
|
|
100
|
-
onSave
|
|
101
|
+
onSave?: (values: SettingsValues) => void;
|
|
102
|
+
draft?: ModelSettingsDraft;
|
|
103
|
+
onProfileSave?: (draft: ModelSettingsDraft) => ModelSettingsSaveResult;
|
|
104
|
+
onClose?: () => void;
|
|
105
|
+
onReload?: () => ModelSettingsDraft;
|
|
101
106
|
/**
|
|
102
107
|
* `API retries` READ-ONLY row (llm-api-retry-backoff §6.9), e.g. `10` or `off`.
|
|
103
108
|
*
|
|
@@ -178,5 +183,7 @@ export declare function compactionSettingsFrom(compaction: CompactionConfig): Pi
|
|
|
178
183
|
* "I set 64000 but answers still stop early" has no answer anywhere in the UI.
|
|
179
184
|
*/
|
|
180
185
|
export declare function effectiveCapLine(values: Pick<SettingsValues, 'provider' | 'model' | 'maxTokens'>): string;
|
|
181
|
-
export declare function SettingsScreen(
|
|
186
|
+
export declare function SettingsScreen(props: SettingsScreenProps): React.ReactElement;
|
|
187
|
+
/** Only fields changed by the user belong in a settings transaction. */
|
|
188
|
+
export declare function readDirtySettings(initial: SettingsValues, values: SettingsValues): ModelSettingsPatch;
|
|
182
189
|
export {};
|