@giovannijecha/jecode 0.7.1 → 0.7.3

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/tui/app.js CHANGED
@@ -21,7 +21,7 @@ import { appWorkflows } from "./app-workflows.js";
21
21
  import { sessionPermissions } from "../permissions.js";
22
22
  import { resumePicker } from "./resume.js";
23
23
  const FRAME_MS = 16;
24
- const SPIN_MS = 80;
24
+ const ACTIVITY_REFRESH_MS = 1_000;
25
25
  /** How long a lone escape waits to prove it is not the start of a sequence. */
26
26
  const ESCAPE_MS = 25;
27
27
  export async function runApp(session, transcriptRoot, environment = {}) {
@@ -36,11 +36,12 @@ export async function runApp(session, transcriptRoot, environment = {}) {
36
36
  const permissions = sessionPermissions(session.tools, session.config.autoApprove);
37
37
  let closed;
38
38
  let frameTimer;
39
- let spinTimer;
39
+ let activityTimer;
40
40
  let escapeTimer;
41
41
  let stopResize = () => { };
42
42
  let stopInput = () => { };
43
43
  let failure;
44
+ let activeWorkflow;
44
45
  // Timers outlive the teardown they were scheduled before. Painting after the
45
46
  // terminal has been handed back would write escapes into the user's shell.
46
47
  let live = true;
@@ -69,8 +70,6 @@ export async function runApp(session, transcriptRoot, environment = {}) {
69
70
  : activityStatus(state.activity, state.status ?? state.activity.label, now),
70
71
  feedback: state.feedback,
71
72
  readiness: turnBlocker(session),
72
- spin: state.spin,
73
- reducedMotion: session.config.reducedMotion,
74
73
  now,
75
74
  modal: overlay.shown(state.open),
76
75
  menu: completionOptions(state.completing),
@@ -97,6 +96,8 @@ export async function runApp(session, transcriptRoot, environment = {}) {
97
96
  }
98
97
  state.lastMaxScroll = frame.maxScroll;
99
98
  paint.paint(frame.rows, frame.cursor);
99
+ if (frame.transcriptPending)
100
+ render();
100
101
  };
101
102
  // Streaming produces a token at a time; painting at that rate is wasted
102
103
  // work, so repaints coalesce onto one frame.
@@ -141,22 +142,42 @@ export async function runApp(session, transcriptRoot, environment = {}) {
141
142
  if (!live)
142
143
  return;
143
144
  live = false;
144
- stopInput();
145
- stopResize();
146
- if (spinTimer !== undefined)
147
- clearInterval(spinTimer);
145
+ safely(stopInput);
146
+ safely(stopResize);
147
+ if (activityTimer !== undefined)
148
+ clearInterval(activityTimer);
148
149
  if (frameTimer !== undefined)
149
150
  clearTimeout(frameTimer);
150
151
  if (escapeTimer !== undefined)
151
152
  clearTimeout(escapeTimer);
152
- feedback.close();
153
- terminal.leave();
153
+ safely(() => feedback.close());
154
+ safely(() => terminal.leave());
154
155
  closed?.();
155
156
  }
157
+ function safely(action) {
158
+ try {
159
+ action();
160
+ }
161
+ catch (error) {
162
+ failure ??= { error };
163
+ }
164
+ }
156
165
  function fail(error) {
157
166
  failure ??= { error };
167
+ state.open = overlay.cancel(state.open);
168
+ state.activity?.control.abort(error);
158
169
  quit();
159
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
+ }
160
181
  function requestQuit() {
161
182
  const activity = state.activity;
162
183
  if (activity === undefined) {
@@ -173,9 +194,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
173
194
  const activity = begin(kind, label);
174
195
  state.activity = activity;
175
196
  state.status = label;
176
- spinTimer = setInterval(() => guard(() => {
177
- if (!session.config.reducedMotion)
178
- state.spin++;
197
+ activityTimer = setInterval(() => guard(() => {
179
198
  let activeTool;
180
199
  for (let index = state.blocks.length - 1; index >= 0; index--) {
181
200
  const block = state.blocks[index];
@@ -185,16 +204,16 @@ export async function runApp(session, transcriptRoot, environment = {}) {
185
204
  break;
186
205
  }
187
206
  render(activeTool);
188
- }), session.config.reducedMotion ? 1_000 : SPIN_MS);
207
+ }), ACTIVITY_REFRESH_MS);
189
208
  render();
190
209
  return activity;
191
210
  }
192
211
  function finishActivity(activity) {
193
212
  if (state.activity !== activity)
194
213
  return;
195
- if (spinTimer !== undefined)
196
- clearInterval(spinTimer);
197
- spinTimer = undefined;
214
+ if (activityTimer !== undefined)
215
+ clearInterval(activityTimer);
216
+ activityTimer = undefined;
198
217
  state.activity = undefined;
199
218
  state.status = undefined;
200
219
  state.open = overlay.cancel(state.open);
@@ -226,7 +245,10 @@ export async function runApp(session, transcriptRoot, environment = {}) {
226
245
  session,
227
246
  state,
228
247
  feedback,
229
- actions,
248
+ actions: {
249
+ command: (text) => track(actions.command(text)),
250
+ turn: (text) => track(actions.turn(text)),
251
+ },
230
252
  live: () => live,
231
253
  quit,
232
254
  requestQuit,
@@ -236,7 +258,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
236
258
  });
237
259
  const resumeAtLaunch = session.resume === undefined
238
260
  ? undefined
239
- : openResumedSession(session.resume);
261
+ : track(openResumedSession(session.resume));
240
262
  async function openResumedSession(launch) {
241
263
  while (live) {
242
264
  const index = await new Promise((resolve) => {
@@ -271,7 +293,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
271
293
  terminal.enter(session.config.reducedMotion);
272
294
  stopResize = terminal.onResize(() => guard(() => {
273
295
  paint.invalidate();
274
- draw();
296
+ render();
275
297
  }));
276
298
  stopInput = terminal.onInput((chunk) => guard(() => {
277
299
  if (escapeTimer !== undefined)
@@ -305,6 +327,7 @@ export async function runApp(session, transcriptRoot, environment = {}) {
305
327
  finally {
306
328
  try {
307
329
  quit();
330
+ await activeWorkflow;
308
331
  }
309
332
  finally {
310
333
  await session.persistence?.close();
@@ -13,8 +13,6 @@ export function render(block, width, pal, context = {}) {
13
13
  case "tool":
14
14
  return renderTool(block, width, pal, {
15
15
  continues: context.previous?.kind === "tool",
16
- spin: context.spin,
17
- reducedMotion: context.reducedMotion,
18
16
  now: context.now,
19
17
  });
20
18
  case "notice":
@@ -10,7 +10,7 @@ export function renderTool(block, width, pal, context = {}) {
10
10
  ...(context.continues === true ? [] : [""]),
11
11
  row(width, [
12
12
  { text: " " },
13
- { text: `${stateGlyph(block, context)} `, fg: statusInk(block.tone, pal), bold: true },
13
+ { text: `${stateGlyph(block)} `, fg: statusInk(block.tone, pal), bold: true },
14
14
  { text: block.name, fg: pal.ink.bright, bold: true },
15
15
  ...(block.target === "" ? [] : [{ text: ` ${block.target}`, fg: pal.technical }]),
16
16
  ], right === "" ? [] : [{ text: right, fg: statusInk(block.tone, pal) }]),
@@ -81,13 +81,9 @@ function emphasized(text, emphasis, fg) {
81
81
  ...(end === text.length ? [] : [{ text: text.slice(end), fg }]),
82
82
  ];
83
83
  }
84
- function stateGlyph(block, context) {
85
- if (block.tone === "pending") {
86
- if (block.startedAt === undefined || context.reducedMotion === true)
87
- return "◌";
88
- const frames = ["◐", "◓", "◑", "◒"];
89
- return frames[Math.floor((context.spin ?? 0) / 2) % frames.length];
90
- }
84
+ function stateGlyph(block) {
85
+ if (block.tone === "pending")
86
+ return "◌";
91
87
  if (block.tone === "fail")
92
88
  return hasColor() ? "●" : "×";
93
89
  if (block.tone === "deny")
@@ -1,13 +1,14 @@
1
1
  // Small projections of the live session used by the shell and footer.
2
2
  import { credentialSource } from "../credentials.js";
3
3
  import { providerFailure } from "../provider-errors.js";
4
- export function controllerOptions(session, tools = session.tools) {
4
+ export function controllerOptions(session, contextPolicy, tools = session.tools) {
5
5
  return {
6
6
  provider: session.provider,
7
7
  tools,
8
8
  model: session.model,
9
9
  system: session.system,
10
10
  maxTokens: session.config.maxTokens,
11
+ contextPolicy,
11
12
  effort: session.config.effort,
12
13
  maxSteps: session.config.maxSteps,
13
14
  toolContext: { root: session.config.root },
@@ -1,138 +1,210 @@
1
1
  // Incremental transcript layout for long-running sessions.
2
2
  import { render } from "./blocks.js";
3
+ const MAX_LAYOUTS = 2;
4
+ const REFLOW_BLOCKS_PER_FRAME = 128;
3
5
  /**
4
- * Keep block rows and cumulative row ends between frames.
6
+ * Keep recent widths and reflow a bounded working set on each frame.
5
7
  *
6
- * The app mutates semantic blocks while streaming, so it marks that one block
7
- * dirty. Appends then cost one render, stable frames cost no block renders,
8
- * and viewport selection starts with a binary search instead of flattening or
9
- * scanning the complete transcript.
8
+ * The visible window is always rendered before it is returned. Older rows use
9
+ * their last known height only to guide scrolling while the remaining blocks
10
+ * are refreshed over later frames. This keeps resize responsive without a
11
+ * worker, hidden loop, or unbounded multi-width cache.
10
12
  */
11
13
  export function transcriptRenderer(draw = render) {
12
- let source;
13
- let layoutWidth = -1;
14
- let layoutPalette;
15
- let entries = [];
16
- let ends = [];
17
- let positions = new WeakMap();
18
- const dirty = new Set();
14
+ let layouts = [];
19
15
  return {
20
16
  viewport(blocks, width, height, scroll, palette, state = {}) {
21
- sync(blocks, width, palette, state);
22
- const visibleHeight = Math.max(0, height);
23
- const totalRows = ends.at(-1) ?? 0;
24
- const maxScroll = Math.max(0, totalRows - visibleHeight);
25
- const offset = Math.min(Math.max(0, scroll), maxScroll);
26
- const start = Math.max(0, totalRows - visibleHeight - offset);
27
- const end = start + visibleHeight;
28
- const rows = [];
29
- let index = firstEndingAfter(start);
30
- let at = index === 0 ? 0 : (ends[index - 1] ?? 0);
31
- while (index < entries.length && at < end) {
32
- const blockRows = entries[index]?.rows ?? [];
33
- rows.push(...blockRows.slice(Math.max(0, start - at), Math.min(blockRows.length, end - at)));
34
- at = ends[index] ?? at;
35
- index++;
36
- }
37
- while (rows.length < visibleHeight)
38
- rows.unshift("");
39
- return { rows, maxScroll };
17
+ const layout = layoutFor(blocks, width, palette);
18
+ // Reflowing rows below a scroll-locked viewport would move the content
19
+ // being read as estimates become exact. Visible rows still resolve on
20
+ // demand; background work resumes when the user follows the tail again.
21
+ if (scroll === 0)
22
+ reflowBatch(layout, state);
23
+ const window = reflowWindow(layout, Math.max(0, height), scroll, state);
24
+ return {
25
+ rows: visibleRows(layout, window, Math.max(0, height)),
26
+ maxScroll: window.maxScroll,
27
+ pending: scroll === 0 && layout.pending > 0,
28
+ };
40
29
  },
41
30
  invalidate(block) {
42
- if (block !== undefined) {
43
- dirty.add(block);
31
+ if (block === undefined) {
32
+ layouts = [];
44
33
  return;
45
34
  }
46
- source = undefined;
47
- entries = [];
48
- ends = [];
49
- positions = new WeakMap();
50
- dirty.clear();
35
+ for (const layout of layouts) {
36
+ const index = layout.positions.get(block);
37
+ if (index === undefined)
38
+ continue;
39
+ const entry = layout.entries[index];
40
+ if (entry?.rows !== undefined) {
41
+ entry.rows = undefined;
42
+ layout.pending++;
43
+ }
44
+ layout.next = Math.max(layout.next, index);
45
+ }
51
46
  },
52
47
  };
53
- function sync(blocks, width, palette, state) {
54
- if (source !== blocks ||
55
- layoutWidth !== width ||
56
- layoutPalette !== palette ||
57
- blocks.length < entries.length ||
58
- edgeChanged(blocks)) {
59
- rebuild(blocks, width, palette, state);
60
- return;
48
+ function layoutFor(blocks, width, palette) {
49
+ const cachedAt = layouts.findIndex((layout) => layout.width === width && layout.palette === palette);
50
+ if (cachedAt !== -1) {
51
+ const cached = layouts[cachedAt];
52
+ layouts.splice(cachedAt, 1);
53
+ if (compatible(cached, blocks)) {
54
+ append(cached, blocks);
55
+ layouts.push(cached);
56
+ return cached;
57
+ }
61
58
  }
62
- append(blocks, width, palette, state);
63
- refreshDirty(width, palette, state);
59
+ const seed = [...layouts].reverse().find((layout) => compatiblePrefix(layout, blocks));
60
+ const created = createLayout(blocks, width, palette, seed);
61
+ layouts.push(created);
62
+ if (layouts.length > MAX_LAYOUTS)
63
+ layouts.shift();
64
+ return created;
64
65
  }
65
- function rebuild(blocks, width, palette, state) {
66
- source = blocks;
67
- layoutWidth = width;
68
- layoutPalette = palette;
69
- entries = [];
70
- ends = [];
71
- positions = new WeakMap();
72
- dirty.clear();
73
- append(blocks, width, palette, state);
66
+ function createLayout(blocks, width, palette, seed) {
67
+ const layout = {
68
+ source: blocks,
69
+ width,
70
+ palette,
71
+ entries: [],
72
+ ends: [],
73
+ positions: new WeakMap(),
74
+ pending: 0,
75
+ next: -1,
76
+ };
77
+ append(layout, blocks, seed);
78
+ return layout;
74
79
  }
75
- function append(blocks, width, palette, state) {
76
- let total = ends.at(-1) ?? 0;
77
- for (let index = entries.length; index < blocks.length; index++) {
80
+ function append(layout, blocks, seed) {
81
+ const firstAdded = layout.entries.length;
82
+ let total = layout.ends.at(-1) ?? 0;
83
+ for (let index = firstAdded; index < blocks.length; index++) {
78
84
  const block = blocks[index];
79
85
  if (block === undefined)
80
86
  continue;
81
- const rows = draw(block, width, palette, { ...state, previous: blocks[index - 1] });
82
- entries.push({ block, rows });
83
- positions.set(block, index);
84
- dirty.delete(block);
85
- total += rows.length;
86
- ends.push(total);
87
+ const seeded = seed?.entries[index];
88
+ const rowCount = seeded?.block === block ? seeded.rowCount : estimatedRows(block);
89
+ layout.entries.push({ block, rowCount });
90
+ layout.positions.set(block, index);
91
+ layout.pending++;
92
+ total += rowCount;
93
+ layout.ends.push(total);
94
+ }
95
+ if (layout.entries.length > firstAdded) {
96
+ layout.next = Math.max(layout.next, layout.entries.length - 1);
87
97
  }
88
98
  }
89
- function refreshDirty(width, palette, state) {
90
- if (dirty.size === 0)
91
- return;
92
- const changed = [...dirty]
93
- .map((block) => positions.get(block))
94
- .filter((index) => index !== undefined)
95
- .sort((left, right) => left - right);
96
- dirty.clear();
97
- if (changed.length === 0)
98
- return;
99
- const first = changed[0];
100
- for (const index of changed) {
101
- const entry = entries[index];
102
- if (entry !== undefined) {
103
- entry.rows = draw(entry.block, width, palette, {
104
- ...state,
105
- previous: entries[index - 1]?.block,
106
- });
99
+ function reflowBatch(layout, state) {
100
+ let remaining = REFLOW_BLOCKS_PER_FRAME;
101
+ let firstChanged = layout.entries.length;
102
+ let index = layout.next;
103
+ while (index >= 0 && remaining > 0 && layout.pending > 0) {
104
+ if (renderEntry(layout, index, state)) {
105
+ firstChanged = Math.min(firstChanged, index);
106
+ remaining--;
107
107
  }
108
+ index--;
108
109
  }
109
- recomputeEnds(first);
110
+ layout.next = index;
111
+ if (firstChanged < layout.entries.length)
112
+ recomputeEnds(layout, firstChanged);
110
113
  }
111
- function recomputeEnds(from) {
112
- let total = from === 0 ? 0 : (ends[from - 1] ?? 0);
113
- for (let index = from; index < entries.length; index++) {
114
- total += entries[index]?.rows.length ?? 0;
115
- ends[index] = total;
114
+ function reflowWindow(layout, height, scroll, state) {
115
+ while (true) {
116
+ const window = visibleWindow(layout, height, scroll);
117
+ let firstChanged = layout.entries.length;
118
+ for (let index = window.first; index <= window.last; index++) {
119
+ if (renderEntry(layout, index, state))
120
+ firstChanged = Math.min(firstChanged, index);
121
+ }
122
+ if (firstChanged === layout.entries.length)
123
+ return window;
124
+ recomputeEnds(layout, firstChanged);
116
125
  }
117
126
  }
118
- function edgeChanged(blocks) {
119
- if (entries.length === 0)
127
+ function renderEntry(layout, index, state) {
128
+ const entry = layout.entries[index];
129
+ if (entry === undefined || entry.rows !== undefined)
120
130
  return false;
121
- const lastShared = Math.min(blocks.length, entries.length) - 1;
122
- return lastShared < 0 ||
123
- entries[0]?.block !== blocks[0] ||
124
- entries[lastShared]?.block !== blocks[lastShared];
131
+ entry.rows = draw(entry.block, layout.width, layout.palette, {
132
+ ...state,
133
+ previous: layout.entries[index - 1]?.block,
134
+ });
135
+ entry.rowCount = entry.rows.length;
136
+ layout.pending--;
137
+ return true;
138
+ }
139
+ function recomputeEnds(layout, from) {
140
+ let total = from === 0 ? 0 : (layout.ends[from - 1] ?? 0);
141
+ for (let index = from; index < layout.entries.length; index++) {
142
+ total += layout.entries[index]?.rowCount ?? 0;
143
+ layout.ends[index] = total;
144
+ }
125
145
  }
126
- function firstEndingAfter(row) {
127
- let low = 0;
128
- let high = ends.length;
129
- while (low < high) {
130
- const middle = Math.floor((low + high) / 2);
131
- if ((ends[middle] ?? 0) <= row)
132
- low = middle + 1;
133
- else
134
- high = middle;
146
+ function visibleWindow(layout, height, scroll) {
147
+ const totalRows = layout.ends.at(-1) ?? 0;
148
+ const maxScroll = Math.max(0, totalRows - height);
149
+ const offset = Math.min(Math.max(0, scroll), maxScroll);
150
+ const start = Math.max(0, totalRows - height - offset);
151
+ const end = start + height;
152
+ const first = firstEndingAfter(layout.ends, start);
153
+ let last = first - 1;
154
+ let at = first === 0 ? 0 : (layout.ends[first - 1] ?? 0);
155
+ while (last + 1 < layout.entries.length && at < end) {
156
+ last++;
157
+ at = layout.ends[last] ?? at;
135
158
  }
136
- return low;
159
+ return { first, last, start, end, maxScroll };
160
+ }
161
+ function visibleRows(layout, window, height) {
162
+ const rows = [];
163
+ let index = window.first;
164
+ let at = index === 0 ? 0 : (layout.ends[index - 1] ?? 0);
165
+ while (index <= window.last && at < window.end) {
166
+ const blockRows = layout.entries[index]?.rows ?? [];
167
+ rows.push(...blockRows.slice(Math.max(0, window.start - at), Math.min(blockRows.length, window.end - at)));
168
+ at = layout.ends[index] ?? at;
169
+ index++;
170
+ }
171
+ while (rows.length < height)
172
+ rows.unshift("");
173
+ return rows;
174
+ }
175
+ }
176
+ function compatible(layout, blocks) {
177
+ return layout.source === blocks &&
178
+ blocks.length >= layout.entries.length &&
179
+ !edgeChanged(layout, blocks);
180
+ }
181
+ function compatiblePrefix(layout, blocks) {
182
+ if (layout.entries.length === 0 || blocks.length === 0)
183
+ return true;
184
+ const shared = Math.min(layout.entries.length, blocks.length);
185
+ return layout.entries[0]?.block === blocks[0] &&
186
+ layout.entries[shared - 1]?.block === blocks[shared - 1];
187
+ }
188
+ function edgeChanged(layout, blocks) {
189
+ if (layout.entries.length === 0)
190
+ return false;
191
+ const lastShared = Math.min(blocks.length, layout.entries.length) - 1;
192
+ return lastShared < 0 ||
193
+ layout.entries[0]?.block !== blocks[0] ||
194
+ layout.entries[lastShared]?.block !== blocks[lastShared];
195
+ }
196
+ function estimatedRows(block) {
197
+ return block.kind === "user" ? 4 : 2;
198
+ }
199
+ function firstEndingAfter(ends, row) {
200
+ let low = 0;
201
+ let high = ends.length;
202
+ while (low < high) {
203
+ const middle = Math.floor((low + high) / 2);
204
+ if ((ends[middle] ?? 0) <= row)
205
+ low = middle + 1;
206
+ else
207
+ high = middle;
137
208
  }
209
+ return low;
138
210
  }
package/dist/tui/view.js CHANGED
@@ -25,11 +25,16 @@ export function compose(view, size, transcript = transcriptRenderer()) {
25
25
  // Any spare height belongs above the conversation, so short sessions grow
26
26
  // upward from the fixed dock rhythm instead of leaving a changing hole
27
27
  // beneath the latest reply.
28
- const viewport = transcript.viewport(view.blocks, width, transcriptHeight, view.scroll, view.pal, { spin: view.spin, reducedMotion: view.reducedMotion, now: view.now });
28
+ const viewport = transcript.viewport(view.blocks, width, transcriptHeight, view.scroll, view.pal, { now: view.now });
29
29
  const cursor = dock.cursor === undefined
30
30
  ? undefined
31
31
  : { row: transcriptHeight + dock.cursor.row, col: dock.cursor.col };
32
- return { rows: [...viewport.rows, ...dock.rows], cursor, maxScroll: viewport.maxScroll };
32
+ return {
33
+ rows: [...viewport.rows, ...dock.rows],
34
+ cursor,
35
+ maxScroll: viewport.maxScroll,
36
+ transcriptPending: viewport.pending,
37
+ };
33
38
  }
34
39
  function dockRows(view, width, height) {
35
40
  const status = renderStatus({
@@ -82,5 +87,5 @@ function tooSmall(height, width, view) {
82
87
  { text: elide(`need ${MIN_COLS}×${MIN_ROWS}`, Math.max(1, width)), fg: view.pal.ink.dim },
83
88
  ]);
84
89
  }
85
- return { rows, maxScroll: 0 };
90
+ return { rows, maxScroll: 0, transcriptPending: false };
86
91
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@giovannijecha/jecode",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "An owned coding agent with zero external runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -45,6 +45,7 @@
45
45
  "start": "node src/main.ts",
46
46
  "tui:lab": "node dev/tui-lab.ts",
47
47
  "bench:transcript": "node dev/benchmark-transcript.ts",
48
+ "bench:search": "node dev/benchmark-search.ts",
48
49
  "typecheck": "tsc --noEmit",
49
50
  "test": "npm run build:release && node --test",
50
51
  "coverage": "npm run build:release && node --test --experimental-test-coverage --test-coverage-include=\"src/**/*.ts\" --test-coverage-lines=80 --test-coverage-branches=75 --test-coverage-functions=75",