Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/client/src/effect/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -809,6 +809,7 @@ export type Endpoint21_4Input = {
readonly location?: Endpoint21_4Request["query"]["location"]
readonly cursor?: Endpoint21_4Request["query"]["cursor"]
readonly limit?: Endpoint21_4Request["query"]["limit"]
readonly keep?: Endpoint21_4Request["query"]["keep"]
}
export type Endpoint21_4Output = EffectValue<ReturnType<RawClient["server.shell"]["shell.output"]>>
export type ShellOutputOperation<E = never> = (input: Endpoint21_4Input) => Effect.Effect<Endpoint21_4Output, E>
Expand Down
3 changes: 2 additions & 1 deletion packages/client/src/effect/generated/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -971,11 +971,12 @@ type Endpoint21_4Input = {
readonly location?: Endpoint21_4Request["query"]["location"]
readonly cursor?: Endpoint21_4Request["query"]["cursor"]
readonly limit?: Endpoint21_4Request["query"]["limit"]
readonly keep?: Endpoint21_4Request["query"]["keep"]
}
const Endpoint21_4 = (raw: RawClient["server.shell"]) => (input: Endpoint21_4Input) =>
raw["shell.output"]({
params: { id: input["id"] },
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"], keep: input["keep"] },
}).pipe(Effect.mapError(mapClientError))

type Endpoint21_5Request = Parameters<RawClient["server.shell"]["shell.remove"]>[0]
Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/promise/generated/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1411,7 +1411,7 @@ export function make(options: ClientOptions) {
{
method: "GET",
path: `/api/shell/${encodeURIComponent(input.id)}/output`,
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"] },
query: { location: input["location"], cursor: input["cursor"], limit: input["limit"], keep: input["keep"] },
successStatus: 200,
declaredStatuses: [404, 401, 400],
empty: false,
Expand Down
9 changes: 9 additions & 0 deletions packages/client/src/promise/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4623,17 +4623,26 @@ export type ShellOutputInput = {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly cursor?: number | undefined
readonly limit?: number | undefined
readonly keep?: "head" | "tail" | undefined
}["location"]
readonly cursor?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly cursor?: number | undefined
readonly limit?: number | undefined
readonly keep?: "head" | "tail" | undefined
}["cursor"]
readonly limit?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly cursor?: number | undefined
readonly limit?: number | undefined
readonly keep?: "head" | "tail" | undefined
}["limit"]
readonly keep?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
readonly cursor?: number | undefined
readonly limit?: number | undefined
readonly keep?: "head" | "tail" | undefined
}["keep"]
}

export type ShellOutputOutput = {
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,9 @@ export const layer = Layer.effect(
const cursor = input?.cursor ?? 0
const limit = input?.limit ?? 65536
if (cursor >= session.size) return { output: "", cursor: session.size, size: session.size, truncated: false }
const start = Math.max(0, cursor)
const length = Math.min(limit, session.size - start)
const available = session.size - Math.max(0, cursor)
const start = input?.keep === "tail" ? Math.max(cursor, session.size - limit) : Math.max(0, cursor)
const length = Math.min(limit, available)
const buffer = Buffer.alloc(length)
const bytesRead = yield* Effect.promise(
() =>
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/tool/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ export const Plugin = {

const settleShell = Effect.fn("ShellTool.settleShell")(function* () {
const final = yield* shell.wait(info.id)
const page = yield* shell.output(info.id, { limit: MAX_CAPTURE_BYTES })
const page = yield* shell.output(info.id, { limit: MAX_CAPTURE_BYTES, keep: "tail" })

if (final.status === "timeout") {
return {
Expand All @@ -213,7 +213,7 @@ export const Plugin = {
}
}

const truncated = page.size > page.cursor
const truncated = page.size > MAX_CAPTURE_BYTES
const body = page.output || "(no output)"
const notice = truncated ? `\n\n[output truncated; full output saved to: ${final.file}]` : ""
return {
Expand Down
13 changes: 8 additions & 5 deletions packages/core/test/tool-shell.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,10 @@ const idleCommand = isWindows ? "Start-Sleep -Seconds 60" : "sleep 60"
const bodyExitCommand = isWindows
? "[Console]::Out.Write('body'); Start-Sleep -Milliseconds 100; exit 7"
: "printf body && exit 7"
const overflowCommand = (bytes: number) =>
const overflowMarkersCommand = (bytes: number) =>
isWindows
? `[Console]::Out.Write(('x' * ${bytes})); Start-Sleep -Milliseconds 100`
: `head -c ${bytes} /dev/zero | tr '\\0' 'x'`
? `[Console]::Out.Write('head-marker'); [Console]::Out.Write(('x' * ${bytes})); [Console]::Out.Write('tail-marker'); Start-Sleep -Milliseconds 100`
: `printf head-marker; head -c ${bytes} /dev/zero | tr '\\0' 'x'; printf tail-marker`

const withSession = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) =>
Effect.gen(function* () {
Expand Down Expand Up @@ -396,15 +396,18 @@ describe("ShellTool", () => {
reset()
const bytes = ShellTool.MAX_CAPTURE_BYTES + 1024
return withSession(tmp.path, (registry) =>
settleTool(registry, call({ command: overflowCommand(bytes) }, "call-overflow")),
settleTool(registry, call({ command: overflowMarkersCommand(bytes) }, "call-overflow")),
).pipe(
Effect.andThen((settled) =>
Effect.sync(() => {
expect(settled.output?.structured).toMatchObject({ exit: 0, truncated: true })
expect(settled.output?.content[0]).toMatchObject({
type: "text",
text: expect.stringContaining("output truncated; full output saved to:"),
text: expect.stringMatching(/tail-marker[\s\S]*output truncated; full output saved to:/),
})
const content = settled.output?.content[0]
if (content?.type === "text" && typeof content.text === "string")
expect(content.text.includes("head-marker")).toBe(false)
}),
),
)
Expand Down
1 change: 1 addition & 0 deletions packages/schema/src/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export interface CreateInput extends Schema.Schema.Type<typeof CreateInput> {}
export const OutputInput = Schema.Struct({
cursor: optional(NonNegativeInt),
limit: optional(NonNegativeInt),
keep: optional(Schema.Literals(["head", "tail"])),
})
export interface OutputInput extends Schema.Schema.Type<typeof OutputInput> {}

Expand Down
6 changes: 5 additions & 1 deletion packages/server/src/handlers/shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@ export const ShellHandler = HttpApiBuilder.group(Api, "server.shell", (handlers)
Effect.fn(function* (ctx) {
const shell = yield* Shell.Service
return yield* response(
shell.output(ctx.params.id, { cursor: ctx.query.cursor, limit: ctx.query.limit }).pipe(
shell.output(ctx.params.id, {
cursor: ctx.query.cursor,
limit: ctx.query.limit,
keep: ctx.query.keep,
}).pipe(
Effect.catchTag(
"Shell.NotFoundError",
() => new ShellNotFoundError({ id: ctx.params.id, message: `Shell command not found: ${ctx.params.id}` }),
Expand Down
Loading