diff --git a/README.md b/README.md index 73dd1b3..b08b422 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A server and macOS app for running many coding agents (Claude Code, Codex and An - **Subtasks and teams.** Agents can spawn subtasks on their own branches and merge them back, or several agents can share one worktree and talk through a board. - **Workflows.** Named graphs of stages, each run as its own task, with fan-out and fan-in. - **TODO.md.** A project's `TODO.md` items show in the Project tab; start or discuss one in a click. -- **Changes, Files and Terminal.** Per-task diff view with line comments sent to the agent, a read-only file browser, and a shell in the worktree. +- **Changes, Files and Terminal.** Per-task diff view with line comments sent to the agent, a read-only file browser, and shells in the worktree, including ones that start by running a saved command. - **Previews.** Agents expose dev servers, and the app opens each at a local `*.localhost` address forwarded over the tailnet. - **Screen and app control.** Agents can see and drive the Mac byre runs on (screen capture, accessibility, Orion, Chrome DevTools) and the Macs you have the app open on (shell, files, the same screen tools). - **Status.** Running turns, stuck or zombie agent processes, rate-limit usage and connected clients. diff --git a/api/openapi.yaml b/api/openapi.yaml index f839ed8..ba54475 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -150,6 +150,60 @@ paths: description: Deleted. "404": { $ref: "#/components/responses/Error" } + /api/me/commands: + get: + operationId: listSavedCommands + description: > + The calling user's saved commands on this server, by name. Each starts a terminal in a task that runs it. + command.saved and command.deleted events tell the user's clients of changes. + responses: + "200": + description: The saved commands. + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/SavedCommand" } + post: + operationId: createSavedCommand + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/SavedCommandSave" } + responses: + "201": + description: The saved command. + content: + application/json: + schema: { $ref: "#/components/schemas/SavedCommand" } + "400": { $ref: "#/components/responses/Error" } + + /api/me/commands/{commandId}: + parameters: + - { name: commandId, in: path, required: true, schema: { type: string } } + put: + operationId: updateSavedCommand + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/SavedCommandSave" } + responses: + "200": + description: The saved command. + content: + application/json: + schema: { $ref: "#/components/schemas/SavedCommand" } + "400": { $ref: "#/components/responses/Error" } + "404": { $ref: "#/components/responses/Error" } + delete: + operationId: deleteSavedCommand + responses: + "204": + description: Deleted. + "404": { $ref: "#/components/responses/Error" } + /api/me/focus: get: operationId: getFocus @@ -717,16 +771,54 @@ paths: "404": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } - /api/tasks/{taskId}/terminal: + /api/tasks/{taskId}/terminals: parameters: - { name: taskId, in: path, required: true, schema: { type: string } } + get: + operationId: listTaskTerminals + description: > + The task's running terminals, login shells in its worktree, oldest first. A terminals.changed event with the + task's id is broadcast whenever one starts or ends. + responses: + "200": + description: The terminals. + content: + application/json: + schema: + type: array + items: { $ref: "#/components/schemas/TaskTerminal" } + "404": { $ref: "#/components/responses/Error" } + "409": { $ref: "#/components/responses/Error" } + post: + operationId: startTaskTerminal + description: > + Starts a terminal in the task's worktree. With command_id, the calling user's saved command is typed into it + once the shell is ready, and the terminal takes the command's name; otherwise it is named Shell. + requestBody: + required: true + content: + application/json: + schema: { $ref: "#/components/schemas/TerminalStart" } + responses: + "201": + description: The terminal started. + content: + application/json: + schema: { $ref: "#/components/schemas/TaskTerminal" } + "404": { $ref: "#/components/responses/Error" } + "409": { $ref: "#/components/responses/Error" } + + /api/tasks/{taskId}/terminals/{terminalId}: + parameters: + - { name: taskId, in: path, required: true, schema: { type: string } } + - { name: terminalId, in: path, required: true, schema: { type: string } } get: operationId: getTaskTerminal description: > - A WebSocket attached to the task's terminal, a login shell in its worktree, started at cols by rows if none is - running. The server first sends what the terminal has recently printed, then its output as it comes, all as - binary messages. The client sends input as binary messages and resizes as text messages holding a - TerminalSize. When the shell exits the server closes with code 4000 and the exit status as the reason. + A WebSocket attached to one of the task's terminals, resized to cols by rows. The server first sends what the + terminal has recently printed, then its output as it comes, all as binary messages. The client sends input as + binary messages and resizes as text messages holding a TerminalSize. When the shell exits the server closes + with code 4000 and the exit status as the reason. parameters: - { name: cols, in: query, required: true, schema: { type: integer, minimum: 1 } } - { name: rows, in: query, required: true, schema: { type: integer, minimum: 1 } } @@ -736,6 +828,13 @@ paths: "400": { $ref: "#/components/responses/Error" } "404": { $ref: "#/components/responses/Error" } "409": { $ref: "#/components/responses/Error" } + delete: + operationId: closeTaskTerminal + description: Hangs up the terminal's shell. + responses: + "204": + description: Closed. + "404": { $ref: "#/components/responses/Error" } /api/tasks/{taskId}/previews: parameters: @@ -1980,6 +2079,34 @@ components: cols: { type: integer, minimum: 1 } rows: { type: integer, minimum: 1 } + TaskTerminal: + type: object + required: [id, name, started_at] + properties: + id: { type: string } + name: { type: string, description: "Shell, or the name of the saved command it was started with." } + started_at: { type: string, format: date-time } + + TerminalStart: + type: object + properties: + command_id: { type: string, description: "A saved command of the calling user's to run in it." } + + SavedCommand: + type: object + required: [id, name, command] + properties: + id: { type: string } + name: { type: string } + command: { type: string, description: "The line typed into the new terminal's shell." } + + SavedCommandSave: + type: object + required: [name, command] + properties: + name: { type: string, minLength: 1 } + command: { type: string, minLength: 1 } + Permission: type: object required: [name, status, detail, settings] diff --git a/docs/operations.md b/docs/operations.md index a517a7c..ee2a989 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -121,7 +121,8 @@ byre never signs with a key on the machine it runs on. When a project checkout's ## Terminals - `terminal_type` takes named keys because Claude Code passes tool arguments through as the text the model wrote, so an escape like `\u0003` in `text` reaches the shell as six characters. -- In a new shell it waits for the prompt first: typing earlier would reach the PTY while it still echoes in cooked mode, before zsh's line editor takes over, so the text would show twice. +- In a new shell it waits for the prompt first: typing earlier would reach the PTY while it still echoes in cooked mode, before zsh's line editor takes over, so the text would show twice. A terminal started from a saved command waits the same way before its command is typed. +- Saved commands belong to a user on one server, because what they do is usually tied to that Mac: signing in again to the credential an `apiKeyHelper` uses (AppleConnect's `getToken -n cli` on a work Mac) is the case they were made for. Its terminal stays open after the command, so the result stays readable. - It types one byte per write: a program in raw mode that stops reading blocks a PTY write, and the PTY's file doesn't support deadlines, so this keeps the call within its limits while leaving at most one byte to arrive late. - zsh gets shell integration through a `.zshenv` in `/zsh` (via `ZDOTDIR`) that reports the prompt (OSC 133) and the line being edited, so byre can wake the agent when a line it left is run. It turns off `PROMPT_SP`, which would print a `%` padded to the width the shell started at, so an agent-opened shell (120 columns) replayed in a narrower tab wraps each one onto an extra line. A user `.zshrc` that sets `zle-line-pre-redraw` with `zle -N` rather than `add-zle-hook-widget` replaces the hook, and byre then sees the line as empty. Other shells get no integration and nothing wakes their agents. - The terminal WebSocket checks that `Origin` matches `Host`, so another site can't open one through the user's Tailscale identity. diff --git a/internal/agents/leadtools.go b/internal/agents/leadtools.go index 5ce56d4..560e624 100644 --- a/internal/agents/leadtools.go +++ b/internal/agents/leadtools.go @@ -492,7 +492,7 @@ func (m *Manager) leadTools(l *live) []mcp.Tool { tool("show", "Point the human's right pane at a task, or at a project with no task, and select one of its tabs. The tabs' order is fixed. "+ "Whenever the human asks you to show something (\"show me that when you can\") or you want to show something, call show right away; never hold back because you expect a refusal, since only a refused show arranges byre's signal to try again. "+ - "Tabs: transcript, board (a team task's board: its agents' posts, the human's and yours), changes, files, terminal (a shell in the task's worktree), workflow (a workflow stage task's run: its graph and each stage's state, attempts and outputs), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+ + "Tabs: transcript, board (a team task's board: its agents' posts, the human's and yours), changes, files, terminal (the task's shells in its worktree, and buttons that start one running a saved command), workflow (a workflow stage task's run: its graph and each stage's state, attempts and outputs), project (the project's dashboard: its active tasks and its TODO.md items); with only project_id, only project shows anything. "+ "Omit both task_id and project_id to clear the pane. "+ "If the pane already shows that task (or project) on that tab, nothing changes and you are told so. "+ "Otherwise refused for 10 seconds after the human last moved the pane themselves (switching task or tab), whatever else holds; if a show was refused for it, byre signals you \"The move hold is over\" when those 10 seconds pass without another move, and sends no signal otherwise. "+ diff --git a/internal/agents/terminal.go b/internal/agents/terminal.go index 0f19bf3..533a2b7 100644 --- a/internal/agents/terminal.go +++ b/internal/agents/terminal.go @@ -1,7 +1,6 @@ package agents import ( - "bytes" "context" "encoding/json" "fmt" @@ -15,23 +14,34 @@ import ( "g.fc.run/ai/byre/internal/terminal" ) -const ( - typeQuiet = 500 * time.Millisecond - typeMaxWait = 10 * time.Second - typeKeep = 256 << 10 -) - var agentTerminalSize = api.TerminalSize{Cols: 120, Rows: 40} +const terminalArg = `"terminal": {"type": "string", "description": "The id of one of the task's terminals, as the list at the end of each reply gives it. Without it, the first."}` + +type terminalReadArgs struct { + Terminal string `json:"terminal"` +} + func (m *Manager) terminalReadTool(l *live) mcp.Tool { return mcp.Tool{ Name: "terminal_read", - Description: "Read your task's terminal: the shell in this task's worktree that the human uses from the Terminal tab, and that terminal_type types into. " + - "Returns its recent output as plain text, escape sequences removed, at most the last 64 KiB. " + + Description: "Read one of your task's terminals: the shells in this task's worktree that the human uses from the Terminal tab, and that terminal_type types into. " + + "Returns its recent output as plain text, escape sequences removed, at most the last 64 KiB, then the task's terminals by id and name. " + "Full-screen programs (vim, top, less) come out garbled, since this is the output stream, not a picture of the screen.", - InputSchema: json.RawMessage(`{"type": "object", "properties": {}}`), - Handler: func(ctx context.Context, args json.RawMessage) (string, error) { - term := m.terminals.Get(l.taskID) + InputSchema: json.RawMessage(`{"type": "object", "properties": {` + terminalArg + `}}`), + Handler: func(ctx context.Context, raw json.RawMessage) (string, error) { + args := terminalReadArgs{} + err := json.Unmarshal(raw, &args) + if err != nil { + return "", fmt.Errorf("malformed terminal_read call: %w", err) + } + term := m.terminals.First(l.taskID) + if args.Terminal != "" { + term = m.terminals.Get(l.taskID, args.Terminal) + if term == nil { + return "", fmt.Errorf("no terminal %s in this task%s", args.Terminal, m.terminalList(l.taskID)) + } + } if term == nil { return "No shell is running in this task's terminal.", nil } @@ -40,11 +50,23 @@ func (m *Manager) terminalReadTool(l *live) mcp.Tool { if exited { out += "\n[shell ended: " + exit + "]" } - return out + inputLine(term), nil + return out + inputLine(term) + m.terminalList(l.taskID), nil }, } } +func (m *Manager) terminalList(taskID string) string { + terms := m.terminals.List(taskID) + if len(terms) == 0 { + return "" + } + out := "\n[terminals:" + for _, t := range terms { + out += fmt.Sprintf(" %s %q", t.ID, t.Name) + } + return out + "]" +} + func inputLine(term *terminal.Terminal) string { line, ok := term.InputLine() if !ok { @@ -74,15 +96,16 @@ func init() { } type terminalTypeArgs struct { - Keys []string `json:"keys"` - Text string `json:"text"` - Append bool `json:"append"` + Terminal string `json:"terminal"` + Keys []string `json:"keys"` + Text string `json:"text"` + Append bool `json:"append"` } func (m *Manager) terminalTypeTool(l *live) mcp.Tool { return mcp.Tool{ Name: "terminal_type", - Description: "Type into your task's terminal: the shell in this task's worktree that the human uses from the Terminal tab, starting it when none is running and waiting for its prompt before typing. " + + Description: "Type into one of your task's terminals: the shells in this task's worktree that the human uses from the Terminal tab, by default the first, starting a shell when none is running and waiting for its prompt before typing. " + "keys are named keys sent first (ctrl-c cancels, ctrl-u clears the input line, enter runs it), then text goes to the shell exactly as keystrokes, so end a command with a real line break to run it (an escape you write in text, such as \"\\u0003\", can arrive as those literal characters, so send control keys through keys), or press enter in a later call. " + "When the input line still holds unsent text after keys, text is refused, so that it isn't joined onto someone else's command: clear it with ctrl-u first, or set append to add to it on purpose. " + "Replies from this tool and terminal_read end with the shell's current input line when zsh is at its prompt. " + @@ -92,6 +115,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { InputSchema: json.RawMessage(`{ "type": "object", "properties": { + ` + terminalArg + `, "keys": {"type": "array", "items": {"type": "string", "enum": ["ctrl-a", "ctrl-b", "ctrl-c", "ctrl-d", "ctrl-e", "ctrl-f", "ctrl-g", "ctrl-h", "ctrl-i", "ctrl-j", "ctrl-k", "ctrl-l", "ctrl-m", "ctrl-n", "ctrl-o", "ctrl-p", "ctrl-q", "ctrl-r", "ctrl-s", "ctrl-t", "ctrl-u", "ctrl-v", "ctrl-w", "ctrl-x", "ctrl-y", "ctrl-z", "enter", "escape", "tab", "backspace", "up", "down", "left", "right"]}, "description": "Named keys to press, in order, before text."}, "text": {"type": "string", "description": "The keystrokes to send after keys."}, "append": {"type": "boolean", "description": "Type text even though the input line already holds unsent text, adding to it."} @@ -110,9 +134,18 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { if task.Status != api.TaskStatusActive { return "", fmt.Errorf("task %s is %s", task.Id, task.Status) } - term, err := m.terminals.Attach(task, agentTerminalSize) - if err != nil { - return "", err + term := m.terminals.First(l.taskID) + if args.Terminal != "" { + term = m.terminals.Get(l.taskID, args.Terminal) + if term == nil { + return "", fmt.Errorf("no terminal %s in this task%s", args.Terminal, m.terminalList(l.taskID)) + } + } + if term == nil { + term, err = m.terminals.Start(task, "Shell", "", agentTerminalSize) + if err != nil { + return "", err + } } keys := "" for _, k := range args.Keys { @@ -144,7 +177,7 @@ func (m *Manager) terminalTypeTool(l *live) mcp.Tool { sent += args.Text } } - out += inputLine(term) + out += inputLine(term) + m.terminalList(l.taskID) submitted := strings.HasSuffix(sent, "\n") || strings.HasSuffix(sent, "\r") if !term.Prompts { if args.Text != "" { @@ -184,9 +217,9 @@ func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (str defer term.Unsubscribe(out) got := []byte{} - if promptWait(term) > 0 { + if term.Settling() { var err error - got, err = awaitPrompt(ctx, term, out, keepTail(backlog)) + got, err = term.AwaitPrompt(ctx, out, terminal.KeepTail(backlog)) if err != nil { return "", err } @@ -220,8 +253,8 @@ func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (str } return res } - deadline := time.After(typeMaxWait) - quiet := time.NewTimer(typeQuiet) + deadline := time.After(terminal.MaxWait) + quiet := time.NewTimer(terminal.Quiet) defer quiet.Stop() for { select { @@ -240,46 +273,8 @@ func typeAndWait(ctx context.Context, term *terminal.Terminal, text string) (str if !open { return result(), nil } - got = keepTail(append(got, b...)) - quiet.Reset(typeQuiet) - } - } -} - -func keepTail(b []byte) []byte { - if len(b) > 2*typeKeep { - return bytes.Clone(b[len(b)-typeKeep:]) - } - return b -} - -func promptWait(term *terminal.Terminal) time.Duration { - started, last := term.Activity() - wait := time.Until(started.Add(typeMaxWait)) - if !last.IsZero() { - wait = min(wait, time.Until(last.Add(typeQuiet))) - } - return wait -} - -func awaitPrompt(ctx context.Context, term *terminal.Terminal, out chan []byte, got []byte) ([]byte, error) { - for { - wait := promptWait(term) - if wait <= 0 { - return got, nil - } - timer := time.NewTimer(wait) - select { - case <-ctx.Done(): - timer.Stop() - return nil, ctx.Err() - case <-timer.C: - case b, open := <-out: - timer.Stop() - if !open { - return got, nil - } - got = keepTail(append(got, b...)) + got = terminal.KeepTail(append(got, b...)) + quiet.Reset(terminal.Quiet) } } } diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 04a6a6a..063cb96 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -1168,6 +1168,20 @@ type Review struct { // ReviewState skipped: the snapshot's step changed no files, so no reviewer ran. type ReviewState string +// SavedCommand defines model for SavedCommand. +type SavedCommand struct { + // Command The line typed into the new terminal's shell. + Command string `json:"command"` + Id string `json:"id"` + Name string `json:"name"` +} + +// SavedCommandSave defines model for SavedCommandSave. +type SavedCommandSave struct { + Command string `json:"command"` + Name string `json:"name"` +} + // SessionCreate defines model for SessionCreate. type SessionCreate struct { // Images Blob ids from uploadBlob, sent to the agent as images alongside the prompt. @@ -1405,6 +1419,15 @@ type TaskCreate struct { // TaskTab defines model for TaskTab. type TaskTab string +// TaskTerminal defines model for TaskTerminal. +type TaskTerminal struct { + Id string `json:"id"` + + // Name Shell, or the name of the saved command it was started with. + Name string `json:"name"` + StartedAt time.Time `json:"started_at"` +} + // TaskWorkflow The workflow a task froze when it was created, as it was then. type TaskWorkflow struct { // Definition A workflow's definition, which holds everything but its name: its steps, run in order. Run on a brief, each stage runs as its own task once the steps before it have finished; as a project's review workflow, each stage is a reviewer of a snapshot and gets the earlier stages' reviews. @@ -1430,6 +1453,12 @@ type TerminalSize struct { Rows int `json:"rows"` } +// TerminalStart defines model for TerminalStart. +type TerminalStart struct { + // CommandId A saved command of the calling user's to run in it. + CommandId *string `json:"command_id,omitempty"` +} + // Todo defines model for Todo. type Todo struct { // Exists Whether the default branch has a TODO.md at its root. @@ -1734,6 +1763,12 @@ type UpdateMeJSONRequestBody = UserUpdate // UpdateAwayJSONRequestBody defines body for UpdateAway for application/json ContentType. type UpdateAwayJSONRequestBody = Away +// CreateSavedCommandJSONRequestBody defines body for CreateSavedCommand for application/json ContentType. +type CreateSavedCommandJSONRequestBody = SavedCommandSave + +// UpdateSavedCommandJSONRequestBody defines body for UpdateSavedCommand for application/json ContentType. +type UpdateSavedCommandJSONRequestBody = SavedCommandSave + // UpdateDefaultModelJSONRequestBody defines body for UpdateDefaultModel for application/json ContentType. type UpdateDefaultModelJSONRequestBody = DefaultModel @@ -1785,6 +1820,9 @@ type CreateSessionJSONRequestBody = SessionCreate // MarkSummaryReadJSONRequestBody defines body for MarkSummaryRead for application/json ContentType. type MarkSummaryReadJSONRequestBody = SummaryRead +// StartTaskTerminalJSONRequestBody defines body for StartTaskTerminal for application/json ContentType. +type StartTaskTerminalJSONRequestBody = TerminalStart + // PutWorkflowJSONRequestBody defines body for PutWorkflow for application/json ContentType. type PutWorkflowJSONRequestBody = WorkflowDefinition @@ -1848,6 +1886,18 @@ type ServerInterface interface { // (PUT /api/me/away) UpdateAway(w http.ResponseWriter, r *http.Request) + // (GET /api/me/commands) + ListSavedCommands(w http.ResponseWriter, r *http.Request) + + // (POST /api/me/commands) + CreateSavedCommand(w http.ResponseWriter, r *http.Request) + + // (DELETE /api/me/commands/{commandId}) + DeleteSavedCommand(w http.ResponseWriter, r *http.Request, commandId string) + + // (PUT /api/me/commands/{commandId}) + UpdateSavedCommand(w http.ResponseWriter, r *http.Request, commandId string) + // (PUT /api/me/default-model) UpdateDefaultModel(w http.ResponseWriter, r *http.Request) @@ -1992,8 +2042,17 @@ type ServerInterface interface { // (PUT /api/tasks/{taskId}/summary-read) MarkSummaryRead(w http.ResponseWriter, r *http.Request, taskId string) - // (GET /api/tasks/{taskId}/terminal) - GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskId string, params GetTaskTerminalParams) + // (GET /api/tasks/{taskId}/terminals) + ListTaskTerminals(w http.ResponseWriter, r *http.Request, taskId string) + + // (POST /api/tasks/{taskId}/terminals) + StartTaskTerminal(w http.ResponseWriter, r *http.Request, taskId string) + + // (DELETE /api/tasks/{taskId}/terminals/{terminalId}) + CloseTaskTerminal(w http.ResponseWriter, r *http.Request, taskId string, terminalId string) + + // (GET /api/tasks/{taskId}/terminals/{terminalId}) + GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskId string, terminalId string, params GetTaskTerminalParams) // (GET /api/tasks/{taskId}/unmerged) GetUnmerged(w http.ResponseWriter, r *http.Request, taskId string) @@ -2463,6 +2522,86 @@ func (siw *ServerInterfaceWrapper) UpdateAway(w http.ResponseWriter, r *http.Req handler.ServeHTTP(w, r) } +// ListSavedCommands operation middleware +func (siw *ServerInterfaceWrapper) ListSavedCommands(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListSavedCommands(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// CreateSavedCommand operation middleware +func (siw *ServerInterfaceWrapper) CreateSavedCommand(w http.ResponseWriter, r *http.Request) { + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CreateSavedCommand(w, r) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// DeleteSavedCommand operation middleware +func (siw *ServerInterfaceWrapper) DeleteSavedCommand(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "commandId" ------------- + var commandId string + + err = runtime.BindStyledParameterWithOptions("simple", "commandId", r.PathValue("commandId"), &commandId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "commandId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteSavedCommand(w, r, commandId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// UpdateSavedCommand operation middleware +func (siw *ServerInterfaceWrapper) UpdateSavedCommand(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "commandId" ------------- + var commandId string + + err = runtime.BindStyledParameterWithOptions("simple", "commandId", r.PathValue("commandId"), &commandId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "commandId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.UpdateSavedCommand(w, r, commandId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // UpdateDefaultModel operation middleware func (siw *ServerInterfaceWrapper) UpdateDefaultModel(w http.ResponseWriter, r *http.Request) { @@ -3788,6 +3927,93 @@ func (siw *ServerInterfaceWrapper) MarkSummaryRead(w http.ResponseWriter, r *htt handler.ServeHTTP(w, r) } +// ListTaskTerminals operation middleware +func (siw *ServerInterfaceWrapper) ListTaskTerminals(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "taskId" ------------- + var taskId string + + err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.ListTaskTerminals(w, r, taskId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// StartTaskTerminal operation middleware +func (siw *ServerInterfaceWrapper) StartTaskTerminal(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "taskId" ------------- + var taskId string + + err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.StartTaskTerminal(w, r, taskId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + +// CloseTaskTerminal operation middleware +func (siw *ServerInterfaceWrapper) CloseTaskTerminal(w http.ResponseWriter, r *http.Request) { + + var err error + _ = err + + // ------------- Path parameter "taskId" ------------- + var taskId string + + err = runtime.BindStyledParameterWithOptions("simple", "taskId", r.PathValue("taskId"), &taskId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "taskId", Err: err}) + return + } + + // ------------- Path parameter "terminalId" ------------- + var terminalId string + + err = runtime.BindStyledParameterWithOptions("simple", "terminalId", r.PathValue("terminalId"), &terminalId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "terminalId", Err: err}) + return + } + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.CloseTaskTerminal(w, r, taskId, terminalId) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r) +} + // GetTaskTerminal operation middleware func (siw *ServerInterfaceWrapper) GetTaskTerminal(w http.ResponseWriter, r *http.Request) { @@ -3803,6 +4029,15 @@ func (siw *ServerInterfaceWrapper) GetTaskTerminal(w http.ResponseWriter, r *htt return } + // ------------- Path parameter "terminalId" ------------- + var terminalId string + + err = runtime.BindStyledParameterWithOptions("simple", "terminalId", r.PathValue("terminalId"), &terminalId, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true, Type: "string", Format: "", ValueIsUnescaped: true}) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "terminalId", Err: err}) + return + } + // Parameter object where we will unmarshal all parameters from the context var params GetTaskTerminalParams @@ -3833,7 +4068,7 @@ func (siw *ServerInterfaceWrapper) GetTaskTerminal(w http.ResponseWriter, r *htt } handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.GetTaskTerminal(w, r, taskId, params) + siw.Handler.GetTaskTerminal(w, r, taskId, terminalId, params) })) for _, middleware := range siw.HandlerMiddlewares { @@ -4159,6 +4394,10 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/me/lead/messages", wrapper.MessageLead) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/lead/notes", wrapper.ListLeadNotes) m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/me/lead/notes/{noteId}", wrapper.DeleteLeadNote) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/commands", wrapper.ListSavedCommands) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/me/commands", wrapper.CreateSavedCommand) + m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/me/commands/{commandId}", wrapper.DeleteSavedCommand) + m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/commands/{commandId}", wrapper.UpdateSavedCommand) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/focus", wrapper.GetFocus) m.HandleFunc(http.MethodPut+" "+options.BaseURL+"/api/me/focus", wrapper.SetFocus) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/me/summary-reads", wrapper.GetSummaryReads) @@ -4190,7 +4429,10 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/diff", wrapper.GetTaskDiff) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/dir", wrapper.GetTaskDir) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/file", wrapper.GetTaskFile) - m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/terminal", wrapper.GetTaskTerminal) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/terminals", wrapper.ListTaskTerminals) + m.HandleFunc(http.MethodPost+" "+options.BaseURL+"/api/tasks/{taskId}/terminals", wrapper.StartTaskTerminal) + m.HandleFunc(http.MethodDelete+" "+options.BaseURL+"/api/tasks/{taskId}/terminals/{terminalId}", wrapper.CloseTaskTerminal) + m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/terminals/{terminalId}", wrapper.GetTaskTerminal) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/previews", wrapper.ListPreviews) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/snapshots", wrapper.ListSnapshots) m.HandleFunc(http.MethodGet+" "+options.BaseURL+"/api/tasks/{taskId}/reviews", wrapper.ListReviews) @@ -4233,310 +4475,318 @@ func HandlerWithOptions(si ServerInterface, options StdHTTPServerOptions) http.H // const string: with thousands of chunks the chained `+` fold is several // times slower for the Go compiler than parsing a slice literal. var swaggerSpec = []string{ - "7L1rcxs5lij4VxDcjbA9k6Jc09V3d6TYD267Hr5brvJarq2I7a7gQEyQRCsJZAFIySqH97ffOA8gkUkk", - "ScmWy909X2wqE4nHwcHBeZ/3s6XdttYoE/zs7P3MKd9a4xX+8Y1z1sGPpTVBmQA/Zds2eimDtub0794a", - "eOaXG7WV8Ot/d2o1O5v9b6d9r6f01p9Sbx8+fKhmtfJLp1voZHZGw8xn8ILbQlfP1sqE184ulce/W2db", - "5YKmqW2UbMIGfg378qFbXomtksYLKULnjNBeuM4YbdZCmloYK3xwSm6FulYmiI30Qjqnr1UtVtYJ7GEh", - "V0G5hVdLa2p/LtQ7HVTN/YaNEi1NDDpfW6Pms2qmTLednf11Zq9mFc0DHuKHs1+rWbhtFU7QabOeVbN3", - "J/DBybV0Rm5hUX+dfY+L+gm+o58X3Av99Q339aGaNdKHBc5/IcMuFH7ZKDOYJrQXN84GJaRotFEiWKGD", - "Z1DMqtnKui10NatlUCdBb9WsmlknbqArHYQP0gVVz2dnpmua8XI+VLNW1zARfq5NUGvl4IVX3mtrFoP3", - "/Xc+yKDgjfTCGoHbfkGfpMGGq3u7UYI7feQFfV+YUJD+amrQa2Vq6wqvPlQzp37rtFM17Eg2977D9Dkt", - "Oi5hvCtVRNJ+8+3l39UywATyZe5i99IpGVTNe1vcmp0lxW8ub4srXll3lXocYYsOG8HvV85uzxB1aI2P", - "vNC1sCt8hFgUNnCS4E/4RFwp1fpKSLFV3ss1PhXPG9nVSjy3tariMcTntlbv5qXZZ8PvTvAZDvXIi5V2", - "nlFROOW7rYLTqD1PNmJFJZZdEDKItOhKdKZWTkhh1A23XvSbey7kpVcGPxA2bFTqyhdnO4FVW7W9VG53", - "/oCwEjb8kRdw2IU2ePgAoR55EZTcngu1bcMtnACJz8UN7Io1/GWFtAum1yhZe/zLqWutbpQrz3Fra9Xs", - "zuWN+q1THokZNIjj9pQt7XutVrJrwlw8i8AQ1jS3wt/osNwoLy5VuFGKlmJvTP8l9lyelb0xUxBq7Fob", - "cbOxXuEiaeG0v9rHiQIIZIScj3SiMJKzjdodKMLsTEiGX9+NeOyVEm/w6ZPznVG8aLQPMLVrRDvllbBd", - "yEm/3raN2ioTlANyoGQ9q9KQu5dATv1SF3Wj4CO6sWYV3s+NOv4SySnLBfT+knrcef4mDbHz6nk/Jsyx", - "u8St8Lvg/N7eiK00t5FCRCxPnwjpVLx+K3Epl1drZztTA2Z7oc2y6WpVC7xusQekFWEj040DT7d4SytT", - "wwVUFe6YjNaPeIsMaRCp9qFMfysMO/nWNo298QKoE04SERx/RdS5kV4wBcaTeybWbTj5N2qJDAgcbaR/", - "lVirrTa6fwvXngl67eS1DrcVMCbulukQDWVNTlNzlFviY8STWr2bVTPZ9zT7dXKRi+GtPFxvNlR2OnRd", - "iSWcTiMub8XlrVPnwtKm4bpE2DiAMNwXLl+QWFpzrZyXIXZDR7kzQQ+hmNH3EvkYXc6jS5lIS3Y5EwUs", - "rbi/spFKJMo9uEWr/BrOT0HxPg9BLjdbZpJHt7lkwBauDGXqBfBjZdZJb+W6QMTw8ZmoZZDi5zc/FHH5", - "Sps6pysrjQut9Wo1ix3Dn3Lt5JbhCKs0svUbCwvuXFNEoK1yW1lCG+7sTLyiFsLbzi1VcXqtLHHvMMkz", - "4VRrT5xqZNDXSkDLueAlS0O/BLSEaxRJBpHpG+uuglOqgsvcNl1QgIepH2R4yzeFWu1OBQB1JtY64DWB", - "2OmkWZdXE6G2h8d1Yc8+72NVYRsO8qm420W8vJG3uxgp+Sk3v7S2UdLs9IrNSr3+pbGXBTwnKXFBzd9P", - "s6ifkK0tAq1EJwaTmz7nxdVa6erX1ofn2G534UG9C4enga1K/T9vtDLhjWqbwlbBIS+xMbJe0GlBTlw3", - "QKl5jX4uvF4belUrIEyqxkcydA4xOMH+8rYsPKko+g/HvVCBJELo2hErKZa2a2rzKIhLJZbSOa3qyBjt", - "9vtOhwVcVbt9X0q/qbKZQ1OU7jpfiZOvkiQKV+2VbhpVwz2EfLTequGAOQ2ty+wmzx4FnDKr2oW2C+V5", - "ngkfagtChqnhp3JOBLtWcGVXQgaxtSgqKe1IavofX4v/W/8l3xj4BZJU6yxQzUee+6lgoU7hSp0OyuMS", - "vzPd6+8YGijAe5IIQmKc/UY1zSMvaNoThM53Da5I1rWG5cjmdYZtwXVqLGxvpKk9zThYCwO8ev5aPJdN", - "89ba5g32OJ/tYPXuEdyH+bgTBcEUBEwWh1gKQkZuq9wayLy/8kIC1hu1DKoWr+QSyHxtK4HSnL1WTpzK", - "Vp8ucSB/ym1hwiOK6NbdNvK4HwOd1FEBKMjMb6Wpp5AK2ESU+373GyFhcdnexutuY7dK1NqpZbDuthK6", - "l8Ciugd47LaIAmVygoi2S09AVo405Vy00gFQ48haEXPPxDNH7c7Ab1ULWK4OgtYPiK2XG2wBOwVtPC2X", - "VVVwDnAtcO0urVnpNcrLHhl4fNGu50S7xGPbKtOuW0CJd39++p9PKrGUSBUkdyCbxvOdf5De1UUE7NrG", - "yvqMTyQKOG650dcAHGuUUCa4W6FM7UXXDvkOvPLhfOMC///Tc3GzkQE4e6FRenQKfjjVNnKp6ruoGXSt", - "TGGyPfwJ7AFJkRd/m/0ot0r8rXv69E9LtZW6wZ/qb7MKTgt8Bs0SWX8ll48GWwCyl7Gi88rNmWpdqdsy", - "zWxznhOQelYxFGfVrLY3hn+mC2xWZdg3q+hEzSpcTZH9LHOOsetKpJ4rUUJr8di6CvGp/wSfW5dQ+/YJ", - "CFwISaQb+3e2CAi+khZbv/+ig4tsoKnFo5vkYe1FY816Qui1tqDgyUgSyGgn+DdSJ9htOBTlCTtp/Kqk", - "m4mHAHqMrYSuobe1CiQUD4hsbORP38efL+sP8wTwYldtF/IDJoI9UgC07Z6r5eLWLCf0cW37CPTiQfkg", - "VtpoD0ySft7Yrhb+1iwFUH4UWXvtICxVMlKw+IoCQmtdQIpQuFim1PO6H3Y+pX8/li37ZXOL/UndqF6Z", - "iSfaXpUP6lVxWqhyQO1LWr3w3XKp1FD1MiUxoO1jgo0GpZJcTjLRE/rKV/AYzytgB9xISclrjVAaZ0wS", - "/lzAxtomKXmED3C8mI3Cu1Oa2q5Wc/GClJtwMEQYfhUVmGX8KyyL2Q/CuJJYBNS4jIVEZeE9IX/bInt7", - "2emG0A0wA7BOBy/+Asf5ObXeKFkrF5WiiXYzauL2G8THKXNN4pr2G5Cow0de/KIuL+zySoWe3zoebTfW", - "TwCALhtUig/X+b31cZV77sbdDl++SJDcylqJrh2bsHZBSkCLg01BDK07cgljTcMMSC5x/E4tFRoVZRKU", - "EAO7rTQ0NVYnap+2TZslUe041+FZNkxhjgM6so5ldQRTxX3W2ox+Fsku9c5bW0UkH+FViQzwyXsVj/sE", - "FdhP+alZsXvtvgGubLdrvTYWPx9vHBxCeklEArm6MzFf60CPk6RFKln1DrXWyDZ4sZVhuQH0QtEksRBC", - "XtprNVQ5JapZzQDjy3ujf1dTijE02OrfUfEFjKwfIIM24X98XWQU/O220eaqpPGJzXeRWYYeGEJ74JlA", - "C45mdME9JkX9rY/SSdTCzQlYZ+PGvfkkNQXItVabAGwmstos59XqWi9VJTwRHmim24H2u9YOQECsIw54", - "pJUkosnb21a9wE7yJ99Sh/mjn6jzMSLiPvKIPaB5G6uEcxOY+oP2wEnu4ipAnX/qoLb+0GlNWN/fUNI5", - "eTvNMAOtzITIXE062Btnbchtb2FDzyaYyM4sZVD1gfFQpNjCyeKFAm4YcaOcEk6BCegobgNXViVg5eOX", - "AJ7cWUYkhwzXRxAdbljs+7p490tYbvGcoz5jccA34j4K06j4P9b0Cio9a6J5LBrw0IuBzMwSJT+wvttl", - "5+ddC8MzE0/qibBRt+JSgaQCfLv4CczEeNrhS6E9MSRgyJswBtyiWHj2fhewrbPwcwpCXv02AM00EZzW", - "se/4fPw2YzBWvIH9FA8qixERXjM+jQ71dVQwHXWmsafSgYaDs497V06hZkZJ12jl4l7iEUN2o5VrdcTx", - "4vnygKXFAp18vgGzSAH1WYnmyyaPS22kuy1fSbVq1J5PbVMvykTNKaDHjJvQRFyqlWXVDb3ca40qmuc7", - "n+s0ZF0jC7m1tV5p/InTxV88Pg+xWCJoCob7CULGo1UZ7HJgJKBNbkXvpjfcix7Yu0SZLGke9Pc/v/32", - "5P8UYKc4x3/h5CLlL/Mw01BjHuaYU1k2ney9SnBqG9vUnhxSwob55eiFAEs6/vrgq5phlI9cTdtsANwv", - "wJa6A+sW+MHikibgVZ4VdVMe2tTarN+wp+buDJyS7Jg5wdORY432orbIzl2CJUOTdv2GVAnMpk1aTPux", - "ewPzO+K2kOeBn8dxYrycb+Pn/PfL1MsYQGnwKq50D5Quon/NmJ9+B2SCriWbW5EJNsGKlX43Z7GgPhO1", - "9lvtvarJ7NKoVQBLS8S4V8qtlbiRDpSjOYOKAxUZweNggh/nIGGfnHx5P7c1L3KIB8m3aN8tM4DTzlWI", - "T4vgBV6ggPyD2zpzqhsoI3aJEH9HdFqvN0G00ijhN/bGn+fbExuSxoOudGIwQtkH9PIQAN5Kf/VWXu71", - "InrLE5iY3846D9F7mFYJqi/NpX33MqiiDyTqEDSwaoaxVPorVWdaBVRIwxrwz4Iu8qF4zomuMr5uuBg0", - "Kr0L4vEr6a5ALdybgZYbq5foGoZuytKL/zoRj8UTvJD+S5zin/8W/2z18urEGhQh/+tE/FX8mrf767tf", - "83bgp4YufMh+PakSRCthcWZePFZyueE5oGfmu4AGNutq5Z5UYts1QbdgRQiuI1/X2PWTSjgFahB0UxOP", - "YTFb6WCHeE2PsL8nlZDJWcjP/2ZKdsIDXK9T3jbXd9yk9NGEK8XA9f8oDhXRNd1DBU71KAqEvTD9uQOb", - "jlqoDE79pwXhKmfgo+vXwLurX/zkuaSF1qX79qMAxwpxYOy1eUmffzWG5cQdeHC6Jf4gQ7+jZ5z5txX2", - "+T40o1aNvlbu01AadAormNrAUUno2g9u+kg8yZXMCwmCq9d1FBMIbkA+E2h2BhxDAFpOnVYmLgNYH+ww", - "uZSZ6esIfEa4WWREkt4GlheED9bW4u+dD0NJiJaItgYwiah6AJt0o5GJK+0U8T6siu6HRv8fcQlzulJm", - "fkc+n9WPRx8XUMbBh51X7kgqETeHP0rqOsaaanAiDkr4pcP7Gc7YFIpb8jmyji8xUuakCy/ecWfiMp4F", - "tDuQVRfOR/WlHI7RulqaNjMFXpl4S0M4llyG5hZ9MMi3OzEC2Z1ObAGqGbNGOVDutoyIwwX40zShwS4w", - "o9/LVoKSLLfYRRA7Jdb6Wpm5iIum/2UjjA2KqRfuxIBRymBSVsfe72yNj1A6PdjfwSPxlkdN8XAEqUn9", - "fPz+YhwKYVtl0KHd3yhHmpYohaE/dqucV3VRv1LNflCy/tGWjuanZHAHbNp+scZg8B1ETyovgh1IDZLe", - "osq0b8JxANxJeYenyCprae+wyKJzPXQ/4pSynkuo8IqU5FN08eBF/RHECWZ7txN9hM+B9hSSglK/DMph", - "5MpcRB8O8BNXLosNGXkRVPnDPHYq3cYxcI7V5MV99hAnMIVl6PbGkXpp0vBF1bvjknEOXbh+61SnalwQ", - "/h3HT2rifkog0Lja70W9EegOE7nD0vGU0u1HG/SKg49fO7XyJR0XuhiCrM7rMfDRLQGFzBEEi61c/nQB", - "p21XTq61X0pXgDb0O+e3C9+t1xRJRyOVlaTosVOYaIxDwvdxrpZ0GhNdodvronX2slElvzKcHTXywbYt", - "b3L2mKKaKSBtUTtqs2/28aIsjPb/xFdg7wFP7BgvWOzIyaAWjd7qsAcWfSNVHwcSp2R9OwEJfLd3dey4", - "VThVqOvpHIZEyiCUqVm5Iny33Up3Kx7D6znJ+viKX5yJ4Dr1pDwgcOcrCCWbmHJ8v0gOadPTHx2afqf6", - "dSXsG8I/gq1KiL6DW/lUSyfxtTRqQgVYO7mKRu0RVF2ngBdqlJDEK13ad/E4Zlo1Uu13hig/E/VdYKI5", - "UtV7xwnJ0+9Gm9reoOEZP0w+HdyNUBS5KbQX2vQMb5rWEXsQZ1T1QCgDzwW91K0smWpq7dtG3i4mHUTW", - "OizQi3fybfy0FAmZPMWQFG4k2n28CkKiLxq6Futwe0423QCt1yqAuBec1M2EM9aUq9EIPtSsGq4xm3O+", - "uCLglAP2rxgyX6vAMJkwegTl4bauMyMH+UtGqVYim0E3tJiIEC2Dllg9fS2Xt2LtJLD8vluiB//F0ill", - "xBu8SLVZF3v1KgCy+HLP706AJVRzf+uD2rZOrZRTZqk8xP5FuR+HfeTFBTYSF9zjGHNLls3hiJ1ZbtQS", - "tJYJVcIgyMd1pl/esy7YLbmJ2pX4ycEPOnn0m+yKHP+b20VwvmRIhR1FQ0ga+rDBlNElGUx59zNQFvGH", - "br5PIxJMHlCgvSX9PTwfhSxeNnZ5VQmvHDoKBrEJoT07PSVffXAEBCW01I1RYPH0AV+oM3oPHdKD0/nH", - "uTz0mlMGLK7hoCbkNUkmu/CUXa3DIl4hZbSOb8nf0XUmuZD1UeIkxENUDAhGGOdVRV6W2Q1x6aRZbiha", - "hRAVPBFzlyVjBU6ozMc+QFAiT21BUzs+bnEPUjm1tWHqVWsXk7Z4Zvb27wU16reEkjD4gJw7vyRhdKxv", - "ZOH0ESWz8COw06fHhnIz7vXr2YFkgsO+QO3xiqsxOu5B5Smxdc++TAO/TLP6L/bM45dsu4Yzmd7IZ8JL", - "ICOxBeoE8+04xjJ5CEjBLm0z4Uh3J2OUxBVAnhbORESx/xQmOW0ojkbB6F1d7Dv5P4Tdm3Z/LqKi820c", - "F1zguqYRjTLrsBk44e7S3mMzDHG7aphqiBEZ10HA4skd3BZfCkDo9gY2lnysRj6ClDALgEAKAlnLNijX", - "cwWtdJ5jqPBvp5Z2bcBXedepvRJX6paidzlwo2QChR02E1EDGF1LDVAdU0Xij0g00AHt030O8fnDAZsb", - "gzHNrLQVbxKDMZz0T4bJ6S7dHNHeqjfvR6URtdBmjbdhpMAQ772JGQkO2/mniD4qjrjpIJzEX+m2nQhM", - "vM+dORG6RDw4hFiWIwpX5J+yGBhZp/D4CEeX3Fy9m8tmxxEoKp8FOgbwZEjgpxnVi2AX/DxDbB280Aay", - "omjYQIyS5lb+HN9y6BkNAOJpOWI4rh+t1h+7+GRlL6+cvEhkXTvl/WjNR69NJigZG9DfQqGHjm2VmVoh", - "6jo+hUJ+QqH7zXFo3cfHj4NLDUeQ5memu9zqwMq0Puz7rNcQReeWJ5S7iyFUEVhJZwRMFab8Wcqg1uiJ", - "H3RoVCVIoqnIixW+B+JbhN/QUrxLypHuHMX3UTa9tSJlaVyrcPJ4yuDL/nb8CQExo2A+qJbJGGZmxJCa", - "Snjbc480gVx27HNjwcbMKqYhsyoOc6SXHRHrUS6s7OEL6j178m0cKHt2Ecc8OuXgPXBzf+ajDAPidhd9", - "YHYyI0VXGKLNIz56RHlKpLh0BXIesT/CANQ6u23vawIqGfe2bdm69inNMQPzS7aMPcaXsccwTbS4GYwZ", - "BWb9KF9AlKUXfiNL4ga9RY7GKQWtWHDkTBF6GOb1yIvvv3n2YpDHZewx8pEsxjWt6be9LjN88RK8k5Ir", - "CbYwLWZUY6C6ijppil/zIa0LwktTYqkj3N2nxP7dCb+2Hm/5kcYoTtNXdFC+Oud7aUXZJFb+FFjtU2gO", - "8fdEIz6cwnen782HuyqKqlnc24mXer0uRRWBdWShTH3GqQxYl6VMfWJXJ/CW0Wceo2MB3HLIfHG4GQUm", - "oa8r5QFhJwh5pRYRHnNmpM4Gzk70LDPOsp+scqrk9zSH/q/OeqQYzGaFyUTTc5oESq6ACMHaZt473+br", - "5pVyLsN0HqKZQZr+MyQefIpwEO3FpdpotlUMdV7VYK1sjozJWWnOaNYBiGkTIVTRfDGyKw2b0ArttAbT", - "lPD1W8+TOfhx3zHlEkYjDi0097YBWOBKwMkW/ljAHzgkHF0HJhklHUVv2Hx0pAfAY8bYJYiEJxoYb3+e", - "zKxKODarZmmQWTVLq5pRGg43q2YJx5I/CZ3XqyNZhUhH3xK+v0pzGL142znzjakLL6xtntP8Rm+e+avv", - "ebbjb3jyo8dvsrWM+4pLGz3/Fld6gIsoMAxmNrgDMmKQE9ueDBzUGF8Ac/IsBLVtC3fSVKYETFBMea3y", - "BDcx8AfTAbK9VJb9J9jQmVS241x//BZRdAm2PVUvuhYogRX9W1Ar44GOgbDYdCrTEn1WtITGWD0pXlCr", - "/NZJlkkmAKBOWTbWq1rcqklb6J70yVn0goY7DKGPI9JnSB4H/kgJsCnnMn9VXKk2bRcmjFf8nWeaRpvo", - "s2wMODjp+VvlmJjg2ChKAqnaoC+r53SSj/zR+h3EtpcwuxLnN5Xf7e0m4tOCVTbYsAeK51nqINZq7Czb", - "p3XxdxGQWKahGP8Un7XVNd6Vc6HrRp0JuQz6WlVRnuynyIZcYqfSaeg/gF55GcRWEll3a/g/IfhZ/zPe", - "KtQVGO8G56JvR6BIzTDjFJ6R2qqUXcV14A9F2ROQM+mv2JUGN8fBKeagiF1xj3Mjx+VFt4V6lh1woEFp", - "osemTCYkHWdFzp7GHMrZo2/7WeSPX8UZ5Q9fZLPLnz+nmf7cHpQeUStwF0MefdCLeBk5Sid2kkLTmSkF", - "lK3LZpDjg6tZOo0fTEwhlDxLCJkX8OmE1pquME6lVZIyv0EjFjZLGbcQW7UXwcnlFaIw4Fmf9TsmqT6a", - "7AwqV5SiJShPVmF2oBhKtSralpyPSPSAE1xKZdgn5qmEbWqygOKblFD56ImPUxkV5l5fLtDocWR4b1YR", - "4fhIfYg2WExlHH+W753yYtk5pww4g2deBrt9sr59Am9aNgQsem+9o+0GvuDedh8V7c/IUX6oSjiRMuuz", - "7NpB2x0zStWnUMIGgrNNCngF0goGrBpVsbNLSvYPn6hmxYnMBgqo/lQyeBfoFle4NeFxQl5jbxI6DpJ7", - "H42L0N8btSrhoPdqkZ2hosbT3VV3UCj7Uk53j174Mq0URVlw7V/bBGM5rDDDVymm+uowq4anKjNlVP3d", - "bi91iXhdRIrEYYVxCjCsMFY0mC6bTgbxCHJ0WJDUcTrHtC3WHb0p/x9O7aClLIN/RjF2qEF2KqshcR/u", - "cU8xh7RhjJNjL8fxbdCDtrzdu3SgeDuRev+NkoVQwEndk1e/RdNf5jgadVDobUH9kiSwsTfmKJVSIXdJ", - "adJvyYeo5KkTNgtOMb47a3AwwzuIcrVSGEOthI5eXjoIo1SdMtWmMiZho9yN9mXfs431YdqTLr7th0OO", - "kSQiXEbE2Z2OrdPsiDjslrVPmxBaL6hR1MFyl7s1CXDkDSbhXSrHjudK1HYrtbkLa/+WOwN/L7m8Upgx", - "WgZ1Jn60yAVW4keA4Q8AQP79Si432qhnXdhU4oKcueEHZx61TrzZue2mOS5kANNOZ/BPECvjjL+acO0q", - "L3KoneqFdO05hTN86qushAscCUnFMVZ4b/VmcWycLFOFAj1jr/B+H3r/sb1ZcpKCTRoejdgtEqV6faMM", - "QRn4XNyqcAaks48xQ0fqxcq6BWnarBPk0O+tWEnOSAmcHJZJguOSJY3A8TkixCG9hnOPrbZd6M0Er2MU", - "kbzMkn4rWYu1CkTL9drIxqdMHmUlwaX0asHFDnaBNu2rRsqH/XkiSSMqPQuV5AnCQs/xKQuXtimZyF6y", - "sZmlyZ9fsruZaGWjQsBU4JxxWgZKSI1RdFJghwAhul7oK2SoKS7HGo5KwX2Bqx1t8mtbvpofwlfwYdRE", - "nCyQ75RYB+9GaqQfKZFbxsRU8S/0G6sBMqMRxKWCxk5t7XU5P9s9yoyZeBqSAUYPdFNyIq3/HTResJR9", - "2q5D9GSy2tc4pGbfDnKGGIBB/sm+XeEWqfgasv4JTJiiBlQQ+4KFpjSq+Daa43/rpE/kuuhdO84z6wig", - "xnIxOEvHvgg9mggFnU3TEAYPevbFAmUcKmL6/YnGsJQFhgmONuu0o9YsY1wddImnxYtYydAAMmgvNqq5", - "A12iNSQdTAHZ4FXC5AFAaWVbeaVioQBETPyg5zaix1fwacHsIRIW2eiTED5AP3rw5tSDr7peXaiDH9fM", - "rKIVDp8T2wrfRq0JLRYbth3+YCW69dEUhaFNk0gK/DuQ9gVZjfetgu/YFZvUu0vGCfYgpQ5GSBRpGL/k", - "JfMtpcO0Sp0+WBzOCUTsO20gE/ue7kTVNc31TmQHZPEF3NlTKmrnB6xEjCeAZy0mJH+cuoAjmv74d6iV", - "0Ep08WLQRWe3WgTltqiSlV68/unN273qlIM1PXenbckTMqjBzQKM75VqAygzAurZUWJm07zccrp4yr4M", - "UyNvdPIcokWIx8RTzql3Eq3I4cILTusDrZ/MRQ++XsF2VuJciJjQrXou+qv6HNhDFL2jFISCgkEnt8Qe", - "nkfalILeXkUmMeLyGHe150KVN/KWPlcLIFVpIJ+OMlueE7mIHeYEMnYxuHGGk9m9kfqu4X96eN4rG/Br", - "tPauetTxubUCFgjw6IsA8sfnmStrnAUPCvEf8fWiVeRAGAtMUgVRch9AJMUj1XuRcR8+E4+IVWGHqnPm", - "04Pt2fTdKN5kkog5UPnGLfD4/NGCO+E88z5ILBacjJHR0wUWJ2QfXkHWDqIbYzMO2XDOUQd9LoxdsL2d", - "u2LlDap68M38SEvJELTpAf2ON8jITFC0rsQpkaU9a4iIOivyRdVsuAF9lz38j7TVgFQ6MtSkR2ylSX9n", - "Jpr+M5zR6wSG0YvRw1cJMOnRixxCg4aqLrQbPvzRPmPYpUfPIhCHfX1PwBw+fAZAfRNhmk1d1rdv7SsG", - "7XgKFz2IyylNUTYq7/fdNqXzz2Jf/aMd0HS+hw3OiKSUxVHKs4EP164qbScS+1w8HR0eo47SrGU2t5LD", - "Zs+6DZyEECTJRQm4N3i9YKfemx1xOa8Zyur5crYFW9uFLmYG3OE/3/704qf5tsY0d8PRcpv7nXiR/eFj", - "sOg8ARW07AdeOfu7ygVz8XitAiBDjHJKFv6lbW+fjDKjJF+rfeCJoy5cZyYkz9iEmHBdMwsR1VAx0HC8", - "DL+RTu0HymLCw7owbA6pzNUaJwG3xOQ46HhzXJxZITEe24JzMSZpfMb9Z3qinAeNypks6HZPrVTWOe65", - "V8Zs/8S9kdOGgWZvSmNJ6agL1uNpTjpKvo5cPcHuIV2q45tEyRPoYaC7IwgmEWPsnYeRT0l+amFNthsw", - "LjDeRNRLc4d0glka7lLaKqdKC3cqLWaQNG5FB4Mke/SURESt00IOz3+EkIw/OI+4tMntu6/bOHCj7BMe", - "uOIlqwuJ02e3QU5sgIQwE7wbfZX8rmur/Fz8aJmGxwP8STIL5ZXgsQldCTyydcdky8k908dheqjr2LmU", - "OGeY9lEcHYCkGshZLDlPAGDaL2XS3RpbzMUvTMHZbS+g7pvmC1dpSIkYenF03106F28Yw0aT/JspTfO4", - "+0uOImgBg+BO0MYHJWtoQncYh+L1bsDG0pzPSNKMpD1WMcdVXKql3XI5T0h5C19zuE8t/jajmH78E3+q", - "My7Zh531JftSAjhsCjvae1hdOq1WJN+mHI/shZ+pC6JzsS8C68PEyXxLqZQj04jyOMIR6LKlLDJLJr7x", - "lFezqE7IMsr0t9PR3OXl23y0t/LyLzzgW3n5PI35Vl5+y8PCJ/3Ib+XlL/3gb+VlTFzwK6/tl+MyFcic", - "ocnDGPjyq+h84aOwUYUMzLUCkSxWA99H0+OUXvRfTOcHKEeaZ4MVya2S21dU9/zYYmw75CvmNpSNt5he", - "kdB7J6z3mAwqsWNokaIelGTnUAz8tivxFRzKr54+BW2Ok8ugnK9EZ/RvHekysg/vRDvfInnkKdgbOvSu", - "Q4WTJ82os40S+/svb0QMt9oTqBOx9YIj0MeO0Q1tizZ6223z/MCZvOLszcFWOzHNjZ/xl8Vp2druTke9", - "0z74/cbVkTGYDOlRMpGBAWonlK/poj3OV8jWFrOmH3JM4YnH/qdWHDOwjxhIS6nGpBe7acuhw0cUFmsC", - "STnJTjaZzIA/WnUOwQYo7sv5gZaRZOx2gjEYzMgZJZ3iunlc2eFapfkVrDjaT+WF2Hu1x4njnQLT7v3L", - "MLRZ+0BOSpwYNUKHs0sdPjhJRrGYpywCYGrDkPEp5tJadr7krKaa3OySRQZJQzUh+dM0ezj3y41MzlzS", - "MIesuVrjhIFjMrrwWGMSDh4s8RHHAq4IJ3aoK/gs7Y0BPC6mdf+sBkElvY8yf16a7c+GlVETQSKFTaXK", - "mL537OklNEqs6hSYfooGzrKV40AdoiSkDeeBPAgNSclB+/AzXJOI8tCwQ8x/lQLFmJPKnI+RimC0hVdu", - "z6yyXgpRhTJsuOZ2ebRhDb/ZERfIlr3tIseXzW9YvCifWHHLfbFqllN/R5/kctXb3IkV4x87NDvCCvos", - "ZTL6w0qsFl6rFgs2iaVTtQ4+K7LJ1hz0ppiLZzFroIx0basD8Tq9J2ecX5GGDtPwTvjOmA6YME/CNvsz", - "H2+bpin6stdMq/BCEltFjBJvMJazQKqyIU3d9mgXUNylX3DMg9dttvp+npNb/5y8lScCiagEfvRoxtMV", - "6y6rgYt02Tc63n2dV36/+7Mv5FMxQa+dvNbhdsJBSnZ1OTBiaWv17jCB5B5i+2ow5CTEeBt2jkwjL0vc", - "+8XGusC1dlmXg3hRTumoJ4sW004+4vMwQFTbXeK9rQ3w5GGkyr2UGAzbmcDkLI1LR4BzcKjg9x8YnLWg", - "lscfFJjQbqedrysR7JUyvorKD3QXaZVbYi0Erxq1JIvoVmJ2wfJRL+YifbaF5Qp8uQdQ0/AgYE+7pA83", - "BDNT0UDs9bCE+6avanzI7FHNkDu8MvbG8JUzTuKJyMXrZaiWMbQkWMobeVvaWSC8wXWqIseSGLgNa0iZ", - "baOpOTkCcUbnmHwOyDQY7MusWEwlt0e0xVepfDRrboy64e4ncoJ+woytd6jhbLJc2Is2JsPeR7l3s2d/", - "TH7W4gzGUK5ov6fQY6pw2b0BNlrP0YllC8qW3ZOcp8npVStRA0JRWpnq/JKFIZgAe2MG1fqKNYp0B8/B", - "j5sqdqDursr1h+Q2MFQY9u5t2FsWW7KR130c9DmxPXuyjGXjaJ/8MJRjPWj0MpSGvYxhyBiUj5/5Rym9", - "I6oRx4quDHbvC1q+tT2BhyfgonESy12cYBlo5WZnQAvQWK3a4/UAcRsvgmrvVk+JxtmHGm+6QtJh3LOp", - "FKAfpeiLG7lfyVIInq0jQipTJ9XwHWuQsxG15NZVNMK8TCIVr+CRZ/Q+lqvMoIwBqEU71nEhsHzgYQ29", - "5ZM2arAtA+8YXtkBBLgIRQkFTZYTXCv2uxv8Dkeq6uVCRL9HYGtu71haJ8bXT5tgWBR10mRW58fJJMDB", - "5NZdkQ0tPEmBe3cL2BukdijMdBLbJpwF2SsaAy6Q8oGnpvgpBhbFxYTkiMY+IVkqCJwLlUYaNEkpEJJH", - "MKaF5w+H0ec8jT7Y7AHj0BGE6LnzSxq1f9Y7PPXP2OOpf5C5PPUPkwdO/yj3TuqfDuPReemLKc1jlqvB", - "JzPYGPzRwfZYlC6fZhwqcyzIppadgX3HF++EXV+erJCUXcXDkTL7EENoHXOedFZIi/+kIhIHb1vpZNOo", - "BjQG1EW6MuOZz30+sqIS8fD39zjXVZc+9AlDJXMLFfuspJTXo2b9RAo38lRA9ZuOI/uoH+QbgpLbM8pO", - "CJ7A+GWyEnNekGQAxez5rDFivsKaxLSwmchvpEONtEBbYe6Dy9mVCG+yqdAijjMA9Kasu9niec1Hm7TO", - "xLoNJ/9GLT05DBnxHIT2SqwVWF76t9aIZ70YHzePvXUJs4x4jpI/djGyaWMkMeMdKrb4+UQd/eOZKsSl", - "kveAauOS4uFm52Le1jUl1y8hNnxCOHznOz9ya+Nti8hccPpA/Fc+ZobvncQxQfyhueYHjkMD6NR8splP", - "2hjHGJcbGc9pd+OWw8zy14w++DquBM4p0ZpEP/DUfQx27EkXGo8+EOV7WWWnXUmOnd+HfSSefMVKtUH2", - "CCN3t6r3nZUuHI6R31OgZGf8K02hqiM/7gVTgRjCTiQdfqU3bFgpVsBrB0abPD3CfQ0+I4DgtFPtjV1Y", - "fEBrxQoNyMy8YyQ2Wn6cJ7x6On86f0qFIJWRrZ6dzf40fzr/E5XA3yDoMO+JrLfanLapBAy+aa0vnTPU", - "4eFRV+6aSjlTua9BlRbIh5DiGmBiJ5AT0D8SWIYk3QP+Kn5N4Rb8NaY3vlVB1GqpMZ/SLxg4QUYE+oDi", - "eaXJ66QQcag4c+SfnwpW7cGZAHzBVi9rMGjBNF5nCx7VaP6Pp0/JIGYCB05h7UDSyJz+nXPgE9E6Wobu", - "xyvwZh+qQl5zyposWuUIMHP48M9Pn06NlNZwynnXsd9sj3sP9TWlLBiC5TsVOEPQR8LjgDQDIxSWfKHc", - "tXJio2TDmZnR4jDfXQc+P1lmZo2p9QztHw+4rOFAHz4cSDczSCMzp/bFnGkXqmyHid8inJySUUU2bibX", - "nFRhCBpSEO5CB1XCf2F/jIcCTE/q+Gb843YFL190TYzYhIfs63sdMqgDvId4XgTrFBIt9EVFotd5dHyS", - "0WWU2HdwzojuhU7eUHtKbg90A17VKqBxtFd1YC4YRIctJS8Vr3/8rhL/8/U331Xiu5ffCusgpdXruXgW", - "qM7Dn8Ur/RcSBsbYEZNE70UKnNbpWq+GG5IsIZfaANdQ8qygT//eqvV9v23NvT+9UZftXb89Am2/+mRo", - "i5CfwlbAohprTt8ZVavZ11/96b6Iffoe/ntZf9hHbxPO7D3Q/2KIs7uJAMlHno5s1ec7ZhDhIefN/foO", - "20WSHXkkzM7++n6mYTyOPSFWfEZ7OBtjc5WtabyCXyMajBLkZXgwNuik1HnCwpV+09eOZZVH5zlA55Vc", - "niWGUjlM5+4FpcnjfLzkiMcEsi8vSbNJBs2+9oNMn7fN7eBjjFq+jcIrCra6Rt8NYD7ZbgT78xfgWb+3", - "7PCnHHvM98NCClWDVp08J2DSOkPPG/jcqbaRS/4UtL+Yz7lEczlFIE19fIS+evrVLpgvKMFNCsfogR6z", - "bX3EXRb32t+aJXxeZE6o+KIX7OuCmak5wyKI4M8b29UCeiB92g3WbO9T/OqR9it0PgP/87i/9c52sCtz", - "38sug3OhGJQXsICHYW2yAY66IL6egqCqP8FOYdzASjlITs8/h8S6lOmg4ZBtp5bBghLEcIWI0RHEY1mR", - "DnP9O8Z+iyAdGmdhx3Xg8PqonYQNmosLqr/Yl4zlbRgnZ4Gv4Kjc7m7kd3Ej3/Ki7sbFw2Q/CcmWbrnR", - "1w9FlvsduxtpnpAaJjZXgGt1YXdREl9Zd/cNfrZcqjZwPofqozb6dVfa6GNO7T33+H7HFYvj3QML4nGl", - "utOTx5KE4BP07aKmc4GiI/5BtU80pz5Rv52LWgYZRYVvrrmGy/+8+OnHOf0ZU2r/IH04wScnL1+Ix0yD", - "6d1vnXK3IiHpEwySw5vrts9XjYkR4ox+oTIRqM6qqNoAWl44Mk3xyE4Jj5cl3suNkvX/hRgdv4gdRq1r", - "KvDSYGEKqhzBy6Cr3QDuKSfWlvuwNDAWfCncqlRfj7qYlQ8iXSn9URxAalaVsGo6ZyT3iRDtu0xGto/v", - "KqtNNEkVJj4FoJa+64tu/3qQtAI3RSh8QplYh+fuICUlFKVPP+IQaXNp302eoZcGy58HtfUjk7v4VjeB", - "+AfnmGmxRvEJCHbg2ZOC5B95Ae44Jfz6QfuAw00g12gLBsHjd97Aj9j7aN49jrHBFXFJuyOQ4hMoR3HE", - "iUijHSR6JcOSQkSg62S60y7FZvq5eEH+chhzgsmDsPF8B4tOSYA4QUXZtPLmO31NuptUtzB6TwrJ0Qi9", - "NALqb3D8oDl6DkjlRHmxggiGQ1fJyhafUqGWvP4MvKZsorKJA0AgZLHODkbdsmMlGbAkJn+IJLbPsCkb", - "TB3TL6gPZo5gHMcNJ+M7z6IU9ExTSPONeag6E2wH0grYYsnkSq7LCYxIxynbYpxS1QNc1ApuoIEJNwGN", - "5pkihwZQJgOvLx1eKuryDDf+YSQEROtYGJMj8D+zLgmTzU7wtBFR7qNIevr1nVr/532p/OkAMuXD+Yx1", - "AHmpJKINmmzuWFISbS9zERs71BgMat9B06gqQHdQjFihqgWAYSbLzpU5TzslOhNxtoRohAF1vCgeGNPq", - "h9Cyf3oqDgjIcSyJNn+5WPgepnhA+dkv/wFNGhmMyzCFifYqxf5GfBjBlcByP33iELKntfZwSPGYP8i4", - "1QT5eEEDq91Eg/1dg6eb7nNSYaGXViIexlKOY8dZ28FHnYq9QQ+1UzecsToWIDKYaLapz7nrlN4gCtyk", - "JpOhnwvIPF0o0RdeQE9f/ijc4y3kEz3/XEd0q/Ydy1fqYQ2/yk2BI+pDUTodGHlL1lie50OYYFNEyGe3", - "vyp3iPYzcO6r/tyq0xj6NG1Av8lc+/NNwQiNG3k7FxQoBb9HgVJkVe+ZfZPYhaukuy7FSlUiKBpIB0wS", - "q5dX2BhNr42+Us1tKhyJNo3OGW5uzVScFvPsseR3CY2eATQeBpGw639KFOLoqpPk0TqNS2EQz5Zj0yO/", - "L7yttFcspL7imK6H2LPBEP+Ue7eyy/1OTd9igwdcJw1QWOgvMR56hCZOrzdBtNIo8mTb5wEUjWx9Lg2q", - "bBHNmH1fj1KZj4pD2pwI8rJoHOth8ulRLgPH58O1yT3InCYQUx5YzBigJqhcD2n83+6iB3zGqvGhArPC", - "UAQKSrjZ2IaSx2P6e0yVjur8mFPe//tXj8ImT+WaKZT4p1e/oTEge7NbxZtfas9B50/m4rXEdBvLK1B1", - "tdKnlPQ4zWSsgO5l9KGe0p/+oGS9V0M/0mVSb59Inc71rfqutvId5Xj689On1f6MT78+IDojQF5jwN4u", - "Sv+MHq44d/73MV9g4s9PIQV903l0aYCXnEO7TzNe2NF5CW+jx8UejzZ01kjOa1EVWcJmZJqqPpO4pMex", - "VlohcxKzZKkkT54HHR7GQbnmBJX5YUUOhgeD4wcifwHvuM70D2QSeQgiyCPcRen3H5+OUQNoc1W7KZo4", - "gH8O0BsAYrDCugRbLAv2MXc0IpSxQU3Twam7skcfTjONxSewHhP7ndxyVpqhqSetkSpEhc1QLbLFqKYY", - "o7lWAeup+/kkifrRUq29h1ejxdGO1aIhXOeTED99D/+x7grzBqldNukFPk8jH2MMp0/qB1Is0Zzvp1iK", - "y2fsvvvdG4vG0OdjzHpGmYzJ6Qt+Ahy4KompI/9PaePehWmMulCfMfxgSBKOQ6wIgDFu5Qkq/CHJG1z/", - "QBZKUmzVh7Li5VVRZTfy7HMyKEp/g0ddr24LQrthcx/q6lBaB6bIT0lZu/k5Hobkl/KA/BPKWyBqnCSh", - "a6/MkutdSHbRHok17KU2I/mF9CDZN7eUe6MzeK+jJye8ynmDWzS/ZF3qUPU8A9cOkhAte61ItdLcxpGX", - "0nPWYMrWZW0jaB2XJWS6UOG1NOohJae+/z/QoXCrTjk5/AkGluyNF+prtn40GZuq7XxMQYnd+LhR6kDr", - "yEM4L1e4Q1iAv3CIN8w50+qEXRFObfR6Q96Qv2G7AV3MC2EXwQVk/3Vs9FkizmiwY6j9s6aJuoOkjLC+", - "sAjiZ2PPD3QKqPc/xl6egDZtMmdAfcQRi6A+fc+/DlgWc3g/0N1xYN3DNX9qZi9B4X78Xgmcp1h59iTP", - "T/9g86gOqIuHJXfJrIdsCyai4tuoL8FRzqT6CFXKygcuqxETL5ICAIcAR8rVXNAdkJSBc776q5w7TaV5", - "U+WcQeexnN9G1hiwkJx2UvPeTw5ztGMl/I3dxolgAgFfxbJaax0GSYp3btZn8FmWS/4B6Uoa5TNzZwdO", - "WGTQ7ktd7qumLB4eSkD2ZZyeUYa1mKIUHvoRKkaHMN/H4qAaCP12zPDExB72HZkiqlKZsP/G1S8FV9HI", - "tpfreostPgfLRd52x0nXPdriCqpIgTk72Jd00U55zhCLNiiMk5VuStWP0mHslzwu6YTtddjEij2puouP", - "qmPrhgVpqthG9uXFZHbL4qC66C9D08a9epjTmxVc+tdz9zx0XLnyxaRCDg8a7GdW1ALvAWtTfp5pRNIm", - "Rj5xmsQdZhpLbzwg7cT+9zjn+XNB5TKE9mIlG696rqpQ3MPYCIh/IOYbNrknzH8YXYp1hdDTPK/eWAmH", - "N340W8KzaDhKFSEHDHPvBT8oTRLLRI184Ht2Oa+0nisfNtL3scLQUzl8SbqQCqc8EK1KZT7+2zP9WMLG", - "vOPpe/oBGL/SWPjWn77XplbvPhyH9/H7O6F9VQ73xPHBB8R61KMlBSuO8cjHJjE/6lOgjoU54fyPmdDA", - "En7Ad0XmxWBkXTuFbqIyTorSLNbWxPmCGx6GHfcudFldGStW+h2Wo74Vem0su+aWXV1ohAsOgXoQj5ds", - "iD/G05KEkqnjRCD9bPd+tBudvudfB9Rb0Sz1gPA5xiLOk32gazaB4n7XbAmmp2yHPI7W3HMCU3fsM8qA", - "rCg8hjtPlTHpyoUQNbtaicdrK5sKE7qxRRUDEuGJrAXlFhg43qORsFH+CZh6FAeHoU6eGod+u4T2yQhL", - "XiCYmiO9tlkNzrw4ZrrG4yTLJTytyePZVd2nrlUNJ0u2Td2vv5zbA7cpR/MHyEZBg3y5DicRzriNWdHs", - "zxYmUDxC2gTlXNf+MYfom5hLnqsTkcPSDgK9jJOcpJSfdysT0HAfU2jmH7uTA3+1z72RF1EXgZwrYngK", - "p02OVVHKyygXEM8UfcD0KZFS7Ef76LtXjf3efutUp1IKwGVM8swBCugM2nvCgXUbrJ2oSZG9f8oEyoFv", - "H/uw/at6yCH4NzuE6ksTRoLUjVFhr04l5fMHs9OjWPUEvxMGM2S/DDGNCLpI8IYPY2E4+3hjbYu+v8zH", - "g+VHUa0Uin4zNhl7KuFtKvyTX8Wv5BIdM7xe01OYhtAmK7yiOUhXXoI+x7CWESddumVBtUNvH1S7w0NM", - "+uLVAF+E93y4S/7Kn76H/w5ww0kv+WArmJa7M5n7k6c4wpXfj/kdAu+UqmIfRebvM+oUjX9tPeWToDz+", - "SeMNs4kUlVLYnLF6yae0/NCjYqf+6KrMVz+8gl4n0pAPnKaRJFG2ZOK7o9MhHhRqE52h+mTzu5dA6fzA", - "8t7aWAD8ISg+9g3D3IXmf12maAi1G+kZ0qp3AP9iyfQAh2NF9ymq/U1f8Eqb3MCSrCqPU9XPihLI15Xo", - "DP1CYHQmOLm8gucwL7VEJJqvdUCViXrCNSmwvAQsQmZpQLim6sml9Klkbl75NaZhnNbER4ON12bJzM9a", - "XytDrtlxGakoeK3e9YHRnCoE80z1WT5wDrB0zSql+IoT/itZH14IJdrcYsFRXsPUzGl96lrbzudj4Rxw", - "QVjxAGbjOjV5J/mrvpT+iGKNE2fyxIKNy6CJ8OLiPvQLGpZiVW4eucMYgbMBNzXxow1CNo29iWCJi0kq", - "wHG+INiz2Z2UkM9yCJmzVAGVsKav36/NcO+Ek+zvyYWUMNNYXim3OEH+erZXL7kzyQFCnQ2BXNxtKuiU", - "/qL9ASWrdFHN0av/rVFPJiccVPux2bc+0iDJSFi4+l8rd4JWMx9k+NIScXxm5mJpT2QXNtbtDxtATbYX", - "doi7dP3HPLRJVBsQ8OCkod7EY3Rs7k1LMtM7OXS+gnse05ntpEGidIzkCA9QeFKl4odMJ7Qbm6WexULZ", - "t/ls15jbiusLqTj53GULcupGsKj6BD53UjflnHuQsdM+YxB+Fj9X6YJe6laacKzvRb/JXz63C9nF9qJi", - "Z/RKqxrTkMHWwybiab5U4UYprIEpPfGJvT/GenA1zaeurhcw+nFxmVR9/U52rFI/MMVP0Q9vyEf3Y5t6", - "wX3tFRoeLrRZNwq3YcqRSIblhvH4X5Ro19pNHhEopdInwbWrEjNdpRZa+RhrHTBU9pZT3X5jAr6Ev2rk", - "ojH0pFGrIGwXzkXiq2uhuC000GbZdFE+2Up3BYztM9i0DTFCjZLXrLnrpxM2znbrjZjPKQ+lv9022lxV", - "qRh/OdNYOrTuEK950Ui/OfEKGgUSERpJ7gl2MBf0u4muk9ZxxHDifODtFM/zh56aF9qB79+elM4JKR75", - "uGP/6qcIa1d+fs0KZdfMziVrQeDkqhr2n4MFATlvnA4qNY5BOlXU0sYEPCSgUaLkgbodEx5ow7r6QQS6", - "jjHGBoRjUwubqqJKL7y1Jup5Yjh9ZpOMhTqZJYQXsiYBXWyVNOgLdC5qZ1uKzsfQamTIzmPk29jSCSHO", - "mEZMBx7FKQhl80WNAHTC4nlMXAbdwzwj8ek/FKmiKgnk3BmpU6oIVvyQszQ7tYViQiPnptSf9nG0Prno", - "pYLB00jM1vroCeW7S/jUn1OlbuyGSvpiideuFStgM5E6EUUNm/RuIhMajFRWof7HZ1OhVpx0gZZZ8ZoY", - "DH+I7XN00o/ISbIrrXDlSgh/G4dEUz6QYSoQqqGqfqM8IZIzkcPdBwlah12PbfKEUKQwgNZc6CgWdwOk", - "pHs1DpYUGhiNkjStdjV4O+pSBvEYuvIW8I9cFuCVsCv+9eScU6vwEbhSqo3Bx1w4fqHrqcwmgCZ3yWzy", - "CfOEYwh1ObHJV08fPLPJUWIbQuYYgY3SneCK4j5k+U6ePvnyRTiQxvaUy3Fq3TXSkdBW1vbOxVtQ4CP3", - "B7cDKADoKAivf+eQ+XNB9Q6wo9hSGBsLCz043wmSyqdlPA+wlnfiPR5UQHvOHU3cCrAj/9rMJZbQmj4E", - "qSoenIEUMLIjpgGRfiXdVW1v+AvKG0EfyxDkcrOlIhWI6ZTiyDZdUIDQU7h2zpebU94213CpMOKj2joW", - "1NPG61oNPpyPquxVWGMPp3Tx/35XiSXcHihFqndBGfQvnD4/L7fkbnFMKYFPfwD+Ucuy+ev1v7/bNn94", - "VTdA3VjV7V5n/U//mpQBRaDPL3SmAFMmMjiNBdZTTpnMrFlpB/oefMwqoSzBD7qysE86CnzczYk2VZLB", - "NFY5u9YBzzv0DVYmdh7FjotersIAGjZNzEjrf+tkKuQ/Fv20CRbl5WhBhG7rIUM9WqM1h2LyWN8/MhfE", - "IntFgwASaDIlDBRtvcK9Em2Hy9CcZQwqRVRiBWV3VtbdyJjssp8Ylo0GSAYb87uYgSBckoALEi+uvk7u", - "dhyVg6/0ABgDWTZZXFM6tGiOfUzgJDe8BZavEF6FJ3ncEAr1xvY9aKxUvXbK+/Go0R7NMBvpLUoJzmgn", - "Nags38VisOiL1Zmgm37ly8ZiVZRRXNKuhyFr9dJsoyAP6051nzBdGllAKxabWJJH5zECGT/pQ0IZpqom", - "ZIVTQX2MQkcZulQWklokjRDMyF9R1ROeYxCPaQOEXrHXWMwT+KRKII3zI0Ckc1rHOg/5LLUZDv39N89e", - "xCBEZepoYrGrYTPOxMh4s4BFLniBhBf9ppgnJS1KD7HRtgisnJZvBw8yhuRcfKMZp3GPeqZ9rKwpKGGi", - "c4S9MbzTyBMtYjwOHBcYkxH7VoWzcam35IvDJyPvYHA4trJWFenStI+EJcUA40NaQtV7zXI3Q7rC2UqT", - "gM+pxXzSxvVBT0OvA+1ZR5PlPKUaH2Pon2daQNoFUOG1kbrKXI9hbMii/x5//fQ/n4jONHDWs7QFWecM", - "adArLDAC4jFvHaja+DQZy0GHOTifVLv6Uq73wykyJWn/mESOIkuB5PbMLmgj4a5r9DL4dGiMaKSpewor", - "vBUryRcA3yR2gRPl6ya/Xh5n0wNmPX+HBYHIA6pGFUsfmnHrKbt5rpPk6Ml1qiqHVDiepvzOPGPlTKoW", - "i6SBN3aAvtQOPvTo/ApKRQfQ80EixmCGp9vFyroFTn28rTG0c2xS4mlmZIgcirC5zq+WcmJSt/7YyPRy", - "Ii2MNK5mbfbo/SzHqF0OCSdDFDLXeQ8P4h1OYObGZOzNfFbtOMFUs3QSdqfzQyTmOTLCs6iVIySCjwdj", - "Ac3v4wN3h8wwc8J7Gjmv3vkHm57hJdBozFvHFdX/j/8Qy410chmUw9uWmFQylvXner4rUVQzer87AfhA", - "PM6O8JOzOCFrgs0ZiB0ebs73wlm8LNMHhYv3nDy/B/c5zFQZUBP+FSdCsqkyYfbreAnV7N0JND25lg7L", - "JcM3jM6wslf0efbkNff0oZzB7fMFA+xT5xPgMpV9hdSnwgs9WHqaZd6Ot6V14nHC5ScUWE7ZmL/sWIJd", - "yewEF3eSE4mYQ3bXua528sYLKYh07Nxascob3727pWGh0Xmi1Yll3HVPeS7NUjU4zDMYgANg/yC/+cgD", - "AZ7cMBjMl6+XjrLq5xe+U/TogIFJXP0+otZTscwfeZKbYe/4tTKIPWYt+KeqSU0OVapNEFmXj/wgphXP", - "cso1M2UvPihbTd72L//wcD6GeTku8wsnUS3RjP22zNa6UEA39a61PvLx9McCmlaZ148kmdBTJfM8ggla", - "CtNhGvE+siqPqqqEBdt928AV/v3bt6+rsVzey8TWRfuwmrQnvo5r/TzZQzmlwLHZrGhuXz7RQ5EA0nS0", - "XdjJij5amLxSnu3XMdoyOvU+ptj3mGfKk/89N0Iz5ZO8rEYmoWkuWNrHCOGnnck/7ulJ4r/jyBssqaFM", - "rAALSFZCmXgf52Gcx4TWxHE+7jL7QpTO1ftyjhPc/L09Hba9T2EYSo778KqvOJhQg4IGi5In3j/deq18", - "WDCNSGGVkYr0BQjp42D7b/mbBfehoOggfjintij6RrxLWFnKpsLzfoPr+wNYrQi3+sunMsdcS8nlCptW", - "KFC2ymURTwONb/FOePMZr4Q3d7oR/mEuhEKZiF04f/HFGgaczUTpii9nKw7lUTDDhUzFQPaRtDpEoWHb", - "hmEul3KCA4pBfdicLNz7H5MK8hjOP/OA/cdg9yNpPIqwpsZj01nk6mU0l7Ius8crD9Yk1cfg3iinUpTt", - "lQIUw9ry/hT4/tIsT9+bD8ex+6QAd/YmutgmdZIMU6LARYLD56BHcbRjaVEC/D8A8Y9Thf06BZ/PB1OG", - "lFlRc/dUewdyXsbkiex2XMrIS1rL5GaVRe7SgWBv9phlYEiLUYcHCKZNp/y4A2q7tOZaOS+paC6SZx+s", - "Tclch/G118rU1rGhKA6D/rdkB8rI1KOcqseUJYMwR6oCh3bRfb7qaAnmpPis845zipaLPrUArin2L4Po", - "SyaWMwx/a93VRR+i/N85NA8T9qzkzIOqI4/KU1kuC5MH8fb1YbTy7Ivr1W/gYotVLxlHGg6Ct0Yl431C", - "K84BEdXdIJZhiRmqLq+wCBbDBUW1Cs5H749DpZx2dIrSXWVleR6Ks8lG+Oy1iu6bl3KEckG5rTay2eOZ", - "/Yu6vLDLKxXYtZQ8T3I84D4qIUVj19oIv1FNQ5WocsfVLDppaRvwEqQbX69QfTxwCcArFBSOMYCbWNyb", - "iJ1xUMbMpTKhuRWtg1uiZvcoGN12oe2YGQbCrjxE5jTwgB3Fo52cxlw2Wpk4GqpICk1ZdQVe5+hHg2W5", - "0suNbWpKQ/OWJ3mhf+/92xSDR73T0YOO1sl+OUh5l7ZW4uunT58m7x1oDhAMnU8BWEr6vb60cfzj3Glh", - "T/aSkr3xEhNBGLDB9+907LD71dOvdlH0AmtB92jZI2zrbLBL2/xre513hhjuw2VHx6IlYDLfCOhgNWWN", - "qq2CUjnFAPqf4+gPWfQvjjFVq4Pfx9tm/k+2w3mNmkmBMCsewxzgytnfU4woqro5cqzKctUAJZ2LH2RQ", - "TiisawQUibS6Phcase/aQskkAvMEX8i0aVA85gF1qMPyMbtgQSCYtIIvX1qLMz11ndlv8YsutuNNcp3J", - "bMcKlOZeBHsWneS0oeTpQ0TpxQT0bUZFAMUWBxlUDJJ1t/QJEg0Zgtq2cOUncQr65MeehTO6orPsK5FP", - "ANlsAoPirr7pHjRldj7MlIq3A3kswvoLRJ+47ft1u7+kVp9DmRJHYxb6GJ0KJoNLOPyIklAwLUrteqt1", - "zvMmCJy+h3dHFIGeJk57ikALIDd8XRJp1SGPll3a9vZe+DGVJfNzkNA4xotEFqYOQrY1PQ15oPOA/30K", - "qTdqjDAurm3kMoumyEsoDdZE+hhAP+2TByRQ2a9EsOKrp08HHpAZVkTpl7BjMM4YT8gnmgK+dRBOwXh+", - "NK9gcRoVl5jGGD1jMU4vyCtliGTH6jQD9+ub2P3gAS2K3EaL+TG7HOmOkClghMPpTz69bD6Ftp+vGsUO", - "iZtQEwcMLxqxH1+MourDh/81AA==", + "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", + "h/81AA==", } // decodeSpec returns the embedded OpenAPI spec as raw JSON bytes, diff --git a/internal/ids/ids.go b/internal/ids/ids.go index 4787bf8..75124d6 100644 --- a/internal/ids/ids.go +++ b/internal/ids/ids.go @@ -29,6 +29,8 @@ const ( Client = "cli" ClientRequest = "req" Transfer = "xfr" + Terminal = "trm" + SavedCommand = "cmd" ) var prefixes = map[string]bool{ @@ -36,6 +38,7 @@ var prefixes = map[string]bool{ InboxItem: true, InboxResponse: true, Blob: true, LeadNote: true, WorkflowDefinition: true, WorkflowRun: true, Send: true, Client: true, ClientRequest: true, Transfer: true, + Terminal: true, SavedCommand: true, } func New(prefix string) string { diff --git a/internal/server/commands.go b/internal/server/commands.go new file mode 100644 index 0000000..3ed5a0b --- /dev/null +++ b/internal/server/commands.go @@ -0,0 +1,52 @@ +package server + +import ( + "errors" + "net/http" + "strings" + + "g.fc.run/ai/byre/internal/api" +) + +func (s *Server) ListSavedCommands(w http.ResponseWriter, r *http.Request) { + cmds, err := s.store.SavedCommands(r.Context(), actor(r)) + if err != nil { + writeStoreError(w, err) + return + } + writeJSON(w, http.StatusOK, cmds) +} + +func (s *Server) CreateSavedCommand(w http.ResponseWriter, r *http.Request) { + s.saveCommand(w, r, "", http.StatusCreated) +} + +func (s *Server) UpdateSavedCommand(w http.ResponseWriter, r *http.Request, commandID string) { + s.saveCommand(w, r, commandID, http.StatusOK) +} + +func (s *Server) saveCommand(w http.ResponseWriter, r *http.Request, id string, status int) { + req := api.SavedCommandSave{} + if !readJSON(w, r, &req) { + return + } + if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Command) == "" { + writeError(w, http.StatusBadRequest, errors.New("a saved command needs a name and a command")) + return + } + c, err := s.store.SaveSavedCommand(r.Context(), actor(r), id, req.Name, req.Command, actor(r)) + if err != nil { + writeStoreError(w, err) + return + } + writeJSON(w, status, c) +} + +func (s *Server) DeleteSavedCommand(w http.ResponseWriter, r *http.Request, commandID string) { + err := s.store.DeleteSavedCommand(r.Context(), actor(r), commandID, actor(r)) + if err != nil { + writeStoreError(w, err) + return + } + w.WriteHeader(http.StatusNoContent) +} diff --git a/internal/server/server.go b/internal/server/server.go index 3c09e36..e70e13f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -54,22 +54,21 @@ func New(dataDir string, portFirst int, ts *tsnet.Server) (*Server, error) { return nil, err } - terminals, err := terminal.New(filepath.Join(dataDir, "zsh")) - if err != nil { - return nil, err - } lc, err := ts.LocalClient() if err != nil { return nil, err } s := &Server{ hub: newHub(), - terminals: terminals, tailnet: lc, hostname: ts.Hostname, startedAt: time.Now().UTC(), users: map[string]string{}, } + s.terminals, err = terminal.New(filepath.Join(dataDir, "zsh"), s.terminalsChanged) + if err != nil { + return nil, err + } s.clients = clients.New(s.clientsChanged) s.store, err = store.Open(filepath.Join(dataDir, "byre.db"), portFirst, s.publish) diff --git a/internal/server/terminal.go b/internal/server/terminal.go index 488eacd..5eb4f6f 100644 --- a/internal/server/terminal.go +++ b/internal/server/terminal.go @@ -18,16 +18,78 @@ const ( terminalExited = websocket.StatusCode(4000) ) -func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID string, params api.GetTaskTerminalParams) { +var newTerminalSize = api.TerminalSize{Cols: 80, Rows: 24} + +func (s *Server) terminalsChanged(taskID string) { + s.store.Broadcast(api.Event{Kind: "terminals.changed", TaskId: &taskID, Actor: "byre", Payload: map[string]any{}}) +} + +func taskTerminal(term *terminal.Terminal) api.TaskTerminal { + return api.TaskTerminal{Id: term.ID, Name: term.Name, StartedAt: term.Started().UTC()} +} + +func (s *Server) ListTaskTerminals(w http.ResponseWriter, r *http.Request, taskID string) { + _, ok := s.activeTask(w, r, taskID) + if !ok { + return + } + out := []api.TaskTerminal{} + for _, term := range s.terminals.List(taskID) { + out = append(out, taskTerminal(term)) + } + writeJSON(w, http.StatusOK, out) +} + +func (s *Server) StartTaskTerminal(w http.ResponseWriter, r *http.Request, taskID string) { t, ok := s.activeTask(w, r, taskID) if !ok { return } + req := api.TerminalStart{} + if !readJSON(w, r, &req) { + return + } + name, command := "Shell", "" + if req.CommandId != nil { + c, err := s.store.SavedCommand(r.Context(), actor(r), *req.CommandId) + if err != nil { + writeStoreError(w, err) + return + } + name, command = c.Name, c.Command + } + term, err := s.terminals.Start(t, name, command, newTerminalSize) + if err != nil { + slog.Error("start terminal", "task", taskID, "err", err) + writeError(w, http.StatusInternalServerError, err) + return + } + writeJSON(w, http.StatusCreated, taskTerminal(term)) +} + +func (s *Server) CloseTaskTerminal(w http.ResponseWriter, r *http.Request, taskID, terminalID string) { + if !s.terminals.Kill(taskID, terminalID) { + writeError(w, http.StatusNotFound, fmt.Errorf("no terminal %s in task %s", terminalID, taskID)) + return + } + w.WriteHeader(http.StatusNoContent) +} + +func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID, terminalID string, params api.GetTaskTerminalParams) { + _, ok := s.activeTask(w, r, taskID) + if !ok { + return + } size := api.TerminalSize{Cols: params.Cols, Rows: params.Rows} if !terminal.ValidSize(size) { writeError(w, http.StatusBadRequest, fmt.Errorf("cols and rows must be between 1 and %d", terminal.MaxSize)) return } + term := s.terminals.Get(taskID, terminalID) + if term == nil { + writeError(w, http.StatusNotFound, fmt.Errorf("no terminal %s in task %s", terminalID, taskID)) + return + } conn, err := websocket.Accept(w, r, nil) if err != nil { @@ -37,12 +99,6 @@ func (s *Server) GetTaskTerminal(w http.ResponseWriter, r *http.Request, taskID defer conn.CloseNow() conn.SetReadLimit(terminalReadLimit) - term, err := s.terminals.Attach(t, size) - if err != nil { - slog.Error("start terminal", "task", taskID, "err", err) - conn.Close(websocket.StatusInternalError, "couldn't start the shell; see byre's log") - return - } term.Resize(size) backlog, out := term.Subscribe() diff --git a/internal/store/migrations/087-add-saved-commands.sql b/internal/store/migrations/087-add-saved-commands.sql new file mode 100644 index 0000000..fee1d87 --- /dev/null +++ b/internal/store/migrations/087-add-saved-commands.sql @@ -0,0 +1,7 @@ +CREATE TABLE saved_commands ( + id TEXT PRIMARY KEY, + owner TEXT NOT NULL REFERENCES users(login), + name TEXT NOT NULL, + command TEXT NOT NULL +); +CREATE INDEX saved_commands_owner ON saved_commands(owner, name); diff --git a/internal/store/savedcommands.go b/internal/store/savedcommands.go new file mode 100644 index 0000000..98181ed --- /dev/null +++ b/internal/store/savedcommands.go @@ -0,0 +1,83 @@ +package store + +import ( + "context" + "database/sql" + "errors" + "fmt" + + "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/ids" +) + +func (s *Store) SavedCommands(ctx context.Context, owner string) ([]api.SavedCommand, error) { + rows, err := s.db.QueryContext(ctx, `SELECT id, name, command FROM saved_commands WHERE owner = ? ORDER BY name, id`, owner) + if err != nil { + return nil, err + } + defer rows.Close() + + cmds := []api.SavedCommand{} + for rows.Next() { + c := api.SavedCommand{} + err = rows.Scan(&c.Id, &c.Name, &c.Command) + if err != nil { + return nil, err + } + cmds = append(cmds, c) + } + return cmds, rows.Err() +} + +func (s *Store) SavedCommand(ctx context.Context, owner, id string) (api.SavedCommand, error) { + c := api.SavedCommand{Id: id} + err := s.db.QueryRowContext(ctx, `SELECT name, command FROM saved_commands WHERE id = ? AND owner = ?`, id, owner).Scan(&c.Name, &c.Command) + if errors.Is(err, sql.ErrNoRows) { + return c, fmt.Errorf("saved command %s: %w", id, ErrNotFound) + } + return c, err +} + +func (s *Store) SaveSavedCommand(ctx context.Context, owner, id, name, command, actor string) (api.SavedCommand, error) { + c := api.SavedCommand{Id: id, Name: name, Command: command} + err := s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + if id == "" { + c.Id = ids.New(ids.SavedCommand) + _, err := tx.ExecContext(ctx, `INSERT INTO saved_commands (id, owner, name, command) VALUES (?, ?, ?, ?)`, c.Id, owner, name, command) + if err != nil { + return err + } + } else { + res, err := tx.ExecContext(ctx, `UPDATE saved_commands SET name = ?, command = ? WHERE id = ? AND owner = ?`, name, command, id, owner) + if err != nil { + return err + } + n, err := res.RowsAffected() + if err != nil { + return err + } + if n == 0 { + return fmt.Errorf("saved command %s: %w", id, ErrNotFound) + } + } + return emit(api.Event{Kind: "command.saved", Owner: &owner, Actor: actor, Payload: ToPayload(c)}) + }) + return c, err +} + +func (s *Store) DeleteSavedCommand(ctx context.Context, owner, id, actor string) error { + return s.write(ctx, func(tx *sql.Tx, emit emitFunc) error { + res, err := tx.ExecContext(ctx, `DELETE FROM saved_commands WHERE id = ? AND owner = ?`, id, owner) + if err != nil { + return err + } + n, err := res.RowsAffected() + if err != nil { + return err + } + if n == 0 { + return fmt.Errorf("saved command %s: %w", id, ErrNotFound) + } + return emit(api.Event{Kind: "command.deleted", Owner: &owner, Actor: actor, Payload: map[string]any{"id": id}}) + }) +} diff --git a/internal/terminal/terminal.go b/internal/terminal/terminal.go index ea75cf3..359039e 100644 --- a/internal/terminal/terminal.go +++ b/internal/terminal/terminal.go @@ -2,12 +2,14 @@ package terminal import ( "bytes" + "context" "errors" "fmt" "log/slog" "os" "os/exec" "path/filepath" + "slices" "strings" "sync" "syscall" @@ -16,6 +18,7 @@ import ( "github.com/creack/pty" "g.fc.run/ai/byre/internal/api" + "g.fc.run/ai/byre/internal/ids" ) const ( @@ -25,6 +28,9 @@ const ( promptMark = "\x1b]133;D;" lineMark = "\x1b]7701;" carryMax = 64 << 10 + keepSize = 256 << 10 + Quiet = 500 * time.Millisecond + MaxWait = 10 * time.Second ) var lineMarks = [][]byte{[]byte(promptMark), []byte(lineMark)} @@ -52,12 +58,15 @@ fi type Terminals struct { zdotdir string + changed func(taskID string) mu sync.Mutex - byTask map[string]*Terminal + byTask map[string][]*Terminal } type Terminal struct { + ID string + Name string ptmx *os.File cmd *exec.Cmd Prompts bool @@ -80,7 +89,7 @@ type wake struct { out []byte } -func New(zdotdir string) (*Terminals, error) { +func New(zdotdir string, changed func(taskID string)) (*Terminals, error) { err := os.MkdirAll(zdotdir, 0o755) if err != nil { return nil, err @@ -89,23 +98,50 @@ func New(zdotdir string) (*Terminals, error) { if err != nil { return nil, err } - return &Terminals{zdotdir: zdotdir, byTask: map[string]*Terminal{}}, nil + return &Terminals{zdotdir: zdotdir, changed: changed, byTask: map[string][]*Terminal{}}, nil } -func (ts *Terminals) Get(taskID string) *Terminal { +func (ts *Terminals) List(taskID string) []*Terminal { ts.mu.Lock() defer ts.mu.Unlock() - return ts.byTask[taskID] + return slices.Clone(ts.byTask[taskID]) } -func (ts *Terminals) Attach(t api.Task, size api.TerminalSize) (*Terminal, error) { +func (ts *Terminals) Get(taskID, id string) *Terminal { ts.mu.Lock() defer ts.mu.Unlock() - - term := ts.byTask[t.Id] - if term != nil { - return term, nil + for _, term := range ts.byTask[taskID] { + if term.ID == id { + return term + } } + return nil +} + +func (ts *Terminals) First(taskID string) *Terminal { + ts.mu.Lock() + defer ts.mu.Unlock() + if len(ts.byTask[taskID]) == 0 { + return nil + } + return ts.byTask[taskID][0] +} + +func (ts *Terminals) Start(t api.Task, name, command string, size api.TerminalSize) (*Terminal, error) { + term, err := ts.start(t, name, size) + if err != nil { + return nil, err + } + ts.changed(t.Id) + if command != "" { + go term.typeWhenReady(command + "\r") + } + return term, nil +} + +func (ts *Terminals) start(t api.Task, name string, size api.TerminalSize) (*Terminal, error) { + ts.mu.Lock() + defer ts.mu.Unlock() shell := os.Getenv("SHELL") if shell == "" { @@ -123,27 +159,49 @@ func (ts *Terminals) Attach(t api.Task, size api.TerminalSize) (*Terminal, error return nil, fmt.Errorf("start %s: %w", shell, err) } - term = &Terminal{ptmx: ptmx, cmd: cmd, Prompts: prompts, clients: map[chan []byte]bool{}, started: time.Now()} - ts.byTask[t.Id] = term - slog.Info("terminal started", "task", t.Id, "shell", shell, "pid", cmd.Process.Pid) + term := &Terminal{ID: ids.New(ids.Terminal), Name: name, ptmx: ptmx, cmd: cmd, Prompts: prompts, clients: map[chan []byte]bool{}, started: time.Now()} + ts.byTask[t.Id] = append(ts.byTask[t.Id], term) + slog.Info("terminal started", "task", t.Id, "terminal", term.ID, "name", name, "shell", shell, "pid", cmd.Process.Pid) go func() { term.pump() - ts.mu.Lock() - if ts.byTask[t.Id] == term { - delete(ts.byTask, t.Id) + if ts.remove(t.Id, term) { + ts.changed(t.Id) } - ts.mu.Unlock() - slog.Info("terminal ended", "task", t.Id, "exit", term.exit) + slog.Info("terminal ended", "task", t.Id, "terminal", term.ID, "exit", term.exit) }() return term, nil } +func (ts *Terminals) remove(taskID string, term *Terminal) bool { + ts.mu.Lock() + defer ts.mu.Unlock() + i := slices.Index(ts.byTask[taskID], term) + if i < 0 { + return false + } + ts.byTask[taskID] = slices.Delete(ts.byTask[taskID], i, i+1) + if len(ts.byTask[taskID]) == 0 { + delete(ts.byTask, taskID) + } + return true +} + +func (ts *Terminals) Kill(taskID, id string) bool { + term := ts.Get(taskID, id) + if term == nil || !ts.remove(taskID, term) { + return false + } + term.kill() + ts.changed(taskID) + return true +} + func (ts *Terminals) Close(taskID string) { ts.mu.Lock() - term := ts.byTask[taskID] + terms := ts.byTask[taskID] delete(ts.byTask, taskID) ts.mu.Unlock() - if term != nil { + for _, term := range terms { term.kill() } } @@ -151,10 +209,12 @@ func (ts *Terminals) Close(taskID string) { func (ts *Terminals) CloseAll() { ts.mu.Lock() all := ts.byTask - ts.byTask = map[string]*Terminal{} + ts.byTask = map[string][]*Terminal{} ts.mu.Unlock() - for _, term := range all { - term.kill() + for _, terms := range all { + for _, term := range terms { + term.kill() + } } } @@ -349,6 +409,70 @@ func (term *Terminal) Activity() (started, lastOutput time.Time) { return term.started, term.lastOutput } +func (term *Terminal) Started() time.Time { + term.mu.Lock() + defer term.mu.Unlock() + return term.started +} + +func (term *Terminal) typeWhenReady(text string) { + backlog, out := term.Subscribe() + if out == nil { + return + } + _, err := term.AwaitPrompt(context.Background(), out, backlog) + term.Unsubscribe(out) + if err != nil { + return + } + err = term.Write([]byte(text)) + if err != nil { + slog.Warn("type saved command", "terminal", term.ID, "err", err) + } +} + +func (term *Terminal) Settling() bool { + return term.promptWait() > 0 +} + +func (term *Terminal) promptWait() time.Duration { + started, last := term.Activity() + wait := time.Until(started.Add(MaxWait)) + if !last.IsZero() { + wait = min(wait, time.Until(last.Add(Quiet))) + } + return wait +} + +func (term *Terminal) AwaitPrompt(ctx context.Context, out chan []byte, got []byte) ([]byte, error) { + for { + wait := term.promptWait() + if wait <= 0 { + return got, nil + } + timer := time.NewTimer(wait) + select { + case <-ctx.Done(): + timer.Stop() + return nil, ctx.Err() + case <-timer.C: + case b, open := <-out: + timer.Stop() + if !open { + return got, nil + } + got = KeepTail(append(got, b...)) + } + } +} + +func KeepTail(b []byte) []byte { + if len(b) > 2*keepSize { + return bytes.Clone(b[len(b)-keepSize:]) + } + return b +} + func (term *Terminal) Write(b []byte) error { _, err := term.ptmx.Write(b) return err diff --git a/mac/Sources/Byre/Settings.swift b/mac/Sources/Byre/Settings.swift index 93d02af..3a3a5c9 100644 --- a/mac/Sources/Byre/Settings.swift +++ b/mac/Sources/Byre/Settings.swift @@ -42,6 +42,11 @@ struct SettingsView: View { ModelMenu(current: store.defaultModel, help: "Model for new tasks") { m in try await store.api.client.updateDefaultModel(body: .json(.init(model: m))).ok.body.json } + heading("Saved commands") + Text("Each is a button in the Terminal tab of every task on this server. It opens a new shell in the task's worktree and runs the command there.") + .foregroundStyle(Palette.muted) + .fixedSize(horizontal: false, vertical: true) + SavedCommands() heading("iCloud sync") SyncState() heading("This Mac's permissions") @@ -217,6 +222,140 @@ private struct PrefsBox: View { } } +private struct SavedCommands: View { + @Environment(Store.self) private var store + @Environment(Overlays.self) private var overlays + @ViewState private var list: [SavedCommand] = [] + @ViewState private var editing: String? + @ViewState private var name = "" + @ViewState private var command = "" + @ViewState private var off: (() -> Void)? + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(list, id: \.id) { c in + if editing == c.id { + form(c.id) + } else { + HStack(alignment: .firstTextBaseline, spacing: 12) { + VStack(alignment: .leading, spacing: 2) { + Text(c.name) + Text(c.command) + .font(Fonts.font(12)) + .foregroundStyle(Palette.muted) + .textSelection(.enabled) + .fixedSize(horizontal: false, vertical: true) + } + .frame(maxWidth: .infinity, alignment: .leading) + Button("Edit") { + editing = c.id + name = c.name + command = c.command + } + .buttonStyle(SmallLinkButtonStyle()) + Button("Delete") { + delete(c) + } + .buttonStyle(SmallLinkButtonStyle(color: Palette.danger)) + } + .padding(.vertical, 6) + .overlay(alignment: .top) { + Rectangle().fill(Palette.border).frame(height: 1) + } + } + } + if editing == nil { + form(nil) + } + } + .padding(.top, 8) + .onAppear { + off = store.events.add { ev in + if ev.kind == "command.saved" || ev.kind == "command.deleted" { + load() + } + } + load() + } + .onDisappear { + off?() + off = nil + } + } + + private func form(_ id: String?) -> some View { + let valid = !name.trimmingCharacters(in: .whitespaces).isEmpty && !command.trimmingCharacters(in: .whitespaces).isEmpty + return HStack(spacing: 6) { + TextField("Name", text: $name) + .modifier(FieldStyle()) + .frame(width: 180) + .onSubmit { + save(id, valid) + } + TextField("Command", text: $command) + .modifier(FieldStyle()) + .onSubmit { + save(id, valid) + } + Button(id == nil ? "Add" : "Save") { + save(id, valid) + } + .buttonStyle(WebButtonStyle()) + .disabled(!valid) + if id != nil { + Button("Cancel") { + editing = nil + name = "" + command = "" + } + .buttonStyle(WebButtonStyle()) + } + } + .padding(.vertical, 6) + } + + private func load() { + Task { + do { + list = try await store.api.client.listSavedCommands().ok.body.json + } catch { + overlays.showAlert(errorText(error)) + } + } + } + + private func save(_ id: String?, _ valid: Bool) { + if !valid { + return + } + let body = Components.Schemas.SavedCommandSave(name: name, command: command) + Task { + do { + if let id { + _ = try await store.api.client.updateSavedCommand(path: .init(commandId: id), body: .json(body)).ok.body.json + } else { + _ = try await store.api.client.createSavedCommand(body: .json(body)).created.body.json + } + editing = nil + name = "" + command = "" + } catch { + overlays.showAlert(errorText(error)) + } + } + } + + private func delete(_ c: SavedCommand) { + Task { + do { + _ = try await store.api.client.deleteSavedCommand(path: .init(commandId: c.id)).noContent + } catch { + overlays.showAlert(errorText(error)) + } + } + } +} + private struct ModelMenu: View { let current: String let help: String diff --git a/mac/Sources/Byre/Terminal.swift b/mac/Sources/Byre/Terminal.swift index dd46b3e..74f4c85 100644 --- a/mac/Sources/Byre/Terminal.swift +++ b/mac/Sources/Byre/Terminal.swift @@ -1,11 +1,14 @@ import AppKit +import ByreAPI import SwiftTerm import SwiftUI let exitedCode = 4000 -let internalErrorCode = 1011 private let retryAfter = Duration.seconds(2) +typealias TaskTerminal = Components.Schemas.TaskTerminal +typealias SavedCommand = Components.Schemas.SavedCommand + @MainActor func terminalFocused() -> Bool { NSApp.keyWindow?.firstResponder is TerminalView @@ -13,39 +16,76 @@ func terminalFocused() -> Bool { struct TerminalTab: View { @Environment(Store.self) private var store + @Environment(Overlays.self) private var overlays + @ViewState private var list: [TaskTerminal]? + @ViewState private var commands: [SavedCommand] = [] + @ViewState private var selected: String? @ViewState private var status = "" - @ViewState private var exited = false - @ViewState private var shell = 0 + @ViewState private var failure: String? + @ViewState private var started = false + @ViewState private var loads = 0 + @ViewState private var off: (() -> Void)? var body: some View { let taskId = store.selectedTaskId let task = taskId.flatMap { store.findTask($0) } + let current = list?.first { $0.id == selected }?.id ?? list?.first?.id VStack(spacing: 0) { HStack(alignment: .firstTextBaseline, spacing: 12) { - Text("Shell in the task's worktree") - .foregroundStyle(Palette.muted) - .frame(maxWidth: .infinity, alignment: .leading) + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .firstTextBaseline, spacing: 4) { + ForEach(list ?? [], id: \.id) { term in + HStack(alignment: .firstTextBaseline, spacing: 0) { + RangeTabButton(label: term.name, active: term.id == current, help: "Show this terminal") { + selected = term.id + } + Button("×") { + close(term) + } + .buttonStyle(.plain) + .foregroundStyle(Palette.muted) + .help("Close this terminal") + } + } + if task?.status == .active { + Button("+ Shell") { + start(nil) + } + .buttonStyle(SmallLinkButtonStyle()) + .help("Open another shell in the task's worktree") + .padding(.leading, 8) + ForEach(commands, id: \.id) { c in + Button("▸ \(c.name)") { + start(c.id) + } + .buttonStyle(SmallLinkButtonStyle()) + .help("Open a shell and run \(c.command)") + } + } + } + } Text(status) .font(Fonts.font(11)) .foregroundStyle(Palette.muted) .lineLimit(1) - if exited { - Button("New shell") { - shell += 1 - } - .buttonStyle(WebButtonStyle()) - } } .modifier(HeaderBar()) Group { if let taskId, task?.status == .active { - TerminalHost(api: store.api, taskId: taskId, active: { [weak store] in - store?.findTask(taskId)?.status == .active - }, show: { text, ended in - status = text - exited = ended - }) - .id("\(taskId) \(shell)") + if let current { + TerminalHost(api: store.api, taskId: taskId, terminalId: current, active: { [weak store] in + store?.findTask(taskId)?.status == .active + }, show: { text in + status = text + }, lost: { + load() + }) + .id("\(taskId) \(current)") + } else if let failure { + EmptyText(text: failure, error: true) + } else if list != nil { + EmptyText(text: "No terminals are open in this task.") + } } else if taskId == nil { EmptyText(text: "No task selected.") } else { @@ -58,8 +98,97 @@ struct TerminalTab: View { .background(Palette.bg) } .onChange(of: "\(taskId ?? "") \(task?.status.rawValue ?? "")", initial: true) { + list = nil + selected = nil status = "" - exited = false + failure = nil + started = false + load() + } + .onChange(of: store.connected) { + if store.connected { + load() + } + } + .onAppear { + off = store.events.add { ev in + if ev.kind == "terminals.changed" && ev.taskId == store.selectedTaskId { + load() + } + if ev.kind == "command.saved" || ev.kind == "command.deleted" { + loadCommands() + } + } + loadCommands() + } + .onDisappear { + off?() + off = nil + loads += 1 + } + } + + private func load() { + guard let taskId = store.selectedTaskId, store.findTask(taskId)?.status == .active else { + return + } + loads += 1 + let n = loads + Task { + do { + let loaded = try await store.api.client.listTaskTerminals(path: .init(taskId: taskId)).ok.body.json + if n != loads { + return + } + list = loaded + failure = nil + if loaded.isEmpty && !started { + start(nil) + } + } catch { + failure = errorText(error) + } + } + } + + private func loadCommands() { + Task { + do { + commands = try await store.api.client.listSavedCommands().ok.body.json + } catch { + overlays.showAlert(errorText(error)) + } + } + } + + private func start(_ commandId: String?) { + guard let taskId = store.selectedTaskId else { + return + } + started = true + Task { + do { + let term = try await store.api.client.startTaskTerminal(path: .init(taskId: taskId), body: .json(.init(commandId: commandId))).created.body.json + if store.selectedTaskId == taskId { + selected = term.id + load() + } + } catch { + failure = errorText(error) + } + } + } + + private func close(_ term: TaskTerminal) { + guard let taskId = store.selectedTaskId else { + return + } + Task { + do { + _ = try await store.api.client.closeTaskTerminal(path: .init(taskId: taskId, terminalId: term.id)).noContent + } catch { + overlays.showAlert(errorText(error)) + } } } } @@ -67,12 +196,14 @@ struct TerminalTab: View { struct TerminalHost: NSViewRepresentable { let api: API let taskId: String + let terminalId: String let active: @MainActor () -> Bool - let show: @MainActor (String, Bool) -> Void + let show: @MainActor (String) -> Void + let lost: @MainActor () -> Void @Environment(\.colorScheme) private var scheme func makeCoordinator() -> TerminalSession { - TerminalSession(api: api, taskId: taskId, active: active, show: show) + TerminalSession(api: api, taskId: taskId, terminalId: terminalId, active: active, show: show, lost: lost) } func makeNSView(context: Context) -> TerminalView { @@ -127,19 +258,23 @@ struct TerminalHost: NSViewRepresentable { final class TerminalSession: NSObject, TerminalViewDelegate, URLSessionWebSocketDelegate { let api: API let taskId: String + let terminalId: String let active: @MainActor () -> Bool - let show: @MainActor (String, Bool) -> Void + let show: @MainActor (String) -> Void + let lost: @MainActor () -> Void weak var view: TerminalView? var scheme: ColorScheme? private var ws: URLSessionWebSocketTask? private var retry: Task? private var done = false - init(api: API, taskId: String, active: @escaping @MainActor () -> Bool, show: @escaping @MainActor (String, Bool) -> Void) { + init(api: API, taskId: String, terminalId: String, active: @escaping @MainActor () -> Bool, show: @escaping @MainActor (String) -> Void, lost: @escaping @MainActor () -> Void) { self.api = api self.taskId = taskId + self.terminalId = terminalId self.active = active self.show = show + self.lost = lost } func connect() { @@ -147,9 +282,9 @@ final class TerminalSession: NSObject, TerminalViewDelegate, URLSessionWebSocket return } retry?.cancel() - show("connecting…", false) + show("connecting…") let t = view.terminalDimensions - let url = api.url("/api/tasks/\(taskId)/terminal", [URLQueryItem(name: "cols", value: String(t.cols)), URLQueryItem(name: "rows", value: String(t.rows))], scheme: "wss") + let url = api.url("/api/tasks/\(taskId)/terminals/\(terminalId)", [URLQueryItem(name: "cols", value: String(t.cols)), URLQueryItem(name: "rows", value: String(t.rows))], scheme: "wss") let task = api.session.webSocketTask(with: url) task.delegate = self ws = task @@ -171,7 +306,7 @@ final class TerminalSession: NSObject, TerminalViewDelegate, URLSessionWebSocket return } self.view?.resetToInitialState() - self.show("", false) + self.show("") } } @@ -202,18 +337,15 @@ final class TerminalSession: NSObject, TerminalViewDelegate, URLSessionWebSocket let code = task.closeCode.rawValue let reason = task.closeReason.map { String(decoding: $0, as: UTF8.self) } ?? "" if code == exitedCode { - show("shell ended: \(reason)", true) - return - } - if code == internalErrorCode { - show(reason, true) + show("shell ended: \(reason)") return } if !active() { - show("task closed", false) + show("task closed") return } - show("disconnected, reconnecting…", false) + show("disconnected, reconnecting…") + lost() retry = Task { try? await Task.sleep(for: retryAfter) if !Task.isCancelled {