@giovannijecha/jecode 0.7.0 → 0.7.1
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 +242 -267
- package/dist/config.js +46 -21
- package/dist/credential-safety.js +9 -5
- package/dist/providers/anthropic-wire.js +5 -4
- package/dist/providers/ollama-wire.js +3 -2
- package/dist/providers/openai-wire.js +6 -5
- package/dist/providers/wire-usage.js +6 -0
- package/dist/tui/app.js +54 -32
- package/dist/tui/picker.js +2 -2
- package/dist/ui/terminal-text.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
<p align="center"><strong>Your code. Your loop.</strong></p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
A focused coding agent
|
|
13
|
-
|
|
12
|
+
A focused terminal coding agent with visible tool use, durable sessions, and
|
|
13
|
+
one controller that stays under your control.
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
@@ -21,367 +21,342 @@
|
|
|
21
21
|
</p>
|
|
22
22
|
|
|
23
23
|
<p align="center">
|
|
24
|
-
<a href="https://github.com/giovannijecha/jecode/blob/main/CHANGELOG.md">Changelog</a>
|
|
24
|
+
<a href="https://github.com/giovannijecha/jecode/blob/main/CHANGELOG.md">Changelog</a> ·
|
|
25
25
|
<a href="https://github.com/giovannijecha/jecode/releases">Releases</a>
|
|
26
26
|
</p>
|
|
27
27
|
|
|
28
|
-
> Jecode is
|
|
28
|
+
> Jecode is currently pre-1.0. The core loop is usable today, but commands and
|
|
29
29
|
> terminal interactions may still evolve before 1.0.
|
|
30
30
|
|
|
31
31
|
## Why Jecode
|
|
32
32
|
|
|
33
|
-
- **One
|
|
34
|
-
control to you. Independent reads
|
|
35
|
-
commands
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
a local
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
has zero third-party
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
33
|
+
- **One visible loop.** One controller talks to the model, runs tools, and
|
|
34
|
+
returns control to you. Independent reads may overlap inside a step; writes
|
|
35
|
+
and commands stay ordered. There are no delegated agents or hidden model
|
|
36
|
+
workers.
|
|
37
|
+
- **Terminal-native today.** The transcript, composer, searchable menus, tool
|
|
38
|
+
output, diffs, approvals, reasoning, and status share one full-screen TUI.
|
|
39
|
+
- **Permission-aware.** Tool use is visible. Dangerous actions ask first, and
|
|
40
|
+
remembered session approvals can be reviewed or revoked.
|
|
41
|
+
- **Durable and context-bounded.** Interactive conversations survive terminal
|
|
42
|
+
exits. Older model context can be compacted while the complete conversation
|
|
43
|
+
and transcript remain available in the saved session.
|
|
44
|
+
- **Multi-provider.** Use Anthropic or OpenAI API keys, an eligible ChatGPT
|
|
45
|
+
account, or a local, cloud, or remote Ollama server without changing the
|
|
46
|
+
workflow.
|
|
47
|
+
- **Lean by construction.** Jecode ships as owned JavaScript built on Node.js
|
|
48
|
+
primitives, executes no installation scripts, and has zero third-party
|
|
49
|
+
runtime dependencies. This is a permanent product constraint, not a temporary
|
|
50
|
+
optimization.
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
Jecode requires **Node.js 22.18+ on the 22.x line, or Node.js 24+**, and npm.
|
|
55
|
+
|
|
56
|
+
```console
|
|
55
57
|
npm install --global @giovannijecha/jecode
|
|
56
58
|
jecode --version
|
|
57
|
-
|
|
59
|
+
cd path/to/your/project
|
|
60
|
+
jecode
|
|
61
|
+
```
|
|
58
62
|
|
|
59
|
-
|
|
60
|
-
`
|
|
63
|
+
Jecode opens directly on an empty composer. Use `/providers` to connect a
|
|
64
|
+
service, `/models` to choose a model, then describe the work you want done.
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
66
|
+
```text
|
|
67
|
+
Review this project, explain its architecture, and propose the smallest safe
|
|
68
|
+
change to improve startup performance.
|
|
69
|
+
```
|
|
65
70
|
|
|
66
|
-
|
|
71
|
+
Use `jecode --root path/to/project` to select another workspace, or
|
|
72
|
+
`jecode --ephemeral` when the conversation must stay memory-only.
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
cd path/to/your/project
|
|
70
|
-
jecode
|
|
71
|
-
~~~
|
|
74
|
+
Resume a saved conversation for the current workspace with a searchable picker:
|
|
72
75
|
|
|
73
|
-
|
|
74
|
-
or open the most recent one directly:
|
|
75
|
-
|
|
76
|
-
~~~console
|
|
76
|
+
```console
|
|
77
77
|
jecode resume
|
|
78
78
|
jecode resume --latest
|
|
79
|
-
|
|
79
|
+
```
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
Run `jecode --help` for every startup option. Windows, Ubuntu, and macOS are
|
|
82
|
+
covered by the project test matrix.
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
### Prereleases
|
|
84
85
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
~~~
|
|
86
|
+
The stable npm package is the supported installation artifact. To try an
|
|
87
|
+
opt-in prerelease:
|
|
88
88
|
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
```console
|
|
90
|
+
npm install --global @giovannijecha/jecode@next
|
|
91
|
+
```
|
|
91
92
|
|
|
92
|
-
|
|
93
|
+
Git URL installs are intentionally unsupported: the source tree contains no
|
|
94
|
+
generated runtime and defines no install-time build hook.
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
### Update and uninstall
|
|
95
97
|
|
|
96
|
-
|
|
98
|
+
```console
|
|
97
99
|
npm install --global @giovannijecha/jecode
|
|
98
100
|
jecode --version
|
|
99
|
-
|
|
101
|
+
npm uninstall --global @giovannijecha/jecode
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Uninstalling the command preserves `~/.jecode`. Remove that directory only when
|
|
105
|
+
you intentionally want to erase saved settings, credentials, accounts, and
|
|
106
|
+
sessions.
|
|
100
107
|
|
|
101
|
-
If
|
|
102
|
-
|
|
108
|
+
If an older GitHub installation still owns the `jecode` executable, remove the
|
|
109
|
+
legacy unscoped package before installing the scoped package:
|
|
103
110
|
|
|
104
|
-
|
|
111
|
+
```console
|
|
105
112
|
npm uninstall --global jecode
|
|
106
113
|
npm install --global @giovannijecha/jecode
|
|
107
|
-
|
|
114
|
+
```
|
|
108
115
|
|
|
109
|
-
|
|
110
|
-
executable still owns the command. Remove it instead of installing with
|
|
111
|
-
`--force`.
|
|
112
|
-
|
|
113
|
-
### Uninstall
|
|
114
|
-
|
|
115
|
-
~~~console
|
|
116
|
-
npm uninstall --global @giovannijecha/jecode
|
|
117
|
-
~~~
|
|
118
|
-
|
|
119
|
-
Uninstalling the command preserves **~/.jecode** so saved settings and
|
|
120
|
-
credentials remain available after a reinstall. Remove that directory only
|
|
121
|
-
when you intentionally want to erase Jecode's local data.
|
|
116
|
+
Do not work around the resulting `EEXIST` error with `--force`.
|
|
122
117
|
|
|
123
118
|
### Linux and WSL
|
|
124
119
|
|
|
125
|
-
WSL
|
|
120
|
+
WSL uses its own Node.js installation and `PATH`; the Node.js version installed
|
|
126
121
|
on Windows does not apply inside it. Keep user-installed npm commands in the
|
|
127
|
-
Linux user path
|
|
122
|
+
Linux user path:
|
|
128
123
|
|
|
129
|
-
|
|
124
|
+
```console
|
|
130
125
|
npm config set prefix "$HOME/.local"
|
|
131
126
|
export PATH="$HOME/.local/bin:$PATH"
|
|
132
127
|
npm install --global @giovannijecha/jecode
|
|
133
128
|
hash -r
|
|
134
129
|
command -v jecode
|
|
135
130
|
jecode --version
|
|
136
|
-
|
|
131
|
+
```
|
|
137
132
|
|
|
138
133
|
Persist the `PATH` export in `~/.profile` or your shell's startup file. Inside
|
|
139
|
-
WSL,
|
|
140
|
-
`/mnt/c
|
|
141
|
-
must report 22.18+ on the 22.x line, or 24+.
|
|
134
|
+
WSL, `command -v jecode` should resolve below `/home/...`, not through an
|
|
135
|
+
inherited Windows path below `/mnt/c`. Do not ignore `EBADENGINE`: `node
|
|
136
|
+
--version` must report 22.18+ on the 22.x line, or 24+.
|
|
142
137
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
~~~console
|
|
146
|
-
git clone https://github.com/giovannijecha/jecode.git
|
|
147
|
-
cd jecode
|
|
148
|
-
npm ci --ignore-scripts
|
|
149
|
-
npm run build:release
|
|
150
|
-
npm link
|
|
151
|
-
jecode
|
|
152
|
-
~~~
|
|
153
|
-
|
|
154
|
-
Development runs TypeScript directly with **npm run start**. **dist/** is an
|
|
155
|
-
ignored, generated tree used only by linked commands and release tarballs.
|
|
156
|
-
**npm run pack:release** rebuilds it from a clean target before packing; the
|
|
157
|
-
trusted publish workflow performs the same explicit build. Installing the
|
|
158
|
-
published package runs no compilation or installation scripts.
|
|
159
|
-
|
|
160
|
-
## First session
|
|
161
|
-
|
|
162
|
-
Jecode opens on an empty composer instead of forcing a setup wizard. Type
|
|
163
|
-
**/providers** to connect the services you use, then **/models** to choose from
|
|
164
|
-
their combined live catalogues. **/settings** keeps the selected model and the
|
|
165
|
-
remaining non-secret defaults together. An API key can remain in memory for
|
|
166
|
-
the current session or be saved explicitly under **~/.jecode**; it is never
|
|
167
|
-
stored in the workspace.
|
|
138
|
+
## Providers
|
|
168
139
|
|
|
169
140
|
| Provider ID | Authentication | Notes |
|
|
170
|
-
|
|
171
|
-
| anthropic | ANTHROPIC_API_KEY | Anthropic API |
|
|
172
|
-
| openai | OPENAI_API_KEY | OpenAI API |
|
|
173
|
-
| openai-codex | ChatGPT OAuth | Experimental; uses eligible ChatGPT Codex access |
|
|
174
|
-
| ollama | OLLAMA_API_KEY for
|
|
175
|
-
|
|
176
|
-
Choose **ChatGPT** in
|
|
177
|
-
pasting a key.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
account and plan, not by OpenAI API credits. This integration is experimental
|
|
182
|
-
and is not an endorsement of Jecode by OpenAI.
|
|
141
|
+
| --- | --- | --- |
|
|
142
|
+
| `anthropic` | `ANTHROPIC_API_KEY` | Anthropic API |
|
|
143
|
+
| `openai` | `OPENAI_API_KEY` | OpenAI API |
|
|
144
|
+
| `openai-codex` | ChatGPT OAuth | Experimental; uses eligible ChatGPT Codex access |
|
|
145
|
+
| `ollama` | `OLLAMA_API_KEY` for cloud or remote use | Cloud with a key, local without one |
|
|
146
|
+
|
|
147
|
+
Choose **ChatGPT** in `/providers` to sign in on OpenAI's website without
|
|
148
|
+
pasting a key. Jecode supports a local browser callback and a device-code flow;
|
|
149
|
+
WSL and remote terminals default to the device code. Availability and usage
|
|
150
|
+
limits depend on the ChatGPT account and plan, not on OpenAI API credits. This
|
|
151
|
+
integration is experimental and is not an endorsement of Jecode by OpenAI.
|
|
183
152
|
|
|
184
153
|
Anthropic remains API-key only. Jecode does not reuse a Claude consumer
|
|
185
154
|
subscription or copy credentials from another client.
|
|
186
155
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
daemon at **http://127.0.0.1:11434**. Remote custom endpoints must use HTTPS.
|
|
156
|
+
For Ollama, `/providers` can select cloud, local, or a custom endpoint. With an
|
|
157
|
+
Ollama API key Jecode defaults to `https://ollama.com`; without one it defaults
|
|
158
|
+
to `http://127.0.0.1:11434`. Remote custom endpoints must use HTTPS.
|
|
191
159
|
|
|
192
160
|
## Use the TUI
|
|
193
161
|
|
|
194
|
-
Type
|
|
162
|
+
Type `/` to open searchable command completion inside the composer.
|
|
195
163
|
|
|
196
164
|
| Command | What it does |
|
|
197
|
-
|
|
198
|
-
|
|
|
199
|
-
|
|
|
200
|
-
|
|
|
201
|
-
|
|
|
202
|
-
|
|
|
203
|
-
|
|
|
204
|
-
|
|
|
205
|
-
|
|
|
206
|
-
|
|
|
207
|
-
|
|
|
208
|
-
|
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `/settings` | Manage the selected model and saved non-secret defaults |
|
|
167
|
+
| `/effort` | Change and save reasoning effort directly |
|
|
168
|
+
| `/providers` | Manage provider connections, API keys, ChatGPT sign-in, and Ollama endpoints |
|
|
169
|
+
| `/models` | Search all currently usable provider catalogues and select a model |
|
|
170
|
+
| `/permissions` | Change session tool access and review remembered approvals |
|
|
171
|
+
| `/timeline` | Browse completed turns and select where the next branch should begin |
|
|
172
|
+
| `/compact` | Compact the current branch context without deleting saved conversation history |
|
|
173
|
+
| `/new` | Start a new conversation and reset session tool permissions |
|
|
174
|
+
| `/export` | Save a timestamped Markdown transcript in the launch directory |
|
|
175
|
+
| `/help` | Open a temporary keyboard reference in the composer dock |
|
|
176
|
+
| `/exit` | Restore the terminal and exit |
|
|
209
177
|
|
|
210
178
|
Useful controls:
|
|
211
179
|
|
|
212
|
-
- **Up/Down** moves through
|
|
213
|
-
- **Left/Right** moves the
|
|
180
|
+
- **Up/Down** moves through suggestions, menus, and input history.
|
|
181
|
+
- **Left/Right** moves the cursor or changes an inline value;
|
|
214
182
|
**Ctrl+Left/Right** moves by word.
|
|
215
|
-
- **Backspace/Delete** removes one character;
|
|
216
|
-
|
|
183
|
+
- **Backspace/Delete** removes one character;
|
|
184
|
+
**Ctrl+Backspace/Delete** removes one word.
|
|
185
|
+
- **Home/End** moves to the beginning or end of the composer.
|
|
217
186
|
- **Tab** completes a slash command without running it; **Enter** sends.
|
|
218
187
|
- **Alt+Enter** inserts a newline.
|
|
219
|
-
- **Esc** closes
|
|
188
|
+
- **Esc** closes the active menu or interrupts foreground work.
|
|
220
189
|
- **Ctrl+C** interrupts, or exits while idle. **Ctrl+D** requests a clean exit.
|
|
221
|
-
- **PageUp/PageDown** and the mouse wheel scroll the transcript
|
|
222
|
-
the place you are reading.
|
|
190
|
+
- **PageUp/PageDown** and the mouse wheel scroll the transcript.
|
|
223
191
|
- **Ctrl+O** expands or compacts the latest reasoning or tool-detail block.
|
|
224
192
|
|
|
225
|
-
The
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
193
|
+
The footer keeps the active model, effort, and workspace visible. During work,
|
|
194
|
+
it adds the current state, elapsed time, and interrupt hint. Operational
|
|
195
|
+
feedback uses the same replaceable status area instead of adding noise to the
|
|
196
|
+
conversation or its Markdown export.
|
|
197
|
+
|
|
198
|
+
## Sessions and context
|
|
199
|
+
|
|
200
|
+
Interactive conversations are stored under `~/.jecode/sessions` and scoped to
|
|
201
|
+
the canonical workspace path. A fresh or `/new` conversation is not added to
|
|
202
|
+
the resume picker until it has a settled turn. Resuming and continuing a
|
|
203
|
+
conversation keeps its durable session identity and updates one picker entry
|
|
204
|
+
instead of creating duplicates.
|
|
205
|
+
|
|
206
|
+
`/timeline` shows completed turns in the conversation tree. Selecting an older
|
|
207
|
+
turn changes only the in-memory path: it creates and saves a branch only after
|
|
208
|
+
the next real user message. Cancelling the picker or exiting before that message
|
|
209
|
+
leaves the durable head unchanged. Historical tools are displayed but never
|
|
210
|
+
executed. If a crash interrupted a tool loop, Jecode resumes from the latest
|
|
211
|
+
completed ancestor and lets the next user turn create a branch. `/export`
|
|
212
|
+
writes only the currently selected path.
|
|
213
|
+
|
|
214
|
+
When model-facing context approaches the selected model's usable capacity,
|
|
215
|
+
Jecode asks the provider for a bounded summary of the older prefix and keeps
|
|
216
|
+
recent turns exact. The default trigger is 85% and can be changed from 50% to
|
|
217
|
+
95%. Live provider metadata or Ollama's allocated context determines the budget
|
|
218
|
+
when available; provider safety limits always win.
|
|
219
|
+
|
|
220
|
+
Compaction changes only the projection sent to the model. Complete messages,
|
|
221
|
+
tool evidence, transcript, export, and conversation tree remain intact. The
|
|
222
|
+
branch-local summary anchor is saved with the session so resume does not repeat
|
|
223
|
+
the same compaction. `/compact` requests this process immediately, even below
|
|
224
|
+
the automatic threshold; it leaves very small contexts unchanged. After
|
|
225
|
+
selecting a historical turn, send the first new message before compacting so
|
|
226
|
+
shared history is never rewritten.
|
|
227
|
+
|
|
228
|
+
## Batch mode
|
|
229
|
+
|
|
230
|
+
When stdin or stdout is not a terminal, Jecode switches to a plain
|
|
231
|
+
line-oriented mode:
|
|
232
|
+
|
|
233
|
+
```console
|
|
234
|
+
printf "explain this project\n" | jecode --root .
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Batch conversations are stateless. Dangerous tools remain denied unless
|
|
238
|
+
`--auto-approve` is supplied explicitly. Terminal failures are written to
|
|
239
|
+
stderr and exit non-zero so scripts and CI pipelines can stop reliably.
|
|
231
240
|
|
|
232
241
|
## Configuration
|
|
233
242
|
|
|
234
243
|
Startup precedence is: command-line flags, environment variables, saved
|
|
235
|
-
settings, built-in defaults.
|
|
244
|
+
settings, then built-in defaults.
|
|
236
245
|
|
|
237
246
|
| Flag | Environment | Default |
|
|
238
|
-
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
|
|
|
246
|
-
|
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
permissions where the operating system supports
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
Interactive conversations are stored under **~/.jecode/sessions**, scoped to
|
|
260
|
-
the canonical workspace path. A checkpoint contains normalized messages and
|
|
261
|
-
the settled transcript needed to redraw the conversation. It excludes stored
|
|
262
|
-
provider credentials, OAuth tokens, provider-only opaque response data,
|
|
263
|
-
permission choices, draft composer text, transient footer notices, and pending
|
|
264
|
-
tool state. Session
|
|
265
|
-
files use owner-only modes on POSIX; Windows relies on the user-profile ACL.
|
|
266
|
-
`jecode resume` keeps the same durable session identity and advances that
|
|
267
|
-
session's conversation tree, so reopening and continuing a conversation does
|
|
268
|
-
not create duplicate picker entries. `/new` or a fresh launch starts another
|
|
269
|
-
logical session. **/timeline** shows the completed turns in that tree. Selecting
|
|
270
|
-
an earlier turn changes only the visible path; it creates and persists a branch
|
|
271
|
-
only when the next real message is sent. Cancelling the picker or exiting first
|
|
272
|
-
leaves the durable head untouched, and resume returns to the last branch with a
|
|
273
|
-
persisted turn. Historical tools are displayed but never executed. If a crash
|
|
274
|
-
left the newest turn inside a tool loop, the same session resumes from its
|
|
275
|
-
latest completed ancestor and the next turn becomes a branch because
|
|
276
|
-
provider-only continuation data is intentionally not stored. **/export** writes
|
|
277
|
-
only the currently selected path.
|
|
278
|
-
|
|
279
|
-
When the model-facing context approaches the selected model's usable capacity,
|
|
280
|
-
Jecode asks the provider for one bounded summary of its older prefix and keeps
|
|
281
|
-
the recent turn exact. The trigger defaults to 85% and can be changed from 50%
|
|
282
|
-
through 95% in **/settings**. Live provider metadata or Ollama's allocated
|
|
283
|
-
runtime context determines the budget when available; a metadata failure falls
|
|
284
|
-
back safely without blocking the turn. Only the provider projection is
|
|
285
|
-
replaced: complete messages, tool evidence, transcript, and conversation tree
|
|
286
|
-
remain unchanged. The branch-local summary anchor is checkpointed with the
|
|
287
|
-
session, so resume reuses it instead of summarizing the same prefix again. A
|
|
288
|
-
failed or cancelled optional summary leaves the original context intact; a
|
|
289
|
-
definite provider context-limit rejection may trigger one compacted retry.
|
|
290
|
-
Internal summary requests count toward provider usage but never appear in the
|
|
291
|
-
transcript or Markdown export. **/compact** requests the same model-aware,
|
|
292
|
-
branch-local compaction immediately, even below the automatic trigger. Very
|
|
293
|
-
small contexts are left unchanged. After selecting a historical branch point,
|
|
294
|
-
send its first new message before compacting so shared history is never
|
|
295
|
-
rewritten.
|
|
296
|
-
|
|
297
|
-
Jecode has one interface theme: dark Steel. **NO_COLOR** is supported for
|
|
247
|
+
| --- | --- | --- |
|
|
248
|
+
| `--provider` | `JECODE_PROVIDER` | `anthropic` |
|
|
249
|
+
| `--model` | `JECODE_MODEL` | Provider default or interactive selection |
|
|
250
|
+
| `--ollama-host` | `OLLAMA_HOST` | Cloud with an Ollama key, local without one |
|
|
251
|
+
| `--root` | - | Current directory |
|
|
252
|
+
| `--effort` | `JECODE_EFFORT` | `high` |
|
|
253
|
+
| `--max-tokens` | `JECODE_MAX_TOKENS` | `64000`; not sent by `openai-codex` |
|
|
254
|
+
| `--max-steps` | `JECODE_MAX_STEPS` | `40` |
|
|
255
|
+
| `--compaction-percent` | `JECODE_COMPACTION_PERCENT` | `85`; accepts `50` through `95` |
|
|
256
|
+
| `--reduced-motion` | `JECODE_REDUCED_MOTION=1` | Off |
|
|
257
|
+
| `--auto-approve` | `JECODE_AUTO_APPROVE=1` | Off |
|
|
258
|
+
| `--ephemeral` | `JECODE_EPHEMERAL=1` | Off |
|
|
259
|
+
|
|
260
|
+
Non-secret preferences live in `~/.jecode/settings.json`. Explicitly saved API
|
|
261
|
+
keys live in `~/.jecode/credentials.json`, while ChatGPT OAuth accounts live in
|
|
262
|
+
`~/.jecode/accounts.json`. Environment credentials always take precedence.
|
|
263
|
+
Secret stores use owner-only permissions where the operating system supports
|
|
264
|
+
them.
|
|
265
|
+
|
|
266
|
+
Jecode has one current interface theme, Dark Steel. `NO_COLOR` is supported for
|
|
298
267
|
terminals and pipelines that disable colour.
|
|
299
268
|
|
|
300
|
-
## Automation
|
|
301
|
-
|
|
302
|
-
When stdin or stdout is piped, Jecode switches to a plain line-oriented mode:
|
|
303
|
-
|
|
304
|
-
~~~console
|
|
305
|
-
printf "explain this project\n" | jecode --root .
|
|
306
|
-
~~~
|
|
307
|
-
|
|
308
|
-
Batch conversations are never written to the session store.
|
|
309
|
-
|
|
310
|
-
Dangerous tools stay denied in batch mode unless **--auto-approve** is supplied
|
|
311
|
-
explicitly. A terminal batch failure is written to stderr and exits non-zero,
|
|
312
|
-
so shell pipelines can stop reliably.
|
|
313
|
-
|
|
314
269
|
## Safety model
|
|
315
270
|
|
|
316
271
|
Jecode treats model output, workspace content, tool output, and terminal text as
|
|
317
272
|
untrusted data.
|
|
318
273
|
|
|
319
|
-
-
|
|
320
|
-
junction components,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
274
|
+
- Current filesystem tools are confined to the selected workspace. Writes
|
|
275
|
+
reject symlink and junction components, revalidate boundaries, and use atomic
|
|
276
|
+
replacement.
|
|
277
|
+
- Dangerous tools ask by default unless explicitly allowed for the session or
|
|
278
|
+
the process starts with `--auto-approve`.
|
|
279
|
+
- Credential fields are masked and excluded from transcripts. Recognized
|
|
280
|
+
credential values are redacted before output reaches the model, screen,
|
|
281
|
+
history, or export.
|
|
282
|
+
- Approved shell commands receive no secret-bearing environment variables.
|
|
283
|
+
`SSH_AUTH_SOCK` is preserved so Git and SSH can use the user's agent, which
|
|
284
|
+
means an approved command may ask that agent to authenticate.
|
|
328
285
|
- ChatGPT OAuth uses PKCE and an exact loopback callback or the OpenAI device
|
|
329
|
-
flow. Refresh-token rotation is serialized across Jecode processes
|
|
330
|
-
tokens are withheld and redacted like API keys.
|
|
286
|
+
flow. Refresh-token rotation is serialized across Jecode processes.
|
|
331
287
|
- Terminal control characters are neutralized before rendering.
|
|
332
|
-
- Remote Ollama endpoints require HTTPS
|
|
333
|
-
rather than followed across an implicit trust boundary.
|
|
288
|
+
- Remote Ollama endpoints require HTTPS, and provider redirects are rejected.
|
|
334
289
|
- Provider handshakes and idle response bodies have finite deadlines. Only
|
|
335
290
|
idempotent catalogue reads retry; generation requests are never replayed.
|
|
336
|
-
- Model and filesystem input are bounded before
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
and treated as untrusted when loaded. A live lease prevents the same saved
|
|
340
|
-
session from being resumed by two Jecode processes at once.
|
|
291
|
+
- Model, terminal, and filesystem input are bounded before use.
|
|
292
|
+
- Session files are versioned, size-bounded, atomically checkpointed, and
|
|
293
|
+
treated as untrusted when loaded. A live lease prevents concurrent resume.
|
|
341
294
|
|
|
342
|
-
`run_command` is not an operating-system sandbox
|
|
343
|
-
|
|
344
|
-
commands carefully and reserve
|
|
295
|
+
`run_command` is not an operating-system sandbox. An approved command can still
|
|
296
|
+
access files and account resources available to the current user. Review
|
|
297
|
+
commands carefully and reserve `--auto-approve` for controlled environments.
|
|
345
298
|
|
|
346
|
-
|
|
299
|
+
Read [SECURITY.md](SECURITY.md) before reporting a vulnerability.
|
|
347
300
|
|
|
348
|
-
##
|
|
301
|
+
## Project direction
|
|
349
302
|
|
|
350
|
-
|
|
351
|
-
|
|
303
|
+
The terminal is Jecode's current primary interface, not the limit of the
|
|
304
|
+
product. Future interfaces may reuse the same controller, session model, and
|
|
305
|
+
visible control system. Expansion must remain deliberate: no hidden model
|
|
306
|
+
hierarchy, no delegated authority, and no weakening of the zero-dependency
|
|
307
|
+
runtime.
|
|
352
308
|
|
|
353
|
-
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
309
|
+
The single-controller rule limits delegation, not duration or scope. Jecode may
|
|
310
|
+
eventually supervise visible, interruptible processes such as development
|
|
311
|
+
servers, file watchers, and test runners. Those processes remain tools owned by
|
|
312
|
+
the controller; they do not receive independent goals, model loops, or tool
|
|
313
|
+
authority.
|
|
358
314
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
expectations.
|
|
315
|
+
The selected workspace is the current default filesystem boundary. Any future
|
|
316
|
+
access to additional directories or resources must use explicit, reviewable,
|
|
317
|
+
revocable grants rather than silently widening that boundary.
|
|
363
318
|
|
|
364
|
-
##
|
|
319
|
+
## Build from source
|
|
365
320
|
|
|
366
|
-
|
|
321
|
+
```console
|
|
322
|
+
git clone https://github.com/giovannijecha/jecode.git
|
|
323
|
+
cd jecode
|
|
367
324
|
npm ci --ignore-scripts
|
|
368
|
-
npm run
|
|
369
|
-
|
|
325
|
+
npm run build:release
|
|
326
|
+
npm link
|
|
327
|
+
jecode --version
|
|
328
|
+
```
|
|
370
329
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
330
|
+
Development runs TypeScript directly with `npm run start`. `dist/` is an
|
|
331
|
+
ignored generated tree used only by linked commands and release tarballs.
|
|
332
|
+
`npm run pack:release` rebuilds it from a clean target. Installing the published
|
|
333
|
+
package runs no compilation or installation scripts.
|
|
374
334
|
|
|
375
|
-
|
|
376
|
-
npm run tui:lab
|
|
377
|
-
~~~
|
|
335
|
+
Run the complete project checks with:
|
|
378
336
|
|
|
379
|
-
|
|
337
|
+
```console
|
|
338
|
+
npm run check
|
|
339
|
+
```
|
|
380
340
|
|
|
381
|
-
|
|
382
|
-
|
|
341
|
+
Use `npm run tui:lab` to inspect production TUI components with inert local
|
|
342
|
+
fixtures, and `npm run bench:transcript` for a manual long-session rendering
|
|
343
|
+
probe. Architecture and security boundaries are documented in
|
|
344
|
+
[docs/architecture.md](docs/architecture.md); brand assets and usage rules live
|
|
383
345
|
in [docs/brand.md](docs/brand.md).
|
|
384
346
|
|
|
347
|
+
## Community
|
|
348
|
+
|
|
349
|
+
- Ask questions, share workflows, and explore early ideas in
|
|
350
|
+
[GitHub Discussions](https://github.com/giovannijecha/jecode/discussions).
|
|
351
|
+
- Report reproducible bugs and focused feature requests through
|
|
352
|
+
[GitHub Issues](https://github.com/giovannijecha/jecode/issues).
|
|
353
|
+
- Report security concerns privately through the repository Security tab.
|
|
354
|
+
|
|
355
|
+
Public pull requests are not accepted at this stage. Code changes remain a
|
|
356
|
+
maintainer and invited-collaborator workflow. See
|
|
357
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) and
|
|
358
|
+
[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
359
|
+
|
|
385
360
|
## License
|
|
386
361
|
|
|
387
362
|
Jecode is available under the [MIT License](LICENSE).
|
package/dist/config.js
CHANGED
|
@@ -3,29 +3,24 @@ import * as path from "node:path";
|
|
|
3
3
|
import { DEFAULT_COMPACTION_PERCENT, MAX_COMPACTION_PERCENT, MIN_COMPACTION_PERCENT, } from "./context/policy.js";
|
|
4
4
|
import { EFFORTS, readSettings } from "./settings.js";
|
|
5
5
|
import { parseOllamaEndpoint } from "./providers/ollama-endpoint.js";
|
|
6
|
-
const
|
|
6
|
+
const VALUE_FLAGS = [
|
|
7
7
|
"provider",
|
|
8
8
|
"model",
|
|
9
9
|
"ollama-host",
|
|
10
|
-
"reduced-motion",
|
|
11
10
|
"effort",
|
|
12
11
|
"max-tokens",
|
|
13
12
|
"max-steps",
|
|
14
13
|
"compaction-percent",
|
|
15
14
|
"root",
|
|
15
|
+
];
|
|
16
|
+
const BOOLEAN_FLAGS = [
|
|
17
|
+
"reduced-motion",
|
|
16
18
|
"auto-approve",
|
|
17
19
|
"ephemeral",
|
|
18
20
|
];
|
|
21
|
+
const FLAGS = [...VALUE_FLAGS, ...BOOLEAN_FLAGS];
|
|
19
22
|
export function loadConfig(argv, saved = readSettings()) {
|
|
20
23
|
const flags = parseFlags(argv);
|
|
21
|
-
// A flag nobody declared is a typo. Swallowed in silence it becomes a
|
|
22
|
-
// setting the user believes is on, and the run that ignores it looks like
|
|
23
|
-
// the feature is broken rather than misspelled.
|
|
24
|
-
for (const name of Object.keys(flags)) {
|
|
25
|
-
if (!FLAGS.includes(name)) {
|
|
26
|
-
throw new Error(`unknown flag --${name} (known: ${FLAGS.map((f) => `--${f}`).join(", ")})`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
24
|
const providerId = pick(flags.provider, process.env.JECODE_PROVIDER, saved.provider ?? "anthropic");
|
|
30
25
|
const ollamaHost = optional(flags["ollama-host"], process.env.OLLAMA_HOST, saved.ollamaHost);
|
|
31
26
|
const effort = pick(flags.effort, process.env.JECODE_EFFORT, saved.effort ?? "high");
|
|
@@ -45,7 +40,9 @@ export function loadConfig(argv, saved = readSettings()) {
|
|
|
45
40
|
maxSteps: toInt(pick(flags["max-steps"], process.env.JECODE_MAX_STEPS, String(saved.maxSteps ?? 40)), "max-steps"),
|
|
46
41
|
compactionPercent: toPercent(pick(flags["compaction-percent"], process.env.JECODE_COMPACTION_PERCENT, String(saved.compactionPercent ?? DEFAULT_COMPACTION_PERCENT))),
|
|
47
42
|
root: path.resolve(pick(flags.root, undefined, process.cwd())),
|
|
48
|
-
autoApprove: flags["auto-approve"] === "true" ||
|
|
43
|
+
autoApprove: flags["auto-approve"] === "true" ||
|
|
44
|
+
flags["auto-approve"] === "1" ||
|
|
45
|
+
process.env.JECODE_AUTO_APPROVE === "1",
|
|
49
46
|
ephemeral: bool(flags.ephemeral, process.env.JECODE_EPHEMERAL, false),
|
|
50
47
|
};
|
|
51
48
|
}
|
|
@@ -85,27 +82,55 @@ function toPercent(value) {
|
|
|
85
82
|
}
|
|
86
83
|
return percent;
|
|
87
84
|
}
|
|
88
|
-
//
|
|
85
|
+
// Value flags accept --key value and --key=value. Boolean flags are bare or
|
|
86
|
+
// take a real boolean value, so an accidental positional argument is never
|
|
87
|
+
// swallowed as configuration.
|
|
89
88
|
function parseFlags(argv) {
|
|
90
89
|
const flags = {};
|
|
91
90
|
for (let i = 0; i < argv.length; i++) {
|
|
92
91
|
const arg = argv[i];
|
|
93
|
-
if (arg === undefined
|
|
92
|
+
if (arg === undefined)
|
|
94
93
|
continue;
|
|
94
|
+
if (!arg.startsWith("--"))
|
|
95
|
+
throw new Error(`unexpected argument "${arg}"`);
|
|
95
96
|
const body = arg.slice(2);
|
|
96
97
|
const eq = body.indexOf("=");
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
const name = eq === -1 ? body : body.slice(0, eq);
|
|
99
|
+
const inline = eq === -1 ? undefined : body.slice(eq + 1);
|
|
100
|
+
if (!FLAGS.includes(name)) {
|
|
101
|
+
throw new Error(`unknown flag --${name} (known: ${FLAGS.map((flag) => `--${flag}`).join(", ")})`);
|
|
102
|
+
}
|
|
103
|
+
if (BOOLEAN_FLAGS.includes(name)) {
|
|
104
|
+
if (inline === undefined) {
|
|
105
|
+
const next = argv[i + 1];
|
|
106
|
+
if (next !== undefined && ["true", "false", "1", "0"].includes(next)) {
|
|
107
|
+
flags[name] = next;
|
|
108
|
+
i++;
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
flags[name] = "true";
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (["true", "false", "1", "0"].includes(inline)) {
|
|
115
|
+
flags[name] = inline;
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
throw new Error(`--${name} must be true or false`);
|
|
119
|
+
}
|
|
99
120
|
continue;
|
|
100
121
|
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
122
|
+
if (inline !== undefined) {
|
|
123
|
+
if (inline === "")
|
|
124
|
+
throw new Error(`--${name} requires a value`);
|
|
125
|
+
flags[name] = inline;
|
|
126
|
+
continue;
|
|
105
127
|
}
|
|
106
|
-
|
|
107
|
-
|
|
128
|
+
const next = argv[i + 1];
|
|
129
|
+
if (next === undefined || next.startsWith("--")) {
|
|
130
|
+
throw new Error(`--${name} requires a value`);
|
|
108
131
|
}
|
|
132
|
+
flags[name] = next;
|
|
133
|
+
i++;
|
|
109
134
|
}
|
|
110
135
|
return flags;
|
|
111
136
|
}
|
|
@@ -40,17 +40,21 @@ export function credentialRedactor(source = process.env) {
|
|
|
40
40
|
const ready = [];
|
|
41
41
|
let at = 0;
|
|
42
42
|
while (at < combined.length) {
|
|
43
|
+
const rest = combined.slice(at);
|
|
44
|
+
// A complete shorter credential can also be the prefix of a longer
|
|
45
|
+
// one. Hold that ambiguous suffix until the next chunk proves which
|
|
46
|
+
// value arrived, otherwise the longer credential leaks its tail.
|
|
47
|
+
if (rest.length < longest &&
|
|
48
|
+
values.some((value) => value.length > rest.length && value.startsWith(rest))) {
|
|
49
|
+
pending = rest;
|
|
50
|
+
return ready.join("");
|
|
51
|
+
}
|
|
43
52
|
const complete = values.find((value) => combined.startsWith(value, at));
|
|
44
53
|
if (complete !== undefined) {
|
|
45
54
|
ready.push(REDACTED);
|
|
46
55
|
at += complete.length;
|
|
47
56
|
continue;
|
|
48
57
|
}
|
|
49
|
-
const rest = combined.slice(at);
|
|
50
|
-
if (rest.length < longest && values.some((value) => value.startsWith(rest))) {
|
|
51
|
-
pending = rest;
|
|
52
|
-
return ready.join("");
|
|
53
|
-
}
|
|
54
58
|
ready.push(combined[at]);
|
|
55
59
|
at++;
|
|
56
60
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Translation between the normalized vocabulary and the Anthropic wire shape.
|
|
2
2
|
// Pure functions, no I/O — which is what makes them testable without a key.
|
|
3
|
+
import { wireTokenCount } from "./wire-usage.js";
|
|
3
4
|
export function toWireTool(tool) {
|
|
4
5
|
return { name: tool.name, description: tool.description, input_schema: tool.input };
|
|
5
6
|
}
|
|
@@ -70,10 +71,10 @@ function normalizeUsage(data) {
|
|
|
70
71
|
if (usage === undefined)
|
|
71
72
|
return undefined;
|
|
72
73
|
return {
|
|
73
|
-
inputTokens: usage.input_tokens
|
|
74
|
-
outputTokens: usage.output_tokens
|
|
75
|
-
cachedInputTokens: usage.cache_read_input_tokens
|
|
76
|
-
cacheWriteInputTokens: usage.cache_creation_input_tokens
|
|
74
|
+
inputTokens: wireTokenCount(usage.input_tokens),
|
|
75
|
+
outputTokens: wireTokenCount(usage.output_tokens),
|
|
76
|
+
cachedInputTokens: wireTokenCount(usage.cache_read_input_tokens),
|
|
77
|
+
cacheWriteInputTokens: wireTokenCount(usage.cache_creation_input_tokens),
|
|
77
78
|
reasoningTokens: 0,
|
|
78
79
|
};
|
|
79
80
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// Chat Completions differs on two points that matter here — a tool result is a
|
|
7
7
|
// message of its own with role "tool", not a block inside a user turn, and tool
|
|
8
8
|
// arguments travel as a JSON string rather than an object.
|
|
9
|
+
import { wireTokenCount } from "./wire-usage.js";
|
|
9
10
|
export function toWireTool(tool) {
|
|
10
11
|
return {
|
|
11
12
|
type: "function",
|
|
@@ -83,8 +84,8 @@ function normalizeUsage(reply) {
|
|
|
83
84
|
if (reply.usage === undefined)
|
|
84
85
|
return undefined;
|
|
85
86
|
return {
|
|
86
|
-
inputTokens: reply.usage.prompt_tokens
|
|
87
|
-
outputTokens: reply.usage.completion_tokens
|
|
87
|
+
inputTokens: wireTokenCount(reply.usage.prompt_tokens),
|
|
88
|
+
outputTokens: wireTokenCount(reply.usage.completion_tokens),
|
|
88
89
|
cachedInputTokens: 0,
|
|
89
90
|
cacheWriteInputTokens: 0,
|
|
90
91
|
reasoningTokens: 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Translation between the normalized vocabulary and the OpenAI Responses wire
|
|
2
2
|
// shape: a flat `input` list where tool calls and their outputs are top-level
|
|
3
3
|
// items keyed by `call_id`, rather than blocks nested inside a message.
|
|
4
|
+
import { wireTokenCount } from "./wire-usage.js";
|
|
4
5
|
export function toWireTool(tool) {
|
|
5
6
|
return {
|
|
6
7
|
type: "function",
|
|
@@ -83,11 +84,11 @@ function normalizeUsage(data) {
|
|
|
83
84
|
if (usage === undefined || usage === null)
|
|
84
85
|
return undefined;
|
|
85
86
|
return {
|
|
86
|
-
inputTokens: usage.input_tokens
|
|
87
|
-
outputTokens: usage.output_tokens
|
|
88
|
-
cachedInputTokens: usage.input_tokens_details?.cached_tokens
|
|
89
|
-
cacheWriteInputTokens: usage.input_tokens_details?.cache_write_tokens
|
|
90
|
-
reasoningTokens: usage.output_tokens_details?.reasoning_tokens
|
|
87
|
+
inputTokens: wireTokenCount(usage.input_tokens),
|
|
88
|
+
outputTokens: wireTokenCount(usage.output_tokens),
|
|
89
|
+
cachedInputTokens: wireTokenCount(usage.input_tokens_details?.cached_tokens),
|
|
90
|
+
cacheWriteInputTokens: wireTokenCount(usage.input_tokens_details?.cache_write_tokens),
|
|
91
|
+
reasoningTokens: wireTokenCount(usage.output_tokens_details?.reasoning_tokens),
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
94
|
// Arguments arrive as a JSON string and models vary in how they escape it, so
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Usage comes from remote JSON and eventually reaches the strict session
|
|
2
|
+
// codec. Normalize it at the wire boundary so one malformed counter cannot
|
|
3
|
+
// make an otherwise valid saved session unreadable.
|
|
4
|
+
export function wireTokenCount(value) {
|
|
5
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
6
|
+
}
|
package/dist/tui/app.js
CHANGED
|
@@ -40,12 +40,21 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
40
40
|
let escapeTimer;
|
|
41
41
|
let stopResize = () => { };
|
|
42
42
|
let stopInput = () => { };
|
|
43
|
+
let failure;
|
|
43
44
|
// Timers outlive the teardown they were scheduled before. Painting after the
|
|
44
45
|
// terminal has been handed back would write escapes into the user's shell.
|
|
45
46
|
let live = true;
|
|
46
47
|
const done = new Promise((resolve) => {
|
|
47
48
|
closed = resolve;
|
|
48
49
|
});
|
|
50
|
+
const guard = (action) => {
|
|
51
|
+
try {
|
|
52
|
+
action();
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
fail(error);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
49
58
|
const view = () => {
|
|
50
59
|
const now = Date.now();
|
|
51
60
|
return {
|
|
@@ -94,8 +103,9 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
94
103
|
const render = (block) => {
|
|
95
104
|
if (block !== undefined)
|
|
96
105
|
transcript.invalidate(block);
|
|
97
|
-
if (live && frameTimer === undefined)
|
|
98
|
-
frameTimer = setTimeout(draw, FRAME_MS);
|
|
106
|
+
if (live && frameTimer === undefined) {
|
|
107
|
+
frameTimer = setTimeout(() => guard(draw), FRAME_MS);
|
|
108
|
+
}
|
|
99
109
|
};
|
|
100
110
|
const feedback = feedbackController((next) => {
|
|
101
111
|
state.feedback = next;
|
|
@@ -143,6 +153,10 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
143
153
|
terminal.leave();
|
|
144
154
|
closed?.();
|
|
145
155
|
}
|
|
156
|
+
function fail(error) {
|
|
157
|
+
failure ??= { error };
|
|
158
|
+
quit();
|
|
159
|
+
}
|
|
146
160
|
function requestQuit() {
|
|
147
161
|
const activity = state.activity;
|
|
148
162
|
if (activity === undefined) {
|
|
@@ -159,7 +173,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
159
173
|
const activity = begin(kind, label);
|
|
160
174
|
state.activity = activity;
|
|
161
175
|
state.status = label;
|
|
162
|
-
spinTimer = setInterval(() => {
|
|
176
|
+
spinTimer = setInterval(() => guard(() => {
|
|
163
177
|
if (!session.config.reducedMotion)
|
|
164
178
|
state.spin++;
|
|
165
179
|
let activeTool;
|
|
@@ -171,7 +185,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
171
185
|
break;
|
|
172
186
|
}
|
|
173
187
|
render(activeTool);
|
|
174
|
-
}, session.config.reducedMotion ? 1_000 : SPIN_MS);
|
|
188
|
+
}), session.config.reducedMotion ? 1_000 : SPIN_MS);
|
|
175
189
|
render();
|
|
176
190
|
return activity;
|
|
177
191
|
}
|
|
@@ -253,39 +267,47 @@ export async function runApp(session, transcriptRoot, environment = {}) {
|
|
|
253
267
|
}
|
|
254
268
|
}
|
|
255
269
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
return;
|
|
271
|
-
escapeTimer = setTimeout(() => {
|
|
272
|
-
escapeTimer = undefined;
|
|
270
|
+
try {
|
|
271
|
+
terminal.enter(session.config.reducedMotion);
|
|
272
|
+
stopResize = terminal.onResize(() => guard(() => {
|
|
273
|
+
paint.invalidate();
|
|
274
|
+
draw();
|
|
275
|
+
}));
|
|
276
|
+
stopInput = terminal.onInput((chunk) => guard(() => {
|
|
277
|
+
if (escapeTimer !== undefined)
|
|
278
|
+
clearTimeout(escapeTimer);
|
|
279
|
+
for (const key of keys.push(chunk)) {
|
|
280
|
+
if (!live)
|
|
281
|
+
break;
|
|
282
|
+
input.handle(key);
|
|
283
|
+
}
|
|
273
284
|
if (!live)
|
|
274
285
|
return;
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
286
|
+
escapeTimer = setTimeout(() => guard(() => {
|
|
287
|
+
escapeTimer = undefined;
|
|
288
|
+
if (!live)
|
|
289
|
+
return;
|
|
290
|
+
for (const key of keys.flush())
|
|
291
|
+
input.handle(key);
|
|
292
|
+
if (live)
|
|
293
|
+
render();
|
|
294
|
+
}), ESCAPE_MS);
|
|
295
|
+
render();
|
|
296
|
+
}));
|
|
297
|
+
draw();
|
|
298
|
+
if (resumeAtLaunch !== undefined)
|
|
299
|
+
await Promise.race([resumeAtLaunch, done]);
|
|
285
300
|
if (live)
|
|
286
301
|
await done;
|
|
302
|
+
if (failure !== undefined)
|
|
303
|
+
throw failure.error;
|
|
287
304
|
}
|
|
288
305
|
finally {
|
|
289
|
-
|
|
306
|
+
try {
|
|
307
|
+
quit();
|
|
308
|
+
}
|
|
309
|
+
finally {
|
|
310
|
+
await session.persistence?.close();
|
|
311
|
+
}
|
|
290
312
|
}
|
|
291
313
|
}
|
package/dist/tui/picker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// One interaction model for every terminal selector.
|
|
2
2
|
import { row } from "../ui/render.js";
|
|
3
|
-
import { elide } from "../ui/width.js";
|
|
3
|
+
import { elide, graphemes } from "../ui/width.js";
|
|
4
4
|
import { menuWindow, renderMenuRows } from "./components/menu.js";
|
|
5
5
|
import { promptCursor, promptLine } from "./components/prompt.js";
|
|
6
6
|
const WINDOW = 6;
|
|
@@ -33,7 +33,7 @@ export function type(picker, text) {
|
|
|
33
33
|
export function backspace(picker) {
|
|
34
34
|
if (picker.searchable !== true || (picker.query ?? "") === "")
|
|
35
35
|
return picker;
|
|
36
|
-
return withQuery(picker,
|
|
36
|
+
return withQuery(picker, graphemes(picker.query ?? "").slice(0, -1).join(""));
|
|
37
37
|
}
|
|
38
38
|
export function clear(picker) {
|
|
39
39
|
return withQuery(picker, "");
|
package/dist/ui/terminal-text.js
CHANGED
|
@@ -38,5 +38,8 @@ export function terminalText(text, options = {}) {
|
|
|
38
38
|
return safe;
|
|
39
39
|
}
|
|
40
40
|
function isBidiControl(code) {
|
|
41
|
-
return
|
|
41
|
+
return code === 0x061c ||
|
|
42
|
+
(code >= 0x200e && code <= 0x200f) ||
|
|
43
|
+
(code >= 0x202a && code <= 0x202e) ||
|
|
44
|
+
(code >= 0x2066 && code <= 0x2069);
|
|
42
45
|
}
|