@giovannijecha/jecode 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/timeline.js CHANGED
@@ -7,7 +7,7 @@ import { usageFromHistory } from "./usage.js";
7
7
  import { heading } from "./tui/picker.js";
8
8
  export function timelinePicker(conversation, palette) {
9
9
  const entries = timelineEntries(conversation);
10
- const selectedId = conversation.latestCompleted()?.activeNodeId ?? 0;
10
+ const selectedId = conversation.latestResumable()?.activeNodeId ?? 0;
11
11
  const index = Math.max(0, entries.findIndex((entry) => entry.node.id === selectedId));
12
12
  return Object.freeze({
13
13
  picker: {
@@ -18,7 +18,7 @@ export function timelinePicker(conversation, palette) {
18
18
  options: entries.map((entry) => ({
19
19
  label: `${entry.prefix}${preview(entry.node)}`,
20
20
  hint: stamp(entry.node.createdAt),
21
- ...(entry.node.id === selectedId ? { value: "active" } : {}),
21
+ ...timelineValue(entry.node, selectedId),
22
22
  })),
23
23
  index,
24
24
  },
@@ -38,12 +38,12 @@ export async function selectTimeline(session, choose) {
38
38
  return true;
39
39
  }
40
40
  function timelineEntries(conversation) {
41
- const completed = conversation.nodes.filter((node) => node.settlement === "completed");
42
- const completedIds = new Set(completed.map((node) => node.id));
41
+ const resumable = conversation.nodes.filter((node) => node.settlement !== "checkpointed");
42
+ const resumableIds = new Set(resumable.map((node) => node.id));
43
43
  const children = new Map();
44
- for (const node of completed) {
44
+ for (const node of resumable) {
45
45
  let parentId = node.parentId;
46
- while (parentId !== 0 && !completedIds.has(parentId)) {
46
+ while (parentId !== 0 && !resumableIds.has(parentId)) {
47
47
  parentId = conversation.node(parentId)?.parentId ?? 0;
48
48
  }
49
49
  const siblings = children.get(parentId) ?? [];
@@ -70,6 +70,12 @@ function timelineEntries(conversation) {
70
70
  visit(0, []);
71
71
  return entries;
72
72
  }
73
+ function timelineValue(node, selectedId) {
74
+ const state = node.settlement === "completed" ? undefined : node.settlement;
75
+ const active = node.id === selectedId ? "active" : undefined;
76
+ const value = [state, active].filter((part) => part !== undefined).join(" · ");
77
+ return value === "" ? {} : { value };
78
+ }
73
79
  function preview(node) {
74
80
  for (const message of node.messages) {
75
81
  if (message.role !== "user")
@@ -10,6 +10,7 @@ import { saveTranscript } from "../transcript-export.js";
10
10
  import { recordAuxiliaryUsage, recordUsage } from "../usage.js";
11
11
  import { selectTimeline } from "../timeline.js";
12
12
  import { answerAt } from "./approve.js";
13
+ import * as edit from "./editor.js";
13
14
  import { cancel as cancelOpen } from "./overlay.js";
14
15
  import { controllerOptions, turnFailure } from "./session-view.js";
15
16
  import { transcribe } from "./turn.js";
@@ -136,7 +137,7 @@ export function appWorkflows(options) {
136
137
  },
137
138
  usage: (usage) => recordUsage(session.usage, usage),
138
139
  });
139
- const persist = async (checkpoint, settlement) => {
140
+ const persist = async (checkpoint, settlement, failure) => {
140
141
  const next = session.conversation.commit({
141
142
  ...(nodeId === undefined ? {} : { nodeId }),
142
143
  parentId,
@@ -149,6 +150,7 @@ export function appWorkflows(options) {
149
150
  messages: checkpoint.slice(historyStart),
150
151
  blocks: state.blocks.slice(blockStart),
151
152
  ...(context === undefined ? {} : { context }),
153
+ ...(failure === undefined ? {} : { failure }),
152
154
  }, settlement);
153
155
  await session.persistence?.checkpoint(next);
154
156
  session.conversation = next;
@@ -217,18 +219,65 @@ export function appWorkflows(options) {
217
219
  return compacted;
218
220
  };
219
221
  let finishReason;
222
+ let failed;
220
223
  try {
221
224
  await runTurn(history, controllerOptions(session, permissions.availableTools()), events, activity.control.signal, modelHistory);
222
225
  }
223
226
  catch (error) {
224
227
  const interrupted = activity.control.signal.aborted;
225
- finishReason = interrupted ? "interrupted" : "failed";
226
- options.emit(turnFailure(session, error, interrupted));
228
+ const completed = nodeId !== undefined && session.conversation.activeNodeId === nodeId &&
229
+ session.conversation.activeNode?.settlement === "completed";
230
+ if (completed) {
231
+ const notice = turnFailure(session, error, interrupted);
232
+ feedback.show({ text: notice.text, tone: notice.tone, timeoutMs: 6_000 });
233
+ }
234
+ else {
235
+ finishReason = interrupted ? "interrupted" : "failed";
236
+ failed = { error: error, interrupted };
237
+ }
227
238
  }
228
239
  finally {
229
- events.finish(finishReason);
230
- options.finishActivity(activity);
240
+ try {
241
+ events.finish(finishReason);
242
+ if (failed !== undefined) {
243
+ const notice = turnFailure(session, failed.error, failed.interrupted);
244
+ const settlement = failed.interrupted ? "interrupted" : "failed";
245
+ const failure = {
246
+ text: notice.text,
247
+ tone: failed.interrupted ? "warn" : "error",
248
+ };
249
+ options.emit(notice);
250
+ try {
251
+ await persist(closeFailedTurn(history, settlement), settlement, failure);
252
+ }
253
+ catch (error) {
254
+ // A failed persistence boundary cannot remain visible as if it had
255
+ // been saved. Revert to the last durable path and return the input
256
+ // to the composer so the user can retry without losing it.
257
+ options.replaceTranscript();
258
+ state.editor = edit.of(text);
259
+ feedback.show({
260
+ text: error.message,
261
+ tone: "error",
262
+ timeoutMs: 6_000,
263
+ });
264
+ }
265
+ }
266
+ }
267
+ finally {
268
+ options.finishActivity(activity);
269
+ }
231
270
  }
232
271
  }
233
272
  return { command, turn };
234
273
  }
274
+ function closeFailedTurn(history, settlement) {
275
+ const closed = [...history];
276
+ if (closed.at(-1)?.role === "assistant")
277
+ return closed;
278
+ const text = settlement === "interrupted"
279
+ ? "The previous attempt was interrupted by the user before completion."
280
+ : "The previous attempt failed before completion.";
281
+ closed.push({ role: "assistant", content: [{ kind: "text", text }] });
282
+ return closed;
283
+ }
package/dist/tui/app.js CHANGED
@@ -40,12 +40,22 @@ export async function runApp(session, transcriptRoot, environment = {}) {
40
40
  let escapeTimer;
41
41
  let stopResize = () => { };
42
42
  let stopInput = () => { };
43
+ let failure;
44
+ let activeWorkflow;
43
45
  // Timers outlive the teardown they were scheduled before. Painting after the
44
46
  // terminal has been handed back would write escapes into the user's shell.
45
47
  let live = true;
46
48
  const done = new Promise((resolve) => {
47
49
  closed = resolve;
48
50
  });
51
+ const guard = (action) => {
52
+ try {
53
+ action();
54
+ }
55
+ catch (error) {
56
+ fail(error);
57
+ }
58
+ };
49
59
  const view = () => {
50
60
  const now = Date.now();
51
61
  return {
@@ -94,8 +104,9 @@ export async function runApp(session, transcriptRoot, environment = {}) {
94
104
  const render = (block) => {
95
105
  if (block !== undefined)
96
106
  transcript.invalidate(block);
97
- if (live && frameTimer === undefined)
98
- frameTimer = setTimeout(draw, FRAME_MS);
107
+ if (live && frameTimer === undefined) {
108
+ frameTimer = setTimeout(() => guard(draw), FRAME_MS);
109
+ }
99
110
  };
100
111
  const feedback = feedbackController((next) => {
101
112
  state.feedback = next;
@@ -131,18 +142,42 @@ export async function runApp(session, transcriptRoot, environment = {}) {
131
142
  if (!live)
132
143
  return;
133
144
  live = false;
134
- stopInput();
135
- stopResize();
145
+ safely(stopInput);
146
+ safely(stopResize);
136
147
  if (spinTimer !== undefined)
137
148
  clearInterval(spinTimer);
138
149
  if (frameTimer !== undefined)
139
150
  clearTimeout(frameTimer);
140
151
  if (escapeTimer !== undefined)
141
152
  clearTimeout(escapeTimer);
142
- feedback.close();
143
- terminal.leave();
153
+ safely(() => feedback.close());
154
+ safely(() => terminal.leave());
144
155
  closed?.();
145
156
  }
157
+ function safely(action) {
158
+ try {
159
+ action();
160
+ }
161
+ catch (error) {
162
+ failure ??= { error };
163
+ }
164
+ }
165
+ function fail(error) {
166
+ failure ??= { error };
167
+ state.open = overlay.cancel(state.open);
168
+ state.activity?.control.abort(error);
169
+ quit();
170
+ }
171
+ function track(work) {
172
+ const tracked = work
173
+ .catch((error) => fail(error))
174
+ .finally(() => {
175
+ if (activeWorkflow === tracked)
176
+ activeWorkflow = undefined;
177
+ });
178
+ activeWorkflow = tracked;
179
+ return tracked;
180
+ }
146
181
  function requestQuit() {
147
182
  const activity = state.activity;
148
183
  if (activity === undefined) {
@@ -159,7 +194,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
159
194
  const activity = begin(kind, label);
160
195
  state.activity = activity;
161
196
  state.status = label;
162
- spinTimer = setInterval(() => {
197
+ spinTimer = setInterval(() => guard(() => {
163
198
  if (!session.config.reducedMotion)
164
199
  state.spin++;
165
200
  let activeTool;
@@ -171,7 +206,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
171
206
  break;
172
207
  }
173
208
  render(activeTool);
174
- }, session.config.reducedMotion ? 1_000 : SPIN_MS);
209
+ }), session.config.reducedMotion ? 1_000 : SPIN_MS);
175
210
  render();
176
211
  return activity;
177
212
  }
@@ -212,7 +247,10 @@ export async function runApp(session, transcriptRoot, environment = {}) {
212
247
  session,
213
248
  state,
214
249
  feedback,
215
- actions,
250
+ actions: {
251
+ command: (text) => track(actions.command(text)),
252
+ turn: (text) => track(actions.turn(text)),
253
+ },
216
254
  live: () => live,
217
255
  quit,
218
256
  requestQuit,
@@ -222,7 +260,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
222
260
  });
223
261
  const resumeAtLaunch = session.resume === undefined
224
262
  ? undefined
225
- : openResumedSession(session.resume);
263
+ : track(openResumedSession(session.resume));
226
264
  async function openResumedSession(launch) {
227
265
  while (live) {
228
266
  const index = await new Promise((resolve) => {
@@ -253,39 +291,48 @@ export async function runApp(session, transcriptRoot, environment = {}) {
253
291
  }
254
292
  }
255
293
  }
256
- terminal.enter(session.config.reducedMotion);
257
- stopResize = terminal.onResize(() => {
258
- paint.invalidate();
259
- draw();
260
- });
261
- stopInput = terminal.onInput((chunk) => {
262
- if (escapeTimer !== undefined)
263
- clearTimeout(escapeTimer);
264
- for (const key of keys.push(chunk)) {
265
- if (!live)
266
- break;
267
- input.handle(key);
268
- }
269
- if (!live)
270
- return;
271
- escapeTimer = setTimeout(() => {
272
- escapeTimer = undefined;
294
+ try {
295
+ terminal.enter(session.config.reducedMotion);
296
+ stopResize = terminal.onResize(() => guard(() => {
297
+ paint.invalidate();
298
+ draw();
299
+ }));
300
+ stopInput = terminal.onInput((chunk) => guard(() => {
301
+ if (escapeTimer !== undefined)
302
+ clearTimeout(escapeTimer);
303
+ for (const key of keys.push(chunk)) {
304
+ if (!live)
305
+ break;
306
+ input.handle(key);
307
+ }
273
308
  if (!live)
274
309
  return;
275
- for (const key of keys.flush())
276
- input.handle(key);
277
- if (live)
278
- render();
279
- }, ESCAPE_MS);
280
- render();
281
- });
282
- draw();
283
- try {
284
- await resumeAtLaunch;
310
+ escapeTimer = setTimeout(() => guard(() => {
311
+ escapeTimer = undefined;
312
+ if (!live)
313
+ return;
314
+ for (const key of keys.flush())
315
+ input.handle(key);
316
+ if (live)
317
+ render();
318
+ }), ESCAPE_MS);
319
+ render();
320
+ }));
321
+ draw();
322
+ if (resumeAtLaunch !== undefined)
323
+ await Promise.race([resumeAtLaunch, done]);
285
324
  if (live)
286
325
  await done;
326
+ if (failure !== undefined)
327
+ throw failure.error;
287
328
  }
288
329
  finally {
289
- await session.persistence?.close();
330
+ try {
331
+ quit();
332
+ await activeWorkflow;
333
+ }
334
+ finally {
335
+ await session.persistence?.close();
336
+ }
290
337
  }
291
338
  }
@@ -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, Array.from(picker.query ?? "").slice(0, -1).join(""));
36
+ return withQuery(picker, graphemes(picker.query ?? "").slice(0, -1).join(""));
37
37
  }
38
38
  export function clear(picker) {
39
39
  return withQuery(picker, "");
@@ -38,5 +38,8 @@ export function terminalText(text, options = {}) {
38
38
  return safe;
39
39
  }
40
40
  function isBidiControl(code) {
41
- return (code >= 0x202a && code <= 0x202e) || (code >= 0x2066 && code <= 0x2069);
41
+ return code === 0x061c ||
42
+ (code >= 0x200e && code <= 0x200f) ||
43
+ (code >= 0x202a && code <= 0x202e) ||
44
+ (code >= 0x2066 && code <= 0x2069);
42
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {