effect 4.0.0-beta.35 → 4.0.0-beta.36

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.
Files changed (51) hide show
  1. package/dist/Channel.d.ts.map +1 -1
  2. package/dist/Channel.js +9 -7
  3. package/dist/Channel.js.map +1 -1
  4. package/dist/Cron.d.ts +7 -0
  5. package/dist/Cron.d.ts.map +1 -1
  6. package/dist/Cron.js +109 -45
  7. package/dist/Cron.js.map +1 -1
  8. package/dist/Equivalence.d.ts +52 -0
  9. package/dist/Equivalence.d.ts.map +1 -1
  10. package/dist/Equivalence.js +52 -0
  11. package/dist/Equivalence.js.map +1 -1
  12. package/dist/LayerMap.d.ts +5 -4
  13. package/dist/LayerMap.d.ts.map +1 -1
  14. package/dist/LayerMap.js.map +1 -1
  15. package/dist/PubSub.d.ts.map +1 -1
  16. package/dist/PubSub.js +9 -3
  17. package/dist/PubSub.js.map +1 -1
  18. package/dist/Schema.d.ts +29 -5
  19. package/dist/Schema.d.ts.map +1 -1
  20. package/dist/Schema.js +20 -0
  21. package/dist/Schema.js.map +1 -1
  22. package/dist/SchemaParser.d.ts +29 -0
  23. package/dist/SchemaParser.d.ts.map +1 -1
  24. package/dist/SchemaParser.js +38 -0
  25. package/dist/SchemaParser.js.map +1 -1
  26. package/dist/internal/effect.js +3 -2
  27. package/dist/internal/effect.js.map +1 -1
  28. package/dist/unstable/ai/LanguageModel.d.ts.map +1 -1
  29. package/dist/unstable/ai/LanguageModel.js +15 -3
  30. package/dist/unstable/ai/LanguageModel.js.map +1 -1
  31. package/dist/unstable/cli/Prompt.js +146 -13
  32. package/dist/unstable/cli/Prompt.js.map +1 -1
  33. package/dist/unstable/httpapi/HttpApiEndpoint.js +2 -2
  34. package/dist/unstable/httpapi/HttpApiEndpoint.js.map +1 -1
  35. package/dist/unstable/httpapi/HttpApiMiddleware.d.ts +15 -13
  36. package/dist/unstable/httpapi/HttpApiMiddleware.d.ts.map +1 -1
  37. package/dist/unstable/httpapi/HttpApiMiddleware.js +5 -3
  38. package/dist/unstable/httpapi/HttpApiMiddleware.js.map +1 -1
  39. package/package.json +1 -1
  40. package/src/Channel.ts +12 -10
  41. package/src/Cron.ts +142 -45
  42. package/src/Equivalence.ts +56 -0
  43. package/src/LayerMap.ts +7 -5
  44. package/src/PubSub.ts +13 -5
  45. package/src/Schema.ts +32 -8
  46. package/src/SchemaParser.ts +53 -0
  47. package/src/internal/effect.ts +3 -2
  48. package/src/unstable/ai/LanguageModel.ts +17 -2
  49. package/src/unstable/cli/Prompt.ts +147 -14
  50. package/src/unstable/httpapi/HttpApiEndpoint.ts +2 -2
  51. package/src/unstable/httpapi/HttpApiMiddleware.ts +36 -20
package/src/PubSub.ts CHANGED
@@ -41,6 +41,7 @@ import { nextPow2 } from "./Number.ts"
41
41
  import * as Option from "./Option.ts"
42
42
  import { type Pipeable, pipeArguments } from "./Pipeable.ts"
43
43
  import * as Scope from "./Scope.ts"
44
+ import * as ServiceMap from "./ServiceMap.ts"
44
45
  import type { Covariant, Invariant } from "./Types.ts"
45
46
 
46
47
  const TypeId = "~effect/PubSub"
