zames_pro 2.73.11 → 2.75.0
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 +48 -0
- package/README.md +37 -7
- package/dist/attachments.js +6 -1
- package/dist/banner.js +71 -0
- package/dist/browser.js +84 -27
- package/dist/code-blocks.js +48 -0
- package/dist/commands.js +1 -0
- package/dist/config-live.js +53 -0
- package/dist/config.js +38 -0
- package/dist/goal.js +81 -0
- package/dist/i18n/common.js +35 -0
- package/dist/i18n/config.js +20 -0
- package/dist/i18n/deepseek.js +14 -0
- package/dist/i18n/help.js +30 -2
- package/dist/i18n/qwen.js +4 -0
- package/dist/i18n/self-review.js +8 -0
- package/dist/i18n/spinner.js +6 -0
- package/dist/index.js +487 -65
- package/dist/input/layout.js +19 -0
- package/dist/input/suggest.js +29 -1
- package/dist/input.js +14 -3
- package/dist/net-capture.js +18 -0
- package/dist/qwen-browser.js +43 -20
- package/dist/run-task.js +97 -4
- package/dist/theme.js +16 -1
- package/dist/tool-summary.js +83 -0
- package/dist/tui/ConfigMenu.js +337 -0
- package/dist/tui/ConfirmDialog.js +42 -0
- package/dist/tui/FullScreenApp.js +113 -0
- package/dist/tui/InputLine.js +318 -0
- package/dist/tui/LogView.js +48 -0
- package/dist/tui/LoginForm.js +83 -0
- package/dist/tui/SelectList.js +54 -0
- package/dist/tui/StatusBar.js +170 -0
- package/dist/tui/TextPrompt.js +41 -0
- package/dist/tui/TuiApp.js +29 -0
- package/dist/tui/TuiEditor.js +490 -0
- package/dist/tui/edit.js +317 -0
- package/dist/tui/keys.js +195 -0
- package/dist/tui/log-window.js +93 -0
- package/dist/tui/login-pure.js +20 -0
- package/dist/tui/run-config-menu.js +19 -0
- package/dist/tui/run-confirm.js +22 -0
- package/dist/tui/run-login.js +43 -0
- package/dist/tui/run-select.js +18 -0
- package/dist/tui/store.js +104 -0
- package/dist/tui/types.js +10 -0
- package/dist/tui/useMouseWheel.js +164 -0
- package/dist/tui/widgets.js +60 -0
- package/package.json +7 -3
- package/scripts/coverage-gate.mts +10 -0
- package/scripts/inline-status-driver.mts +31 -0
- package/scripts/inline-status.tsx +33 -0
- package/scripts/mouse-churn.mts +34 -0
- package/scripts/qwen-models-smoke.mts +117 -0
- package/scripts/render-demo.mts +1 -1
- package/scripts/tui-comp-shot.mts +63 -0
- package/scripts/tui-components.tsx +62 -0
- package/scripts/tui-config-driver.mts +82 -0
- package/scripts/tui-config-probe.tsx +74 -0
- package/scripts/tui-edit-driver.mts +100 -0
- package/scripts/tui-edit-probe.tsx +38 -0
- package/scripts/tui-editor-probe.tsx +24 -0
- package/scripts/tui-input-driver.mts +54 -0
- package/scripts/tui-input-shot.tsx +59 -0
- package/scripts/tui-log-driver.mts +35 -0
- package/scripts/tui-log-shot.tsx +48 -0
- package/scripts/tui-longpaste-driver.mts +56 -0
- package/scripts/tui-longpaste.tsx +35 -0
- package/scripts/tui-manyselect-shot.mts +34 -0
- package/scripts/tui-manyselect.tsx +17 -0
- package/scripts/tui-mouse-churn-probe.tsx +28 -0
- package/scripts/tui-paste-driver.mts +88 -0
- package/scripts/tui-paste-kinds.tsx +76 -0
- package/scripts/tui-pastetest.mts +38 -0
- package/scripts/tui-probe.tsx +62 -0
- package/scripts/tui-repro.tsx +64 -0
- package/scripts/tui-resize.mts +70 -0
- package/scripts/tui-scroll-driver.mts +53 -0
- package/scripts/tui-scroll-probe.tsx +46 -0
- package/scripts/tui-shot.mts +106 -0
- package/scripts/tui-status-driver.mts +24 -0
- package/scripts/tui-status-shot.tsx +27 -0
- package/scripts/tui-submit.tsx +57 -0
- package/scripts/tui-wheel-driver.mts +61 -0
- package/scripts/tui-wheel-probe.tsx +48 -0
- package/scripts/tui-widgets-driver.mts +20 -0
- package/scripts/tui-widgets.tsx +44 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { LoginForm } from './LoginForm.js';
|
|
4
|
+
import { TextPrompt } from './TextPrompt.js';
|
|
5
|
+
/**
|
|
6
|
+
* Show the Ink login form and resolve with the credentials (or null when the
|
|
7
|
+
* operator cancelled / stdin is not a TTY). Non-TTY resolves null so the
|
|
8
|
+
* caller falls back to its own prompt.
|
|
9
|
+
*/
|
|
10
|
+
export async function runLoginForm(opts) {
|
|
11
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
12
|
+
return null;
|
|
13
|
+
return await new Promise((resolve) => {
|
|
14
|
+
let done = false;
|
|
15
|
+
const app = render(_jsx(LoginForm, { t: opts.t, initialUsername: opts.initialUsername, initialPassword: opts.initialPassword, note: opts.note, onResult: (res) => {
|
|
16
|
+
done = true;
|
|
17
|
+
resolve(res);
|
|
18
|
+
} }), { exitOnCtrlC: false });
|
|
19
|
+
app.waitUntilExit().then(() => {
|
|
20
|
+
if (!done)
|
|
21
|
+
resolve(null);
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Show a one-line Ink text prompt (label + input) and resolve with the value,
|
|
27
|
+
* or null on Esc / non-TTY. Used for the MFA code so it stays in the TUI.
|
|
28
|
+
*/
|
|
29
|
+
export async function runTextPrompt(opts) {
|
|
30
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
31
|
+
return null;
|
|
32
|
+
return await new Promise((resolve) => {
|
|
33
|
+
let done = false;
|
|
34
|
+
const app = render(_jsx(TextPrompt, { t: opts.t, label: opts.label, secret: opts.secret, onResult: (v) => {
|
|
35
|
+
done = true;
|
|
36
|
+
resolve(v);
|
|
37
|
+
} }), { exitOnCtrlC: false });
|
|
38
|
+
app.waitUntilExit().then(() => {
|
|
39
|
+
if (!done)
|
|
40
|
+
resolve(null);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { render } from 'ink';
|
|
3
|
+
import { SelectList } from './SelectList.js';
|
|
4
|
+
export async function selectListInk(opts) {
|
|
5
|
+
if (!(process.stdin.isTTY && process.stdout.isTTY))
|
|
6
|
+
return null;
|
|
7
|
+
return await new Promise((resolve) => {
|
|
8
|
+
let done = false;
|
|
9
|
+
const app = render(_jsx(SelectList, { ...opts, onSelect: (v) => {
|
|
10
|
+
done = true;
|
|
11
|
+
resolve(v);
|
|
12
|
+
} }), { exitOnCtrlC: false });
|
|
13
|
+
app.waitUntilExit().then(() => {
|
|
14
|
+
if (!done)
|
|
15
|
+
resolve(null);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The TUI STATE STORE — the bridge between the imperative agent code and the
|
|
2
|
+
// React/Ink render tree.
|
|
3
|
+
//
|
|
4
|
+
// Why a store instead of React state:
|
|
5
|
+
// * the agent loop calls the UI from OUTSIDE React (`printAbove`, `toolCall`,
|
|
6
|
+
// `thinking`, ...), i.e. imperatively;
|
|
7
|
+
// * Ink re-renders from React state, which can only be set from inside a
|
|
8
|
+
// component or via a setter we own.
|
|
9
|
+
// So the imperative API writes into this store and the Ink tree subscribes
|
|
10
|
+
// via `useSyncExternalStore`. One direction (calls -> store -> render) means no
|
|
11
|
+
// conflict between the two worlds.
|
|
12
|
+
//
|
|
13
|
+
// The store keeps the WHOLE app state in one immutable snapshot object so
|
|
14
|
+
// `useSyncExternalStore` gets a stable reference between changes (a new object
|
|
15
|
+
// is produced only on an actual mutation).
|
|
16
|
+
import { initialEditState } from './edit.js';
|
|
17
|
+
function initialState() {
|
|
18
|
+
return {
|
|
19
|
+
log: [],
|
|
20
|
+
title: 'zames',
|
|
21
|
+
version: '',
|
|
22
|
+
workdir: '',
|
|
23
|
+
scroll: 0,
|
|
24
|
+
busy: false,
|
|
25
|
+
locked: false,
|
|
26
|
+
statusText: '',
|
|
27
|
+
sendState: '',
|
|
28
|
+
animBase: '',
|
|
29
|
+
animStart: 0,
|
|
30
|
+
pendingText: null,
|
|
31
|
+
edit: initialEditState(),
|
|
32
|
+
pastes: [],
|
|
33
|
+
attachments: [],
|
|
34
|
+
history: [],
|
|
35
|
+
prompt: '> ',
|
|
36
|
+
commands: [],
|
|
37
|
+
locale: 'ru',
|
|
38
|
+
contextText: null,
|
|
39
|
+
contextLevel: null,
|
|
40
|
+
queueLength: 0,
|
|
41
|
+
tasks: '',
|
|
42
|
+
provider: '',
|
|
43
|
+
mouseScroll: true,
|
|
44
|
+
thinkingEnabled: false,
|
|
45
|
+
searchEnabled: false,
|
|
46
|
+
statusLine: '',
|
|
47
|
+
codeBlocks: [],
|
|
48
|
+
codeFocus: -1,
|
|
49
|
+
columns: process.stdout.columns || 80,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A minimal external store for the Ink tree.
|
|
54
|
+
*
|
|
55
|
+
* Mutations go through `set(partial)` which shallow-merges into a NEW snapshot
|
|
56
|
+
* object, so `useSyncExternalStore` sees a changed reference and re-renders.
|
|
57
|
+
* A render that changes nothing produces no new object and no repaint.
|
|
58
|
+
*/
|
|
59
|
+
export class TuiStore {
|
|
60
|
+
state = initialState();
|
|
61
|
+
listeners = new Set();
|
|
62
|
+
nextLogId = 1;
|
|
63
|
+
subscribe = (cb) => {
|
|
64
|
+
this.listeners.add(cb);
|
|
65
|
+
return () => this.listeners.delete(cb);
|
|
66
|
+
};
|
|
67
|
+
getSnapshot = () => this.state;
|
|
68
|
+
/** Shallow-merge a partial update and notify subscribers. */
|
|
69
|
+
set(partial) {
|
|
70
|
+
let changed = false;
|
|
71
|
+
for (const k of Object.keys(partial)) {
|
|
72
|
+
if (partial[k] !== this.state[k]) {
|
|
73
|
+
changed = true;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!changed)
|
|
78
|
+
return;
|
|
79
|
+
this.state = { ...this.state, ...partial };
|
|
80
|
+
this.emit();
|
|
81
|
+
}
|
|
82
|
+
/** Functional update over the whole snapshot. */
|
|
83
|
+
update(fn) {
|
|
84
|
+
this.set(fn(this.state));
|
|
85
|
+
}
|
|
86
|
+
/** Append one permanent log entry (an Ink `<Static>` item). */
|
|
87
|
+
pushLog(kind, text, raw = false) {
|
|
88
|
+
const entry = { id: this.nextLogId++, kind, text, raw };
|
|
89
|
+
this.state = { ...this.state, log: [...this.state.log, entry] };
|
|
90
|
+
this.emit();
|
|
91
|
+
}
|
|
92
|
+
/** Append several entries in one notification (cheaper than N pushes). */
|
|
93
|
+
pushLogs(entries) {
|
|
94
|
+
if (!entries.length)
|
|
95
|
+
return;
|
|
96
|
+
const withIds = entries.map((e) => ({ ...e, id: this.nextLogId++ }));
|
|
97
|
+
this.state = { ...this.state, log: [...this.state.log, ...withIds] };
|
|
98
|
+
this.emit();
|
|
99
|
+
}
|
|
100
|
+
emit() {
|
|
101
|
+
for (const cb of this.listeners)
|
|
102
|
+
cb();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The UI EDITOR interface — the shared surface of the legacy `LineEditor`
|
|
2
|
+
// (src/input.ts) and the new Ink-based `TuiEditor` (src/tui/TuiEditor.tsx).
|
|
3
|
+
//
|
|
4
|
+
// index.ts and run-task.ts talk to the UI ONLY through this interface, so the
|
|
5
|
+
// rendering engine (hand-rolled ANSI vs React/Ink) can be swapped without
|
|
6
|
+
// touching the agent code. Both classes implement every member below.
|
|
7
|
+
//
|
|
8
|
+
// Only the members the agent actually uses are listed; the concrete classes may
|
|
9
|
+
// expose more (internal render helpers, etc.).
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// A hook that reads the terminal's MOUSE-WHEEL events and scrolls the log.
|
|
2
|
+
//
|
|
3
|
+
// Ink 8 does NOT parse mouse events (its `useInput` only handles keys), and it
|
|
4
|
+
// STRIPS unknown CSI sequences, so the wheel never reaches a component. To
|
|
5
|
+
// scroll the chat with the wheel we therefore:
|
|
6
|
+
// 1. enable SGR mouse reporting on the terminal (ESC[?1000h ESC[?1006h);
|
|
7
|
+
// 2. listen to the RAW stdin bytes ourselves (Ink's own listener ignores
|
|
8
|
+
// them, so there is no conflict).
|
|
9
|
+
//
|
|
10
|
+
// The SGR encoding is `ESC [ < Cb ; Cx ; Cy M` where Cb 64 = wheel up and
|
|
11
|
+
// 65 = wheel down (plus modifier bits). We only need Cb.
|
|
12
|
+
//
|
|
13
|
+
// ALTERNATE-SCROLL: while the app is in the ALTERNATE SCREEN (full-screen
|
|
14
|
+
// mode), terminals turn the mouse wheel into ARROW KEYS by default (DECSET
|
|
15
|
+
// 1007, "alternate scroll mode"). Ink parses those arrows as normal Up/Down,
|
|
16
|
+
// and the input editor at a buffer edge maps them to HISTORY -- so the wheel
|
|
17
|
+
// scrolled the INPUT HISTORY instead of the log (the operator's bug).
|
|
18
|
+
//
|
|
19
|
+
// Two clear, MUTUALLY EXCLUSIVE modes (a terminal cannot do both at once):
|
|
20
|
+
// - SELECT (ui.mouseScroll=false): mouse reporting stays OFF, so the
|
|
21
|
+
// terminal owns the mouse -- drag-select works natively and the wheel is
|
|
22
|
+
// inert (no arrows, no history jump).
|
|
23
|
+
// - SCROLL (ui.mouseScroll=true): mouse reporting ON, so the wheel arrives
|
|
24
|
+
// as SGR events and scrolls the log; drag-select then needs SHIFT.
|
|
25
|
+
//
|
|
26
|
+
// THE CRITICAL DETAIL (this bug kept coming back): the mode sequences must be
|
|
27
|
+
// written ONCE per mode change, NEVER per render. An earlier version listed
|
|
28
|
+
// `onWheel` in the effect deps, but callers pass an inline arrow, so the
|
|
29
|
+
// effect re-ran on EVERY render (the status spinner re-renders constantly) --
|
|
30
|
+
// each cleanup wrote `1007h` (alternate-scroll ON) before the next setup wrote
|
|
31
|
+
// `1007l`. A wheel notch that landed in that window became an ARROW and
|
|
32
|
+
// scrolled the input history: the intermittent "either scroll or selection"
|
|
33
|
+
// behaviour. The effect therefore depends ONLY on the mode + stdin, and the
|
|
34
|
+
// callback is read from a ref (set in its own effect) so a new closure never tears
|
|
35
|
+
// the terminal mode down.
|
|
36
|
+
//
|
|
37
|
+
// ORDER MATTERS: some terminals only apply `1007l` while mouse reporting is
|
|
38
|
+
// enabled, so we enable 1000/1006 FIRST, then disable 1007. In SELECT mode we
|
|
39
|
+
// actually want reporting off, so we send the on/off pair ONCE (the 1007l then
|
|
40
|
+
// "sticks") and then leave reporting off.
|
|
41
|
+
import { useEffect, useRef } from 'react';
|
|
42
|
+
import { useStdin } from 'ink';
|
|
43
|
+
/** True when a chunk contains an SGR mouse wheel event; returns the delta. */
|
|
44
|
+
export function parseWheel(chunk) {
|
|
45
|
+
// Count wheel-ups (down) and wheel-downs (up) in the chunk.
|
|
46
|
+
let delta = 0;
|
|
47
|
+
const re = /\x1b\[<(\d+);\d+;\d+[Mm]/g;
|
|
48
|
+
let m;
|
|
49
|
+
while ((m = re.exec(chunk)) !== null) {
|
|
50
|
+
const cb = Number(m[1]);
|
|
51
|
+
// Button code 64 = wheel up (scroll back), 65 = wheel down (scroll fwd).
|
|
52
|
+
const button = cb & 0b11000011;
|
|
53
|
+
if (button === 64)
|
|
54
|
+
delta += 1;
|
|
55
|
+
else if (button === 65)
|
|
56
|
+
delta -= 1;
|
|
57
|
+
}
|
|
58
|
+
return delta;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The part of an SGR mouse sequence at the END of `chunk` that is still
|
|
62
|
+
* INCOMPLETE (no `M`/`m` terminator yet), or '' when the chunk ends cleanly.
|
|
63
|
+
* A terminal may split one sequence across two `data` events; scanning each
|
|
64
|
+
* chunk alone dropped the wheel notch (the hook keeps this tail and prepends
|
|
65
|
+
* it to the next chunk).
|
|
66
|
+
*/
|
|
67
|
+
export function incompleteTail(chunk) {
|
|
68
|
+
const start = chunk.lastIndexOf('\u001b[<');
|
|
69
|
+
if (start === -1)
|
|
70
|
+
return '';
|
|
71
|
+
// Complete if an `M`/`m` follows the introducer before the chunk ends.
|
|
72
|
+
if (/[Mm]/.test(chunk.slice(start + 3)))
|
|
73
|
+
return '';
|
|
74
|
+
return chunk.slice(start);
|
|
75
|
+
}
|
|
76
|
+
const ESC = String.fromCharCode(27);
|
|
77
|
+
/**
|
|
78
|
+
* The terminal-mode sequences the hook writes for a given mode. Exported PURE
|
|
79
|
+
* so a test can pin the exact bytes AND their order:
|
|
80
|
+
* - SELECT (enabled=false): NO `1000h/1006h` in the STEADY state, `on`
|
|
81
|
+
* only applies `1007l` (the one-time prime), and `off` guarantees the
|
|
82
|
+
* reporting bits are clear on unmount;
|
|
83
|
+
* - SCROLL (enabled=true): `1000h 1006h 1007l` in the STEADY state.
|
|
84
|
+
*/
|
|
85
|
+
export function mouseModeSequences(enabled) {
|
|
86
|
+
// 1000 = button-event tracking, 1006 = SGR extended coords, 1007 off always
|
|
87
|
+
// (being in alt-screen otherwise turns the wheel into arrows => history).
|
|
88
|
+
const reportingOn = ESC + '[?1000h' + ESC + '[?1006h';
|
|
89
|
+
const reportingOff = ESC + '[?1006l' + ESC + '[?1000l';
|
|
90
|
+
const alternateScrollOff = ESC + '[?1007l';
|
|
91
|
+
const alternateScrollOn = ESC + '[?1007h';
|
|
92
|
+
if (enabled) {
|
|
93
|
+
return {
|
|
94
|
+
on: reportingOn + alternateScrollOff,
|
|
95
|
+
off: reportingOff + alternateScrollOn,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
// SELECT: the on/off pair is applied ONCE at mount so the 1007l sticks even
|
|
99
|
+
// though reporting is then off (some terminals ignore 1007l while reporting
|
|
100
|
+
// is off). After the prime the steady state has reporting OFF, and the
|
|
101
|
+
// listener is not bound, so nothing consumes the native selection.
|
|
102
|
+
return {
|
|
103
|
+
on: reportingOn + alternateScrollOff + reportingOff,
|
|
104
|
+
off: reportingOff + alternateScrollOn,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Subscribe to wheel events on the given terminal and call `onWheel(delta)`
|
|
109
|
+
* where delta > 0 means "scroll up / back".
|
|
110
|
+
*
|
|
111
|
+
* Two mutually exclusive modes:
|
|
112
|
+
* - `enabled=false` (SELECT): mouse reporting stays OFF, native drag-select
|
|
113
|
+
* works, the wheel is inert. The listener is still bound so an accidental
|
|
114
|
+
* SGR byte from a previous mode never leaks into the editor as an arrow.
|
|
115
|
+
* - `enabled=true` (SCROLL): reporting is on, the wheel scrolls the log
|
|
116
|
+
* (via `onWheel`), drag-select needs Shift.
|
|
117
|
+
*
|
|
118
|
+
* The terminal mode is written ONCE per `enabled` change. The wheel listener
|
|
119
|
+
* is bound once and reads the latest `onWheel` from a ref, so an inline
|
|
120
|
+
* callback (a new identity every render) does NOT re-run the effect -- a
|
|
121
|
+
* re-run would momentarily re-enable alternate-scroll and let a wheel notch
|
|
122
|
+
* scroll the input history.
|
|
123
|
+
*/
|
|
124
|
+
export function useMouseWheel(onWheel, enabled = false) {
|
|
125
|
+
const { stdin, isRawModeSupported } = useStdin();
|
|
126
|
+
// The latest callback, without making the mode effect depend on its
|
|
127
|
+
// identity (callers pass an inline arrow).
|
|
128
|
+
const onWheelRef = useRef(onWheel);
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
onWheelRef.current = onWheel;
|
|
131
|
+
}, [onWheel]);
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (!isRawModeSupported)
|
|
134
|
+
return;
|
|
135
|
+
const seq = mouseModeSequences(enabled);
|
|
136
|
+
process.stdout.write(seq.on);
|
|
137
|
+
// A sequence may be split across two chunks; carry the incomplete tail.
|
|
138
|
+
let carry = '';
|
|
139
|
+
const onData = (data) => {
|
|
140
|
+
const s = typeof data === 'string' ? data : data.toString('utf-8');
|
|
141
|
+
const buf = carry + s;
|
|
142
|
+
carry = incompleteTail(buf);
|
|
143
|
+
const scan = carry ? buf.slice(0, buf.length - carry.length) : buf;
|
|
144
|
+
if (!scan.includes('\u001b[<'))
|
|
145
|
+
return;
|
|
146
|
+
// SELECT mode ignores the wheel entirely (the event never reaches the
|
|
147
|
+
// editor either -- there is no reporting to generate it in the first
|
|
148
|
+
// place, but a stale SGR byte from a previous mode is swallowed here).
|
|
149
|
+
if (!enabled)
|
|
150
|
+
return;
|
|
151
|
+
const delta = parseWheel(scan);
|
|
152
|
+
if (delta !== 0)
|
|
153
|
+
onWheelRef.current(delta);
|
|
154
|
+
};
|
|
155
|
+
stdin.on('data', onData);
|
|
156
|
+
return () => {
|
|
157
|
+
stdin.removeListener('data', onData);
|
|
158
|
+
// Disable mouse reporting on unmount so normal selection works again.
|
|
159
|
+
process.stdout.write(seq.off);
|
|
160
|
+
};
|
|
161
|
+
// `onWheel` is deliberately NOT a dependency: it is read via the ref so an
|
|
162
|
+
// inline callback does not tear the terminal mode down on every render.
|
|
163
|
+
}, [stdin, isRawModeSupported, enabled]);
|
|
164
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
// ---- palette (kept in one place so every menu is consistent) ----
|
|
4
|
+
export const UI = {
|
|
5
|
+
accent: '#c8b06a',
|
|
6
|
+
accentDim: '#7a6b45',
|
|
7
|
+
text: '#cfc9b0',
|
|
8
|
+
dim: '#5b616e',
|
|
9
|
+
border: '#4a5058',
|
|
10
|
+
on: '#6fd0b0',
|
|
11
|
+
off: '#5b616e',
|
|
12
|
+
ok: '#a9c08c',
|
|
13
|
+
warn: '#c9a86a',
|
|
14
|
+
err: '#c98a80',
|
|
15
|
+
system: '#808896',
|
|
16
|
+
title: '#9fb8d8',
|
|
17
|
+
};
|
|
18
|
+
/** `[x]` / `[ ]` — a checkbox for a boolean value. */
|
|
19
|
+
export function Checkbox({ checked, focused, }) {
|
|
20
|
+
return (_jsx(Text, { color: checked ? UI.on : UI.off, bold: focused, children: checked ? '[x]' : '[ ]' }));
|
|
21
|
+
}
|
|
22
|
+
/** `‹ value ›` — an inline SELECT for an enum value. The arrows hint that
|
|
23
|
+
* ←/→ (or Space) cycle the option. */
|
|
24
|
+
export function Select({ value, focused, }) {
|
|
25
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: UI.off, children: '‹ ' }), _jsx(Text, { color: focused ? UI.accent : UI.text, bold: focused, children: value }), _jsx(Text, { color: UI.off, children: ' ›' })] }));
|
|
26
|
+
}
|
|
27
|
+
/** A framed panel (rounded border) with an optional title bar and footer. */
|
|
28
|
+
export function Panel({ title, subtitle, footer, width, children, }) {
|
|
29
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: UI.border, paddingX: 1, width: width, children: [title ? (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.title, bold: true, children: title }), subtitle ? _jsx(Text, { color: UI.dim, children: subtitle }) : null] })) : null, title ? (_jsx(Text, { color: "#3a3f4b", children: '─'.repeat(panelInnerCols(width)) })) : null, children, footer ? footer : null] }));
|
|
30
|
+
}
|
|
31
|
+
/** Inner width (minus border + paddingX) the panel divider should span. */
|
|
32
|
+
function panelInnerCols(width) {
|
|
33
|
+
const cols = width || process.stdout.columns || 80;
|
|
34
|
+
return Math.max(1, cols - 6);
|
|
35
|
+
}
|
|
36
|
+
/** A single hint chip: `↑/↓ select`. */
|
|
37
|
+
export function Hint({ keys, label, }) {
|
|
38
|
+
return (_jsxs(Text, { children: [_jsx(Text, { color: UI.accent, children: keys }), _jsx(Text, { color: UI.dim, children: ' ' + label })] }));
|
|
39
|
+
}
|
|
40
|
+
/** A row of hint chips separated by ` · `. */
|
|
41
|
+
export function HintBar({ hints, }) {
|
|
42
|
+
return (_jsx(Text, { children: hints.map(([k, l], i) => (_jsxs(Text, { children: [i > 0 ? _jsx(Text, { color: UI.dim, children: ' · ' }) : null, _jsx(Text, { color: UI.accent, children: k }), _jsx(Text, { color: UI.dim, children: ' ' + l })] }, i))) }));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* A CODE BLOCK with a syntax-highlighted body and a copy affordance.
|
|
46
|
+
*
|
|
47
|
+
* The agent's answers are printed as pre-rendered ANSI (markdown.ts already
|
|
48
|
+
* highlights with cli-highlight), so the live log does NOT use this component —
|
|
49
|
+
* it exists for menus/panels that show a snippet inline (and for a future
|
|
50
|
+
* structured log). Copying is a KEY, not a button: a terminal has no pointer.
|
|
51
|
+
* Pass `onCopy` and show `hint` so the operator learns the binding.
|
|
52
|
+
*/
|
|
53
|
+
export function CodeBlock({ code, lang, highlighted, width, hint, focused, onCopy, }) {
|
|
54
|
+
// React cannot call `onCopy` from a keydown here (Ink has no per-element
|
|
55
|
+
// focus ring by default); the caller wires the key and passes `focused` to
|
|
56
|
+
// show which block it will copy. `onCopy` is kept so a future focus manager
|
|
57
|
+
// can invoke it without changing the call sites.
|
|
58
|
+
void onCopy;
|
|
59
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: focused ? UI.accent : UI.border, paddingX: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: UI.dim, children: lang || 'code' }), hint ? _jsx(Text, { color: UI.accent, children: hint }) : null] }), (highlighted || code).split(String.fromCharCode(10)).map((line, i) => (_jsx(Text, { children: line || ' ' }, i)))] }));
|
|
60
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "zames_pro",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.75.0",
|
|
4
4
|
"description": "Terminal coding agent that drives a real browser to chat with DeepSeek, Qwen and other web LLMs — no API key required. Reads and edits files, runs commands, commits to git, like Claude Code / Codex CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"test:watch": "tsx --test --watch test/*.test.ts",
|
|
35
35
|
"self-smoke": "tsx scripts/self-smoke.mts",
|
|
36
36
|
"qwen-smoke": "tsx scripts/qwen-smoke.mts",
|
|
37
|
+
"qwen-models-smoke": "tsx scripts/qwen-models-smoke.mts",
|
|
37
38
|
"render-demo": "tsx scripts/render-demo.mts",
|
|
38
39
|
"render-how": "tsx scripts/render-how-it-works.mts",
|
|
39
40
|
"build:readme": "tsx scripts/build-readme.mts",
|
|
@@ -66,16 +67,18 @@
|
|
|
66
67
|
],
|
|
67
68
|
"license": "MIT",
|
|
68
69
|
"engines": {
|
|
69
|
-
"node": ">=
|
|
70
|
+
"node": ">=22"
|
|
70
71
|
},
|
|
71
72
|
"packageManager": "npm@11.19.0",
|
|
72
73
|
"dependencies": {
|
|
73
74
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
74
75
|
"chalk": "^5.3.0",
|
|
75
76
|
"cli-highlight": "^2.1.11",
|
|
77
|
+
"ink": "^8.0.0",
|
|
76
78
|
"markdansi": "^0.3.4",
|
|
77
79
|
"ora": "^8.0.1",
|
|
78
|
-
"playwright": "^1.48.0"
|
|
80
|
+
"playwright": "^1.48.0",
|
|
81
|
+
"react": "^19.3.0"
|
|
79
82
|
},
|
|
80
83
|
"publishConfig": {
|
|
81
84
|
"access": "public"
|
|
@@ -91,6 +94,7 @@
|
|
|
91
94
|
"devDependencies": {
|
|
92
95
|
"@eslint/js": "^9.39.5",
|
|
93
96
|
"@types/node": "^26.6.2",
|
|
97
|
+
"@types/react": "^19.3.0",
|
|
94
98
|
"eslint": "^9.39.5",
|
|
95
99
|
"eslint-config-prettier": "^10.1.8",
|
|
96
100
|
"globals": "^17.12.0",
|
|
@@ -39,6 +39,16 @@ const FLOORS = {
|
|
|
39
39
|
// C3: the pure /config subcommand dispatcher — silent routing regressions
|
|
40
40
|
// (a wrong subcommand branch) are invisible without tests.
|
|
41
41
|
'config-command.ts': 85,
|
|
42
|
+
// The Ink TUI's PURE halves: the editor state transitions (tui/edit.ts) and
|
|
43
|
+
// the key dispatcher (tui/keys.ts). A silent regression there breaks the
|
|
44
|
+
// operator's editing without any UI test catching it. NOTE: the gate keys on
|
|
45
|
+
// the BASENAME (its regex has no slash), so these are 'edit.ts' / 'keys.ts' /
|
|
46
|
+
// 'store.ts' — unique in the tree today.
|
|
47
|
+
'edit.ts': 85,
|
|
48
|
+
'keys.ts': 85,
|
|
49
|
+
'store.ts': 85,
|
|
50
|
+
'banner.ts': 85,
|
|
51
|
+
'tool-summary.ts': 85,
|
|
42
52
|
// N12: web.ts holds the SSRF guard and the DuckDuckGo/HTML parsers — a
|
|
43
53
|
// regression there is silent. Floors sit below the current values.
|
|
44
54
|
'web.ts': 40,
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { Term } from '../test/vt100.mts'
|
|
4
|
+
const NL = String.fromCharCode(10)
|
|
5
|
+
const term = new Term(12, 60)
|
|
6
|
+
const child = spawn(
|
|
7
|
+
'script',
|
|
8
|
+
['-qec', 'npx tsx scripts/inline-status.tsx', '/dev/null'],
|
|
9
|
+
{
|
|
10
|
+
cwd: path.resolve('.'),
|
|
11
|
+
env: { ...process.env, COLUMNS: '60', LINES: '12' },
|
|
12
|
+
},
|
|
13
|
+
)
|
|
14
|
+
child.stdout.on('data', (d) => term.write(d.toString('utf-8')))
|
|
15
|
+
child.stderr.on('data', (d) => term.write(d.toString('utf-8')))
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
const g = term.grid
|
|
18
|
+
if (g[term.r]) g[term.r][term.c] = String.fromCharCode(9673)
|
|
19
|
+
process.stdout.write(
|
|
20
|
+
g
|
|
21
|
+
.map((r) => r.join('').replace(/\s+$/, ''))
|
|
22
|
+
.map((l, n) => String(n).padStart(2) + '| ' + l)
|
|
23
|
+
.join(NL) +
|
|
24
|
+
NL +
|
|
25
|
+
'CURSOR row=' +
|
|
26
|
+
term.r +
|
|
27
|
+
NL,
|
|
28
|
+
)
|
|
29
|
+
child.kill('SIGTERM')
|
|
30
|
+
}, 5000)
|
|
31
|
+
child.on('close', () => process.exit(0))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { render } from 'ink'
|
|
3
|
+
import { TuiStore } from '../src/tui/store.js'
|
|
4
|
+
import { TuiApp } from '../src/tui/TuiApp.js'
|
|
5
|
+
import { initialEditState } from '../src/tui/edit.js'
|
|
6
|
+
const store = new TuiStore()
|
|
7
|
+
store.set({
|
|
8
|
+
title: 'z',
|
|
9
|
+
version: '1',
|
|
10
|
+
workdir: '/tmp',
|
|
11
|
+
prompt: '> ',
|
|
12
|
+
locale: 'en',
|
|
13
|
+
commands: [],
|
|
14
|
+
edit: { ...initialEditState(), buf: 'abc' },
|
|
15
|
+
statusText: 'STATUS HERE',
|
|
16
|
+
})
|
|
17
|
+
const p = {
|
|
18
|
+
onSubmit: () => {},
|
|
19
|
+
onEscape: () => {},
|
|
20
|
+
onCtrlC: () => {},
|
|
21
|
+
onCopyCode: () => {},
|
|
22
|
+
onAttach: () => null,
|
|
23
|
+
onClipboard: () => Promise.resolve(null),
|
|
24
|
+
onHistoryChange: () => {},
|
|
25
|
+
onClearScreen: () => {},
|
|
26
|
+
attachments: [],
|
|
27
|
+
onPastesChange: () => {},
|
|
28
|
+
}
|
|
29
|
+
render(React.createElement(TuiApp, { store, inputProps: p }), {
|
|
30
|
+
exitOnCtrlC: false,
|
|
31
|
+
patchConsole: false,
|
|
32
|
+
})
|
|
33
|
+
setInterval(() => {}, 1 << 30)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Prove the mouse-mode churn is ZERO: mount the REAL TuiEditor, force several
|
|
2
|
+
// store updates (like the 1s badge refresh does), and COUNT how many times the
|
|
3
|
+
// raw stdout receives the mouse-mode sequences. With the effect depending on
|
|
4
|
+
// an inline onWheel, the count grew per render (the wheel-vs-history bug);
|
|
5
|
+
// with the ref fix, each mode byte is written ONCE.
|
|
6
|
+
//
|
|
7
|
+
// Exit code 0 = OK, 1 = churn detected. Prints a one-line verdict so the pty
|
|
8
|
+
// test can assert on it.
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'node:child_process'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
|
|
13
|
+
const child = spawn(
|
|
14
|
+
'script',
|
|
15
|
+
['-qec', 'npx tsx scripts/tui-mouse-churn-probe.tsx', '/dev/null'],
|
|
16
|
+
{
|
|
17
|
+
cwd: path.resolve('.'),
|
|
18
|
+
env: { ...process.env, COLUMNS: '80', LIMES: '24' },
|
|
19
|
+
},
|
|
20
|
+
)
|
|
21
|
+
let out = ''
|
|
22
|
+
child.stdout.on('data', (d) => (out += d.toString('utf-8')))
|
|
23
|
+
child.stderr.on('data', (d) => (out += d.toString('utf-8')))
|
|
24
|
+
setTimeout(() => child.kill('SIGTERM'), 8000)
|
|
25
|
+
child.on('close', () => {
|
|
26
|
+
const count = (seq: string): number => out.split(seq).length - 1
|
|
27
|
+
const on = count('\u001b[?1000h')
|
|
28
|
+
const altOff = count('\u001b[?1007l')
|
|
29
|
+
const altOn = count('\u001b[?1007h')
|
|
30
|
+
console.log('MOUSE_CHURN 1000h=' + on + ' 1007l=' + altOff + ' 1007h=' + altOn)
|
|
31
|
+
const ok = on === 1 && altOff === 1 && altOn === 0
|
|
32
|
+
console.log(ok ? 'MOUSE_CHURN_OK' : 'MOUSE_CHURN_FAIL')
|
|
33
|
+
process.exit(ok ? 0 : 1)
|
|
34
|
+
})
|