@@ -881,7 +882,7 @@ export const publish: {
881
882
  } = dual(2, <A>(self: PubSub<A>, value: A): Effect.Effect<boolean> =>
882
883
  Effect.suspend(() => {
883
884
  if (self.shutdownFlag.current) {
884
- return Effect.interrupt
885
+ return Effect.succeed(false)
885
886
  }
886
887
 
887
888
  if (self.pubsub.publish(value)) {
@@ -1110,7 +1111,7 @@ export const publishAll: {
1110
1111
  } = dual(2, <A>(self: PubSub<A>, elements: Iterable<A>): Effect.Effect<boolean> =>
1111
1112
  Effect.suspend(() => {
1112
1113
  if (self.shutdownFlag.current) {
1113
- return Effect.interrupt
1114
+ return Effect.succeed(false)
1114
1115
  }
1115
1116
  const surplus = self.pubsub.publishAll(elements)
1116
1117
  self.strategy.completeSubscribersUnsafe(self.pubsub, self.subscribers)
@@ -1173,9 +1174,16 @@ export const publishAll: {
1173
1174
  * @category subscription
1174
1175
  */
1175
1176
  export const subscribe = <A>(self: PubSub<A>): Effect.Effect<Subscription<A>, never, Scope.Scope> =>
1176
- Effect.acquireRelease(
1177
- Effect.sync(() => makeSubscriptionUnsafe(self.pubsub, self.subscribers, self.strategy)),
1178
- unsubscribe
1177
+ Effect.uninterruptible(
1178
+ Effect.servicesWith((services) => {
1179
+ const localScope = ServiceMap.get(services, Scope.Scope)
1180
+ const scope = Scope.forkUnsafe(self.scope)
1181
+ const subscription = makeSubscriptionUnsafe(self.pubsub, self.subscribers, self.strategy)
1182
+ return Scope.addFinalizer(scope, unsubscribe(subscription)).pipe(
1183
+ Effect.andThen(Scope.addFinalizerExit(localScope, (exit) => Scope.close(scope, exit))),
1184
+ Effect.as(subscription)
1185
+ )
1186
+ })
1179
1187
  )
1180
1188
 
1181
1189
  const unsubscribe = <A>(self: Subscription<A>): Effect.Effect<void> =>
package/src/Schema.ts CHANGED
@@ -1209,6 +1209,18 @@ export const decodeOption = Parser.decodeOption
1209
1209
  * @category Decoding
1210
1210
  * @since 4.0.0
1211
1211
  */
1212
+ export const decodeUnknownResult = Parser.decodeUnknownResult
1213
+
1214
+ /**
1215
+ * @category Decoding
1216
+ * @since 4.0.0
1217
+ */
1218
+ export const decodeResult = Parser.decodeResult
1219
+
1220
+ /**
1221
+ * @category Decoding
1222
+ * @since 4.0.0
1223
+ */
1212
1224
  export const decodeUnknownPromise = Parser.decodeUnknownPromise
1213
1225
 
1214
1226
  /**
@@ -1367,6 +1379,18 @@ export const encodeOption = Parser.encodeOption
1367
1379
  * @category Encoding
1368
1380
  * @since 4.0.0
1369
1381
  */
1382
+ export const encodeUnknownResult = Parser.encodeUnknownResult
1383
+
1384
+ /**
1385
+ * @category Encoding
1386
+ * @since 4.0.0
1387
+ */
1388
+ export const encodeResult = Parser.encodeResult
1389
+
1390
+ /**
1391
+ * @category Encoding
1392
+ * @since 4.0.0
1393
+ */
1370
1394
  export const encodeUnknownPromise = Parser.encodeUnknownPromise
1371
1395
 
1372
1396
  /**
@@ -2276,7 +2300,7 @@ export declare namespace Struct {
2276
2300
  /**
2277
2301
  * @since 4.0.0
2278
2302
  */
2279
- export type Type<F extends Fields> = Type_<F>
2303
+ export type Type<F extends Fields> = Simplify<Type_<F>>
2280
2304
 
2281
2305
  type Iso_<
2282
2306
  F extends Fields,
@@ -2291,7 +2315,7 @@ export declare namespace Struct {
2291
2315
  /**
2292
2316
  * @since 4.0.0
2293
2317
  */
2294
- export type Iso<F extends Fields> = Iso_<F>
2318
+ export type Iso<F extends Fields> = Simplify<Iso_<F>>
2295
2319
 
2296
2320
  type EncodedOptionalKeys<Fields extends Struct.Fields> = {
2297
2321
  [K in keyof Fields]: Fields[K] extends { readonly "~encoded.optionality": "optional" } ? K
@@ -2316,7 +2340,7 @@ export declare namespace Struct {
2316
2340
  /**
2317
2341
  * @since 4.0.0
2318
2342
  */
2319
- export type Encoded<F extends Fields> = Encoded_<F>
2343
+ export type Encoded<F extends Fields> = Simplify<Encoded_<F>>
2320
2344
 
2321
2345
  /**
2322
2346
  * @since 4.0.0
@@ -2343,7 +2367,7 @@ export declare namespace Struct {
2343
2367
  /**
2344
2368
  * @since 4.0.0
2345
2369
  */
2346
- export type MakeIn<F extends Fields> = MakeIn_<F>
2370
+ export type MakeIn<F extends Fields> = Simplify<MakeIn_<F>>
2347
2371
  }
2348
2372
 
2349
2373
  /**
@@ -2351,14 +2375,14 @@ export declare namespace Struct {
2351
2375
  */
2352
2376
  export interface Struct<Fields extends Struct.Fields> extends
2353
2377
  Bottom<
2354
- Simplify<Struct.Type<Fields>>,
2355
- Simplify<Struct.Encoded<Fields>>,
2378
+ Struct.Type<Fields>,
2379
+ Struct.Encoded<Fields>,
2356
2380
  Struct.DecodingServices<Fields>,
2357
2381
  Struct.EncodingServices<Fields>,
2358
2382
  AST.Objects,
2359
2383
  Struct<Fields>,
2360
- Simplify<Struct.MakeIn<Fields>>,
2361
- Simplify<Struct.Iso<Fields>>
2384
+ Struct.MakeIn<Fields>,
2385
+ Struct.Iso<Fields>
2362
2386
  >
2363
2387
  {
2364
2388
  readonly "~rebuild.out": this
@@ -204,6 +204,24 @@ export const decodeOption: <S extends Schema.Top & { readonly DecodingServices:
204
204
  schema: S
205
205
  ) => (input: S["Encoded"], options?: AST.ParseOptions) => Option.Option<S["Type"]> = decodeUnknownOption
206
206
 
207
+ /**
208
+ * @category Decoding
209
+ * @since 4.0.0
210
+ */
211
+ export function decodeUnknownResult<S extends Schema.Top & { readonly DecodingServices: never }>(
212
+ schema: S
213
+ ): (input: unknown, options?: AST.ParseOptions) => Result.Result<S["Type"], Issue.Issue> {
214
+ return asResult(decodeUnknownEffect(schema))
215
+ }
216
+
217
+ /**
218
+ * @category Decoding
219
+ * @since 4.0.0
220
+ */
221
+ export const decodeResult: <S extends Schema.Top & { readonly DecodingServices: never }>(
222
+ schema: S
223
+ ) => (input: S["Encoded"], options?: AST.ParseOptions) => Result.Result<S["Type"], Issue.Issue> = decodeUnknownResult
224
+
207
225
  /**
208
226
  * @category Decoding
209
227
  * @since 4.0.0
@@ -293,6 +311,24 @@ export const encodeOption: <S extends Schema.Top & { readonly EncodingServices:
293
311
  schema: S
294
312
  ) => (input: S["Type"], options?: AST.ParseOptions) => Option.Option<S["Encoded"]> = encodeUnknownOption
295
313
 
314
+ /**
315
+ * @category Encoding
316
+ * @since 4.0.0
317
+ */
318
+ export function encodeUnknownResult<S extends Schema.Top & { readonly EncodingServices: never }>(
319
+ schema: S
320
+ ): (input: unknown, options?: AST.ParseOptions) => Result.Result<S["Encoded"], Issue.Issue> {
321
+ return asResult(encodeUnknownEffect(schema))
322
+ }
323
+
324
+ /**
325
+ * @category Encoding
326
+ * @since 4.0.0
327
+ */
328
+ export const encodeResult: <S extends Schema.Top & { readonly EncodingServices: never }>(
329
+ schema: S
330
+ ) => (input: S["Type"], options?: AST.ParseOptions) => Result.Result<S["Encoded"], Issue.Issue> = encodeUnknownResult
331
+
296
332
  /**
297
333
  * @category Encoding
298
334
  * @since 4.0.0
@@ -343,6 +379,23 @@ export function asOption<T, E, R>(
343
379
  return (input: E, options?: AST.ParseOptions) => Exit.getSuccess(parserExit(input, options))
344
380
  }
345
381
 
382
+ function asResult<T, E, R>(
383
+ parser: (input: E, options?: AST.ParseOptions) => Effect.Effect<T, Issue.Issue, R>
384
+ ): (input: E, options?: AST.ParseOptions) => Result.Result<T, Issue.Issue> {
385
+ const parserExit = asExit(parser)
386
+ return (input: E, options?: AST.ParseOptions) => {
387
+ const exit = parserExit(input, options)
388
+ if (Exit.isSuccess(exit)) {
389
+ return Result.succeed(exit.value)
390
+ }
391
+ const error = Cause.findError(exit.cause)
392
+ if (Result.isFailure(error)) {
393
+ throw Cause.squash(error.failure)
394
+ }
395
+ return Result.fail(error.success)
396
+ }
397
+ }
398
+
346
399
  function asSync<T, E, R>(
347
400
  parser: (input: E, options?: AST.ParseOptions) => Effect.Effect<T, Issue.Issue, R>
348
401
  ): (input: E, options?: AST.ParseOptions) => T {
@@ -4063,7 +4063,8 @@ export const cachedInvalidateWithTTL: {
4063
4063
  const wait = flatMap(latch.await, () => exit!)
4064
4064
  return [
4065
4065
  withFiber((fiber) => {
4066
- const now = isFinite ? fiber.getRef(ClockRef).currentTimeMillisUnsafe() : 0
4066
+ const clock = fiber.getRef(ClockRef)
4067
+ const now = isFinite ? clock.currentTimeMillisUnsafe() : 0
4067
4068
  if (running || now < expiresAt) return exit ?? wait
4068
4069
  running = true
4069
4070
  latch.closeUnsafe()
@@ -4071,7 +4072,7 @@ export const cachedInvalidateWithTTL: {
4071
4072
  return onExit(self, (exit_) =>
4072
4073
  sync(() => {
4073
4074
  running = false
4074
- expiresAt = now + ttlMillis
4075
+ expiresAt = clock.currentTimeMillisUnsafe() + ttlMillis
4075
4076
  exit = exit_
4076
4077
  latch.openUnsafe()
4077
4078
  }))
@@ -1407,6 +1407,7 @@ export const make: (params: {
1407
1407
  | Cause.Done
1408
1408
  | Schema.SchemaError
1409
1409
  >()
1410
+ const deferredFinishParts: Array<Response.StreamPart<Tools>> = []
1410
1411
 
1411
1412
  // Emit pre-resolved tool results so Chat.streamText persists them to
1412
1413
  // history. This ensures collectToolApprovals({ excludeResolved }) can
@@ -1465,8 +1466,19 @@ export const make: (params: {
1465
1466
  }
1466
1467
  }
1467
1468
  }
1468
- // Add decoded response parts to the output queue
1469
- yield* Queue.offerAll(queue, parts)
1469
+ // Defer finish parts until all tool handlers complete. This guarantees
1470
+ // tool results are emitted before finish in streaming mode.
1471
+ const immediateParts: Array<Response.StreamPart<Tools>> = []
1472
+ for (const part of parts) {
1473
+ if (part.type === "finish") {
1474
+ deferredFinishParts.push(part)
1475
+ } else {
1476
+ immediateParts.push(part)
1477
+ }
1478
+ }
1479
+ if (immediateParts.length > 0) {
1480
+ yield* Queue.offerAll(queue, immediateParts)
1481
+ }
1470
1482
  // Fork tool call handlers - use the raw chunk for encoded params
1471
1483
  for (const part of chunk) {
1472
1484
  if (part.type === "tool-call" && part.providerExecuted !== true) {
@@ -1484,6 +1496,9 @@ export const make: (params: {
1484
1496
  FiberSet.awaitEmpty(toolCallFibers)
1485
1497
  )
1486
1498
  ),
1499
+ Effect.andThen(
1500
+ Queue.offerAll(queue, deferredFinishParts)
1501
+ ),
1487
1502
  // And then end the queue
1488
1503
  Effect.andThen(Queue.end(queue)),
1489
1504
  Effect.tapCause((cause) => Queue.failCause(queue, cause)),
@@ -739,7 +739,7 @@ export const file = (options: FileOptions = {}): Prompt<string> => {
739
739
  const currentPath = yield* resolveCurrentPath(Option.none(), opts)
740
740
  const files = yield* getFileList(currentPath, opts)
741
741
  const confirm = Confirm.Hide()
742
- return { cursor: 0, files, path: Option.none(), confirm }
742
+ return { cursor: 0, files, allFiles: files, query: "", path: Option.none(), confirm }
743
743
  })
744
744
  return custom(initialState, {
745
745
  render: handleFileRender(opts),
@@ -1855,11 +1855,16 @@ interface FileOptionsReq extends Required<Omit<FileOptions, "startingPath">> {
1855
1855
  interface FileState {
1856
1856
  readonly cursor: number
1857
1857
  readonly files: ReadonlyArray<string>
1858
+ readonly allFiles: ReadonlyArray<string>
1859
+ readonly query: string
1858
1860
  readonly path: Option.Option<string>
1859
1861
  readonly confirm: Confirm
1860
1862
  }
1861
1863
 
1862
1864
  const CONFIRM_MESSAGE = "The selected directory contains files. Would you like to traverse the selected directory?"
1865
+ const FILE_FILTER_LABEL = "filter"
1866
+ const FILE_FILTER_PLACEHOLDER = "type to filter"
1867
+ const FILE_EMPTY_MESSAGE = "No matches"
1863
1868
  type Confirm = Data.TaggedEnum<{
1864
1869
  readonly Show: {}
1865
1870
  readonly Hide: {}
@@ -1914,6 +1919,40 @@ const getFileList = Effect.fnUntraced(function*(directory: string, options: File
1914
1919
  }, { concurrency: files.length })
1915
1920
  })
1916
1921
 
1922
+ const filterFiles = (files: ReadonlyArray<string>, query: string) => {
1923
+ if (query.length === 0) {
1924
+ return files
1925
+ }
1926
+ const normalizedQuery = query.toLowerCase()
1927
+ const filtered: Array<string> = []
1928
+ for (let index = 0; index < files.length; index++) {
1929
+ if (files[index].toLowerCase().includes(normalizedQuery)) {
1930
+ filtered.push(files[index])
1931
+ }
1932
+ }
1933
+ return filtered
1934
+ }
1935
+
1936
+ const updateFileState = (
1937
+ state: FileState,
1938
+ query: string,
1939
+ allFiles: ReadonlyArray<string> = state.allFiles
1940
+ ): FileState => {
1941
+ const files = filterFiles(allFiles, query)
1942
+ if (files.length === 0) {
1943
+ return { ...state, query, allFiles, files, cursor: 0 }
1944
+ }
1945
+ const selected = state.files[state.cursor]
1946
+ const cursor = selected === undefined ? 0 : files.indexOf(selected)
1947
+ return {
1948
+ ...state,
1949
+ query,
1950
+ allFiles,
1951
+ files,
1952
+ cursor: cursor === -1 ? 0 : cursor
1953
+ }
1954
+ }
1955
+
1917
1956
  const handleFileClear = (options: FileOptionsReq) => {
1918
1957
  return Effect.fnUntraced(function*(state: FileState, _: Action<FileState, string>) {
1919
1958
  const terminal = yield* Terminal.Terminal
@@ -1922,12 +1961,14 @@ const handleFileClear = (options: FileOptionsReq) => {
1922
1961
  const figures = yield* platformFigures
1923
1962
  const currentPath = yield* resolveCurrentPath(state.path, options)
1924
1963
  const selectedPath = state.files[state.cursor]
1925
- const resolvedPath = path.resolve(currentPath, selectedPath)
1964
+ const resolvedPath = selectedPath === undefined ? currentPath : path.resolve(currentPath, selectedPath)
1926
1965
  const resolvedPathText = `${figures.pointerSmall} ${resolvedPath}`
1927
1966
  const isConfirming = showConfirmation(state.confirm)
1928
1967
  const promptText = isConfirming
1929
1968
  ? renderPrompt("(Y/n)", CONFIRM_MESSAGE, "?", figures.pointerSmall, { plain: true })
1930
- : renderPrompt("", options.message, figures.tick, figures.ellipsis, { plain: true })
1969
+ : renderPrompt(renderFileFilter(state, { plain: true }), options.message, figures.tick, figures.ellipsis, {
1970
+ plain: true
1971
+ })
1931
1972
  const filesText = isConfirming
1932
1973
  ? ""
1933
1974
  : renderFiles(state, state.files, figures, options, { plain: true })
@@ -1995,6 +2036,17 @@ const renderFileName = (file: string, isSelected: boolean, renderOptions?: Rende
1995
2036
  : file
1996
2037
  }
1997
2038
 
2039
+ const renderFileFilter = (state: FileState, renderOptions?: RenderOptions | undefined) => {
2040
+ const filterValue = state.query.length === 0
2041
+ ? renderOptions?.plain === true
2042
+ ? FILE_FILTER_PLACEHOLDER
2043
+ : Ansi.annotate(FILE_FILTER_PLACEHOLDER, Ansi.blackBright)
2044
+ : renderOptions?.plain === true
2045
+ ? state.query
2046
+ : Ansi.annotate(state.query, Ansi.combine(Ansi.underlined, Ansi.cyanBright))
2047
+ return `[${FILE_FILTER_LABEL}: ${filterValue}]`
2048
+ }
2049
+
1998
2050
  const renderFiles = (
1999
2051
  state: FileState,
2000
2052
  files: ReadonlyArray<string>,
@@ -2003,6 +2055,11 @@ const renderFiles = (
2003
2055
  renderOptions?: RenderOptions | undefined
2004
2056
  ) => {
2005
2057
  const length = files.length
2058
+ if (length === 0) {
2059
+ return renderOptions?.plain === true
2060
+ ? FILE_EMPTY_MESSAGE
2061
+ : Ansi.annotate(FILE_EMPTY_MESSAGE, Ansi.blackBright)
2062
+ }
2006
2063
  const toDisplay = entriesToDisplay(state.cursor, length, options.maxPerPage)
2007
2064
  const documents: Array<string> = []
2008
2065
  for (let index = toDisplay.startIndex; index < toDisplay.endIndex; index++) {
@@ -2019,7 +2076,7 @@ const renderFileNextFrame = Effect.fnUntraced(function*(state: FileState, option
2019
2076
  const figures = yield* platformFigures
2020
2077
  const currentPath = yield* resolveCurrentPath(state.path, options)
2021
2078
  const selectedPath = state.files[state.cursor]
2022
- const resolvedPath = path.resolve(currentPath, selectedPath)
2079
+ const resolvedPath = selectedPath === undefined ? currentPath : path.resolve(currentPath, selectedPath)
2023
2080
  const resolvedPathMsg = Ansi.annotate(figures.pointerSmall + " " + resolvedPath, Ansi.blackBright)
2024
2081
 
2025
2082
  if (showConfirmation(state.confirm)) {
@@ -2031,33 +2088,36 @@ const renderFileNextFrame = Effect.fnUntraced(function*(state: FileState, option
2031
2088
  }
2032
2089
  const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green)
2033
2090
  const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright)
2034
- const promptMsg = renderPrompt("", options.message, leadingSymbol, trailingSymbol)
2091
+ const promptMsg = renderPrompt(renderFileFilter(state), options.message, leadingSymbol, trailingSymbol)
2035
2092
  const files = renderFiles(state, state.files, figures, options)
2036
2093
  return Ansi.cursorHide + promptMsg + "\n" + resolvedPathMsg + "\n" + files
2037
2094
  })
2038
2095
 
2039
- const renderFileSubmission = Effect.fnUntraced(function*(value: string, options: FileOptionsReq) {
2096
+ const renderFileSubmission = Effect.fnUntraced(function*(state: FileState, value: string, options: FileOptionsReq) {
2040
2097
  const figures = yield* platformFigures
2041
2098
  const leadingSymbol = Ansi.annotate(figures.tick, Ansi.green)
2042
2099
  const trailingSymbol = Ansi.annotate(figures.ellipsis, Ansi.blackBright)
2043
- const promptMsg = renderPrompt("", options.message, leadingSymbol, trailingSymbol)
2100
+ const promptMsg = renderPrompt(renderFileFilter(state), options.message, leadingSymbol, trailingSymbol)
2044
2101
  return promptMsg + " " + Ansi.annotate(value, Ansi.white) + "\n"
2045
2102
  })
2046
2103
 
2047
2104
  const handleFileRender = (options: FileOptionsReq) => {
2048
2105
  return (
2049
- _: FileState,
2106
+ state: FileState,
2050
2107
  action: Action<FileState, string>
2051
2108
  ): Effect.Effect<string, never, Path.Path | FileSystem.FileSystem> => {
2052
2109
  return Action.$match(action, {
2053
2110
  Beep: () => Effect.succeed(renderBeep),
2054
2111
  NextFrame: ({ state }) => renderFileNextFrame(state, options),
2055
- Submit: ({ value }) => renderFileSubmission(value, options)
2112
+ Submit: ({ value }) => renderFileSubmission(state, value, options)
2056
2113
  })
2057
2114
  }
2058
2115
  }
2059
2116
 
2060
2117
  const processFileCursorUp = (state: FileState) => {
2118
+ if (state.files.length === 0) {
2119
+ return Effect.succeed(Action.Beep())
2120
+ }
2061
2121
  const cursor = state.cursor - 1
2062
2122
  return Effect.succeed(Action.NextFrame({
2063
2123
  state: { ...state, cursor: cursor < 0 ? state.files.length - 1 : cursor }
@@ -2065,12 +2125,36 @@ const processFileCursorUp = (state: FileState) => {
2065
2125
  }
2066
2126
 
2067
2127
  const processFileCursorDown = (state: FileState) => {
2128
+ if (state.files.length === 0) {
2129
+ return Effect.succeed(Action.Beep())
2130
+ }
2068
2131
  return Effect.succeed(Action.NextFrame({
2069
2132
  state: { ...state, cursor: (state.cursor + 1) % state.files.length }
2070
2133
  }))
2071
2134
  }
2072
2135
 
2136
+ const processFileBackspace = (state: FileState) => {
2137
+ if (state.query.length === 0) {
2138
+ return Effect.succeed(Action.Beep())
2139
+ }
2140
+ const query = state.query.slice(0, state.query.length - 1)
2141
+ return Effect.succeed(Action.NextFrame({ state: updateFileState(state, query) }))
2142
+ }
2143
+
2144
+ const processFileClear = (state: FileState) => Effect.succeed(Action.NextFrame({ state: updateFileState(state, "") }))
2145
+
2146
+ const processFileInput = (input: string, state: FileState) => {
2147
+ if (input.length === 0) {
2148
+ return Effect.succeed(Action.Beep())
2149
+ }
2150
+ const query = state.query + input
2151
+ return Effect.succeed(Action.NextFrame({ state: updateFileState(state, query) }))
2152
+ }
2153
+
2073
2154
  const processSelection = Effect.fnUntraced(function*(state: FileState, options: FileOptionsReq) {
2155
+ if (state.files.length === 0) {
2156
+ return Action.Beep()
2157
+ }
2074
2158
  const fs = yield* FileSystem.FileSystem
2075
2159
  const path = yield* Path.Path
2076
2160
  const currentPath = yield* resolveCurrentPath(state.path, options)
@@ -2097,6 +2181,8 @@ const processSelection = Effect.fnUntraced(function*(state: FileState, options:
2097
2181
  state: {
2098
2182
  cursor: 0,
2099
2183
  files,
2184
+ allFiles: files,
2185
+ query: "",
2100
2186
  path: Option.some(resolvedPath),
2101
2187
  confirm: Confirm.Hide()
2102
2188
  }
@@ -2107,6 +2193,12 @@ const processSelection = Effect.fnUntraced(function*(state: FileState, options:
2107
2193
 
2108
2194
  const handleFileProcess = (options: FileOptionsReq) => {
2109
2195
  return Effect.fnUntraced(function*(input: Terminal.UserInput, state: FileState) {
2196
+ if (input.key.ctrl && input.key.name === "u") {
2197
+ if (showConfirmation(state.confirm)) {
2198
+ return Action.Beep()
2199
+ }
2200
+ return yield* processFileClear(state)
2201
+ }
2110
2202
  switch (input.key.name) {
2111
2203
  case "k":
2112
2204
  case "up": {
@@ -2117,6 +2209,12 @@ const handleFileProcess = (options: FileOptionsReq) => {
2117
2209
  case "tab": {
2118
2210
  return yield* processFileCursorDown(state)
2119
2211
  }
2212
+ case "backspace": {
2213
+ if (showConfirmation(state.confirm)) {
2214
+ return Action.Beep()
2215
+ }
2216
+ return yield* processFileBackspace(state)
2217
+ }
2120
2218
  case "enter":
2121
2219
  case "return": {
2122
2220
  return yield* processSelection(state, options)
@@ -2133,12 +2231,14 @@ const handleFileProcess = (options: FileOptionsReq) => {
2133
2231
  state: {
2134
2232
  cursor: 0,
2135
2233
  files,
2234
+ allFiles: files,
2235
+ query: "",
2136
2236
  path: Option.some(resolvedPath),
2137
2237
  confirm: Confirm.Hide()
2138
2238
  }
2139
2239
  })
2140
2240
  }
2141
- return Action.Beep()
2241
+ return yield* processFileInput(Option.getOrElse(input.input, () => ""), state)
2142
2242
  }
2143
2243
  case "n":
2144
2244
  case "f": {
@@ -2149,10 +2249,13 @@ const handleFileProcess = (options: FileOptionsReq) => {
2149
2249
  const resolvedPath = path.resolve(currentPath, selectedPath)
2150
2250
  return Action.Submit({ value: resolvedPath })
2151
2251
  }
2152
- return Action.Beep()
2252
+ return yield* processFileInput(Option.getOrElse(input.input, () => ""), state)
2153
2253
  }
2154
2254
  default: {
2155
- return Action.Beep()
2255
+ if (showConfirmation(state.confirm)) {
2256
+ return Action.Beep()
2257
+ }
2258
+ return yield* processFileInput(Option.getOrElse(input.input, () => ""), state)
2156
2259
  }
2157
2260
  }
2158
2261
  })
@@ -3259,6 +3362,20 @@ const processTextCursorRight = (state: TextState) => {
3259
3362
  )
3260
3363
  }
3261
3364
 
3365
+ const processTextCursorStart = (state: TextState) =>
3366
+ Effect.succeed(
3367
+ Action.NextFrame({
3368
+ state: { ...state, cursor: 0, error: Option.none() }
3369
+ })
3370
+ )
3371
+
3372
+ const processTextCursorEnd = (state: TextState) =>
3373
+ Effect.succeed(
3374
+ Action.NextFrame({
3375
+ state: { ...state, cursor: state.value.length, error: Option.none() }
3376
+ })
3377
+ )
3378
+
3262
3379
  const processTab = (state: TextState, options: TextOptionsReq) => {
3263
3380
  if (state.value === options.default) {
3264
3381
  return Effect.succeed(Action.Beep())
@@ -3295,8 +3412,18 @@ const handleTextRender = (options: TextOptionsReq) => {
3295
3412
 
3296
3413
  const handleTextProcess = (options: TextOptionsReq) => {
3297
3414
  return (input: Terminal.UserInput, state: TextState) => {
3298
- if (input.key.ctrl && input.key.name === "u") {
3299
- return processTextClear(state)
3415
+ if (input.key.ctrl) {
3416
+ switch (input.key.name) {
3417
+ case "u": {
3418
+ return processTextClear(state)
3419
+ }
3420
+ case "a": {
3421
+ return processTextCursorStart(state)
3422
+ }
3423
+ case "e": {
3424
+ return processTextCursorEnd(state)
3425
+ }
3426
+ }
3300
3427
  }
3301
3428
  switch (input.key.name) {
3302
3429
  case "backspace": {
@@ -3308,6 +3435,12 @@ const handleTextProcess = (options: TextOptionsReq) => {
3308
3435
  case "right": {
3309
3436
  return processTextCursorRight(state)
3310
3437
  }
3438
+ case "home": {
3439
+ return processTextCursorStart(state)
3440
+ }
3441
+ case "end": {
3442
+ return processTextCursorEnd(state)
3443
+ }
3311
3444
  case "enter":
3312
3445
  case "return": {
3313
3446
  const value = state.value
@@ -175,8 +175,8 @@ export function getErrorSchemas(endpoint: AnyWithProps): [Schema.Top, ...Array<S
175
175
  const schemas = new Set<Schema.Top>(endpoint.error)
176
176
  for (const middleware of endpoint.middlewares) {
177
177
  const key = middleware as any as HttpApiMiddleware.AnyService
178
- if (key.error !== undefined) {
179
- schemas.add(key.error)
178
+ for (const schema of key.error) {
179
+ schemas.add(schema)
180
180
  }
181
181
  }
182
182
  return Arr.append(Array.from(schemas), BadRequestFromSchemaError)