From 5179ae433b6aeee22f69328cebbfd7abd8f117ea Mon Sep 17 00:00:00 2001 From: Darren Shepherd Date: Tue, 7 Jan 2025 08:29:53 -0700 Subject: [PATCH] chore: add more custom tools in user ui --- Dockerfile | 2 +- apiclient/types/tool.go | 1 + pkg/api/handlers/assistants.go | 46 +++ pkg/api/handlers/tools.go | 127 ++++++-- pkg/api/router/router.go | 6 +- pkg/invoke/invoker.go | 13 +- pkg/render/render.go | 19 +- pkg/render/tool.go | 105 +++++-- .../openapi/generated/openapi_generated.go | 6 + ui/user/package.json | 3 +- ui/user/src/lib/actions/textarea.ts | 10 +- ui/user/src/lib/components/Editors.svelte | 9 +- ui/user/src/lib/components/Terminal.svelte | 55 +++- .../lib/components/editor/Codemirror.svelte | 10 +- .../src/lib/components/editor/Controls.svelte | 2 +- ui/user/src/lib/components/navbar/Tool.svelte | 268 ---------------- .../src/lib/components/navbar/Tools.svelte | 51 ++- .../src/lib/components/tasks/Dropdown.svelte | 9 +- ui/user/src/lib/components/tasks/Task.svelte | 2 +- ui/user/src/lib/components/tool/Env.svelte | 81 +++++ ui/user/src/lib/components/tool/Params.svelte | 55 ++++ ui/user/src/lib/components/tool/Tool.svelte | 297 ++++++++++++++++++ ui/user/src/lib/services/chat/http.ts | 31 +- ui/user/src/lib/services/chat/operations.ts | 27 +- ui/user/src/lib/services/chat/types.ts | 1 + .../src/lib/services/editor/index.svelte.ts | 17 + ui/user/src/lib/stores/editor.svelte.ts | 1 + 27 files changed, 857 insertions(+), 397 deletions(-) delete mode 100644 ui/user/src/lib/components/navbar/Tool.svelte create mode 100644 ui/user/src/lib/components/tool/Env.svelte create mode 100644 ui/user/src/lib/components/tool/Params.svelte create mode 100644 ui/user/src/lib/components/tool/Tool.svelte diff --git a/Dockerfile b/Dockerfile index 38d4264bc..cc0317f66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -40,7 +40,7 @@ ENV PGDATA=/data/postgresql COPY --from=build-pgvector /usr/lib/postgresql17/vector.so /usr/lib/postgresql17/ COPY --from=build-pgvector /usr/share/postgresql17/extension/vector* /usr/share/postgresql17/extension/ -RUN apk add --no-cache git python-3.13 py3.13-pip openssh-server npm bash tini procps libreoffice +RUN apk add --no-cache git python-3.13 py3.13-pip openssh-server npm bash tini procps libreoffice docker COPY --chmod=0755 /tools/package-chrome.sh / RUN /package-chrome.sh && rm /package-chrome.sh RUN sed -E 's/^#(PermitRootLogin)no/\1yes/' /etc/ssh/sshd_config -i diff --git a/apiclient/types/tool.go b/apiclient/types/tool.go index d550202af..48ed72b22 100644 --- a/apiclient/types/tool.go +++ b/apiclient/types/tool.go @@ -11,6 +11,7 @@ type ToolManifest struct { Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` ToolType ToolType `json:"toolType,omitempty"` + Image string `json:"image,omitempty"` Context string `json:"context,omitempty"` Instructions string `json:"instructions,omitempty"` Params map[string]string `json:"params,omitempty"` diff --git a/pkg/api/handlers/assistants.go b/pkg/api/handlers/assistants.go index 5ca77909d..d9b75a8ed 100644 --- a/pkg/api/handlers/assistants.go +++ b/pkg/api/handlers/assistants.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "io" + "maps" "net/http" "regexp" "slices" @@ -310,6 +311,51 @@ func (a *AssistantHandler) DeleteFile(req api.Context) error { return deleteFileFromWorkspaceID(req.Context(), req, a.gptScript, thread.Status.WorkspaceID, "files/") } +func (a *AssistantHandler) SetEnv(req api.Context) error { + var ( + id = req.PathValue("id") + ) + + thread, err := getUserThread(req, id) + if err != nil { + return err + } + + var envs map[string]string + if err := req.Read(&envs); err != nil { + return err + } + + if err := setEnvMap(req, a.gptScript, thread.Name, thread.Name, envs); err != nil { + return err + } + + thread.Spec.Env = slices.Collect(maps.Keys(envs)) + if err := req.Update(thread); err != nil { + return err + } + + return req.Write(envs) +} + +func (a *AssistantHandler) GetEnv(req api.Context) error { + var ( + id = req.PathValue("id") + ) + + thread, err := getUserThread(req, id) + if err != nil { + return err + } + + data, err := getEnvMap(req, a.gptScript, thread.Name, thread.Name) + if err != nil { + return err + } + + return req.Write(data) +} + func (a *AssistantHandler) Knowledge(req api.Context) error { var ( id = req.PathValue("id") diff --git a/pkg/api/handlers/tools.go b/pkg/api/handlers/tools.go index 29490fd6d..b94b98e48 100644 --- a/pkg/api/handlers/tools.go +++ b/pkg/api/handlers/tools.go @@ -2,13 +2,15 @@ package handlers import ( "errors" + "maps" "regexp" "slices" - "strings" "github.com/gptscript-ai/go-gptscript" "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/api" + "github.com/obot-platform/obot/pkg/invoke" + "github.com/obot-platform/obot/pkg/render" v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" "github.com/obot-platform/obot/pkg/system" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,14 +18,33 @@ import ( type ToolHandler struct { gptScript *gptscript.GPTScript + invoke *invoke.Invoker } -func NewToolHandler(gptScript *gptscript.GPTScript) *ToolHandler { - return &ToolHandler{gptScript: gptScript} +func NewToolHandler(gptScript *gptscript.GPTScript, invoke *invoke.Invoker) *ToolHandler { + return &ToolHandler{ + gptScript: gptScript, + invoke: invoke, + } } var invalidEnv = regexp.MustCompile("^(OBOT|GPTSCRIPT)") +func setEnvMap(req api.Context, gptScript *gptscript.GPTScript, threadName, toolName string, env map[string]string) error { + for k := range env { + if invalidEnv.MatchString(k) { + return types.NewErrBadRequest("invalid env key %s", k) + } + } + + return gptScript.CreateCredential(req.Context(), gptscript.Credential{ + Context: threadName, + ToolName: toolName, + Type: gptscript.CredentialTypeTool, + Env: env, + }) +} + func (t *ToolHandler) SetEnv(req api.Context) error { toolID := req.PathValue("tool_id") env := map[string]string{} @@ -46,29 +67,11 @@ func (t *ToolHandler) SetEnv(req api.Context) error { return types.NewErrNotFound("tool %s not found", toolID) } - for k := range env { - if invalidEnv.MatchString(k) { - return types.NewErrBadRequest("invalid env key %s", k) - } - } - - err = t.gptScript.CreateCredential(req.Context(), gptscript.Credential{ - Context: thread.Name, - ToolName: tool.Name, - Type: gptscript.CredentialTypeTool, - Env: env, - }) - if err != nil { + if err := setEnvMap(req, t.gptScript, thread.Name, tool.Name, env); err != nil { return err } - var envs []string - for k, v := range env { - if strings.TrimSpace(v) != "" { - envs = append(envs, k) - } - } - tool.Spec.Envs = envs + tool.Spec.Envs = slices.Collect(maps.Keys(env)) if err := req.Update(&tool); err != nil { return err } @@ -93,14 +96,23 @@ func (t *ToolHandler) GetEnv(req api.Context) error { return types.NewErrNotFound("tool %s not found", toolID) } - cred, err := t.gptScript.RevealCredential(req.Context(), []string{thread.Name}, tool.Name) + data, err := getEnvMap(req, t.gptScript, thread.Name, tool.Name) + if err != nil { + return err + } + + return req.Write(data) +} + +func getEnvMap(req api.Context, gptScript *gptscript.GPTScript, threadName, toolName string) (map[string]string, error) { + cred, err := gptScript.RevealCredential(req.Context(), []string{threadName}, toolName) if errors.As(err, &gptscript.ErrNotFound{}) { - return req.Write(map[string]string{}) + return map[string]string{}, nil } else if err != nil { - return err + return nil, err } - return req.Write(cred.Env) + return cred.Env, nil } func (t *ToolHandler) Get(req api.Context) error { @@ -123,6 +135,67 @@ func (t *ToolHandler) Get(req api.Context) error { return req.Write(convertTool(tool, slices.Contains(thread.Spec.Manifest.Tools, tool.Name))) } +type TestInput struct { + Input map[string]string `json:"input"` + Tool *types.AssistantTool `json:"tool"` + Env map[string]string `json:"env,omitempty"` +} + +func (t *ToolHandler) Test(req api.Context) error { + toolID := req.PathValue("tool_id") + + thread, err := getThreadForScope(req) + if err != nil { + return err + } + + var tool v1.Tool + if err := req.Get(&tool, toolID); err != nil { + return err + } + + if tool.Spec.ThreadName != thread.Name { + return types.NewErrNotFound("tool %s not found", toolID) + } + + env, err := getEnvMap(req, t.gptScript, thread.Name, tool.Name) + if err != nil { + return err + } + + var envList []string + for k, v := range env { + envList = append(envList, k+"="+v) + } + + var input TestInput + if err := req.Read(&input); err != nil { + return err + } + + for k, v := range input.Env { + envList = append(envList, k+"="+v) + } + + if input.Tool != nil { + tool.Spec.Manifest = input.Tool.ToolManifest + } + + tools, err := render.CustomTool(req.Context(), req.Storage, tool) + if err != nil { + return err + } + + result, err := t.invoke.EphemeralThreadTask(req.Context(), thread, tools, input.Input, invoke.SystemTaskOptions{ + Env: envList, + }) + if err != nil { + return err + } + + return req.Write(map[string]string{"output": result}) +} + func (t *ToolHandler) Create(req api.Context) error { thread, err := getThreadForScope(req) if err != nil { diff --git a/pkg/api/router/router.go b/pkg/api/router/router.go index 6c50dfee3..479f523ea 100644 --- a/pkg/api/router/router.go +++ b/pkg/api/router/router.go @@ -13,7 +13,7 @@ func Router(services *services.Services) (http.Handler, error) { agents := handlers.NewAgentHandler(services.GPTClient, services.Invoker, services.ServerURL) assistants := handlers.NewAssistantHandler(services.Invoker, services.Events, services.GPTClient) - tools := handlers.NewToolHandler(services.GPTClient) + tools := handlers.NewToolHandler(services.GPTClient, services.Invoker) tasks := handlers.NewTaskHandler(services.Invoker, services.Events) workflows := handlers.NewWorkflowHandler(services.GPTClient, services.ServerURL, services.Invoker) invoker := handlers.NewInvokeHandler(services.Invoker) @@ -72,6 +72,9 @@ func Router(services *services.Services) (http.Handler, error) { mux.HandleFunc("GET /api/assistants/{id}/knowledge", assistants.Knowledge) mux.HandleFunc("POST /api/assistants/{id}/knowledge/{file}", assistants.UploadKnowledge) mux.HandleFunc("DELETE /api/assistants/{id}/knowledge/{file...}", assistants.DeleteKnowledge) + // Env + mux.HandleFunc("GET /api/assistants/{id}/env", assistants.GetEnv) + mux.HandleFunc("PUT /api/assistants/{id}/env", assistants.SetEnv) if services.SupportDocker { shell, err := handlers.NewShellHandler(services.Invoker) @@ -83,6 +86,7 @@ func Router(services *services.Services) (http.Handler, error) { // Tools mux.HandleFunc("POST /api/assistants/{assistant_id}/tools", tools.Create) mux.HandleFunc("PUT /api/assistants/{assistant_id}/tools/{tool_id}/env", tools.SetEnv) + mux.HandleFunc("POST /api/assistants/{assistant_id}/tools/{tool_id}/test", tools.Test) } mux.HandleFunc("GET /api/assistants/{assistant_id}/tools/{tool_id}", tools.Get) mux.HandleFunc("GET /api/assistants/{assistant_id}/tools/{tool_id}/env", tools.GetEnv) diff --git a/pkg/invoke/invoker.go b/pkg/invoke/invoker.go index 89ea71447..8fad4c588 100644 --- a/pkg/invoke/invoker.go +++ b/pkg/invoke/invoker.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "maps" - "slices" "strings" "sync" "sync/atomic" @@ -251,7 +250,7 @@ func CreateThreadForAgent(ctx context.Context, c kclient.WithWatch, agent *v1.Ag return &thread, c.Create(ctx, &thread) } -func (i *Invoker) updateThreadFields(ctx context.Context, c kclient.WithWatch, agent *v1.Agent, thread *v1.Thread, extraEnv []string, opt Options) error { +func (i *Invoker) updateThreadFields(ctx context.Context, c kclient.WithWatch, agent *v1.Agent, thread *v1.Thread, opt Options) error { var updated bool if opt.AgentAlias != "" && thread.Spec.AgentAlias != opt.AgentAlias { thread.Spec.AgentAlias = opt.AgentAlias @@ -261,10 +260,6 @@ func (i *Invoker) updateThreadFields(ctx context.Context, c kclient.WithWatch, a thread.Spec.AgentName = agent.Name updated = true } - if !slices.Equal(thread.Spec.Env, extraEnv) { - thread.Spec.Env = extraEnv - updated = true - } if updated { return c.Status().Update(ctx, thread) } @@ -302,7 +297,7 @@ func (i *Invoker) Agent(ctx context.Context, c kclient.WithWatch, agent *v1.Agen return nil, err } - if err := i.updateThreadFields(ctx, c, agent, thread, extraEnv, opt); err != nil { + if err := i.updateThreadFields(ctx, c, agent, thread, opt); err != nil { return nil, err } @@ -599,7 +594,7 @@ func (i *Invoker) Resume(ctx context.Context, c kclient.WithWatch, thread *v1.Th func (i *Invoker) saveState(ctx context.Context, c kclient.Client, prevThreadName string, thread *v1.Thread, run *v1.Run, runResp *gptscript.Run, retErr error) error { if isEphemeral(run) { // Ephemeral run, don't save state - return nil + return retErr } var err error @@ -886,7 +881,7 @@ func (i *Invoker) stream(ctx context.Context, c kclient.WithWatch, prevThreadNam timeout = run.Spec.Timeout.Duration } go timeoutAfter(runCtx, cancelRun, timeout) - if run.Name != "" { + if !isEphemeral(run) { // Don't watch thread abort for ephemeral runs go watchThreadAbort(runCtx, c, thread, cancelRun) } diff --git a/pkg/render/render.go b/pkg/render/render.go index 815b5ebe2..f64ae8e2a 100644 --- a/pkg/render/render.go +++ b/pkg/render/render.go @@ -12,6 +12,7 @@ import ( "github.com/obot-platform/obot/apiclient/types" "github.com/obot-platform/obot/pkg/gz" v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" + "github.com/obot-platform/obot/pkg/system" apierror "k8s.io/apimachinery/pkg/api/errors" kclient "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -81,9 +82,21 @@ func Agent(ctx context.Context, db kclient.Client, agent *v1.Agent, oauthServerU if err != nil { return nil, nil, err } - mainTool.Tools = append(mainTool.Tools, name) + if name != "" { + mainTool.Tools = append(mainTool.Tools, name) + } otherTools = append(otherTools, tools...) } + + credTool, err := ResolveToolReference(ctx, db, types.ToolReferenceTypeSystem, opts.Thread.Namespace, system.ExistingCredTool) + if err != nil { + return nil, nil, err + } + + mainTool.Credentials = append(mainTool.Credentials, credTool+" as "+opts.Thread.Name) + if len(opts.Thread.Spec.Env) > 0 { + extraEnv = append(extraEnv, fmt.Sprintf("OBOT_THREAD_ENVS=%s", strings.Join(opts.Thread.Spec.Env, ","))) + } } for _, tool := range agent.Spec.Manifest.Tools { @@ -94,7 +107,9 @@ func Agent(ctx context.Context, db kclient.Client, agent *v1.Agent, oauthServerU if err != nil { return nil, nil, err } - mainTool.Tools = append(mainTool.Tools, name) + if name != "" { + mainTool.Tools = append(mainTool.Tools, name) + } otherTools = append(otherTools, tools...) } diff --git a/pkg/render/tool.go b/pkg/render/tool.go index c5ab0aeba..823b08219 100644 --- a/pkg/render/tool.go +++ b/pkg/render/tool.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/gptscript-ai/go-gptscript" + "github.com/gptscript-ai/gptscript/pkg/env" "github.com/obot-platform/nah/pkg/router" "github.com/obot-platform/obot/apiclient/types" v1 "github.com/obot-platform/obot/pkg/storage/apis/otto.otto8.ai/v1" @@ -13,53 +14,99 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" ) -func Tool(ctx context.Context, c client.Client, ns, name string) (_ string, toolDefs []gptscript.ToolDef, _ error) { - if !system.IsToolID(name) { - name, err := ResolveToolReference(ctx, c, types.ToolReferenceTypeTool, ns, name) - return name, nil, err +func CustomTool(ctx context.Context, c client.Client, tool v1.Tool) (toolDefs []gptscript.ToolDef, _ error) { + if tool.Spec.Manifest.Name == "" { + return nil, nil } - var tool v1.Tool - if err := c.Get(ctx, router.Key(ns, name), &tool); err != nil { - return name, nil, err + if tool.Spec.Manifest.ToolType != "" && tool.Spec.Manifest.ToolType != "docker" && tool.Spec.Manifest.Instructions == "" { + return nil, fmt.Errorf("instructions are required for custom tools") } contextName := fmt.Sprintf("%s-context", tool.Name) - dockerTool, err := ResolveToolReference(ctx, c, types.ToolReferenceTypeSystem, ns, system.DockerTool) + dockerTool, err := ResolveToolReference(ctx, c, types.ToolReferenceTypeSystem, tool.Namespace, system.DockerTool) if err != nil { - return name, nil, err + return nil, err } - credTool, err := ResolveToolReference(ctx, c, types.ToolReferenceTypeSystem, ns, system.ExistingCredTool) + credTool, err := ResolveToolReference(ctx, c, types.ToolReferenceTypeSystem, tool.Namespace, system.ExistingCredTool) if err != nil { - return name, nil, err + return nil, err } - params := []string{} + var envs []string + + var params []string for k, v := range tool.Spec.Manifest.Params { params = append(params, k, v) + envs = append(envs, env.ToEnvLike(k)) } - instructions := []string{"#!sys.call", - dockerTool, + envs = append(envs, tool.Spec.Envs...) + + var instructions []string + if len(envs) > 0 { + instructions = append(instructions, fmt.Sprintf("%q as obot_tool_envs", strings.Join(envs, ","))) + } + if tool.Spec.Manifest.Image != "" { + instructions = append(instructions, fmt.Sprintf("%q as obot_tool_image", tool.Spec.Manifest.Image)) + } + if tool.Spec.Manifest.ToolType != "" && tool.Spec.Manifest.ToolType != "docker" { + instructions = append(instructions, fmt.Sprintf("%q as obot_tool_type", tool.Spec.Manifest.ToolType)) + } + + if len(instructions) > 0 { + instructions = append([]string{"#!sys.call", dockerTool, "with"}, instructions...) + } else { + instructions = []string{"#!sys.call", dockerTool} } - if len(tool.Spec.Envs) > 0 { - instructions = append(instructions, strings.Join(tool.Spec.Envs, ","), "as", "envs") + + toolDefs = []gptscript.ToolDef{{ + Name: tool.Spec.Manifest.Name, + Description: tool.Spec.Manifest.Description, + Arguments: gptscript.ObjectSchema(params...), + Tools: []string{dockerTool}, + Credentials: []string{credTool + " as " + tool.Name}, + Instructions: strings.Join(instructions, " ") + "\n" + tool.Spec.Manifest.Instructions, + }} + + if tool.Spec.Manifest.Context != "" { + toolDefs[0].ExportContext = []string{contextName} + toolDefs = append(toolDefs, gptscript.ToolDef{ + Name: contextName, + Type: "context", + Instructions: fmt.Sprintf(`#!sys.echo +START INSTRUCTIONS: TOOL %q + +%s + +END INSTRUCTIONS: TOOL %q`, tool.Spec.Manifest.Name, tool.Spec.Manifest.Context, tool.Spec.Manifest.Name), + }) + } + + return toolDefs, nil +} + +func Tool(ctx context.Context, c client.Client, ns, name string) (_ string, toolDefs []gptscript.ToolDef, _ error) { + if !system.IsToolID(name) { + name, err := ResolveToolReference(ctx, c, types.ToolReferenceTypeTool, ns, name) + return name, nil, err + } + + var tool v1.Tool + if err := c.Get(ctx, router.Key(ns, name), &tool); err != nil { + return name, nil, err + } + + toolDefs, err := CustomTool(ctx, c, tool) + if err != nil { + return "", nil, err + } + + if len(toolDefs) == 0 { + return "", toolDefs, nil } - toolDefs = append(toolDefs, gptscript.ToolDef{ - Name: tool.Spec.Manifest.Name, - Description: tool.Spec.Manifest.Description, - Arguments: gptscript.ObjectSchema(params...), - Tools: []string{dockerTool}, - ExportContext: []string{contextName}, - Credentials: []string{credTool + " as " + tool.Name}, - Instructions: strings.Join(instructions, " ") + "\n" + tool.Spec.Manifest.Instructions, - }, gptscript.ToolDef{ - Name: contextName, - Type: "context", - Instructions: "#!sys.echo\n" + tool.Spec.Manifest.Context, - }) return toolDefs[0].Name, toolDefs, nil } diff --git a/pkg/storage/openapi/generated/openapi_generated.go b/pkg/storage/openapi/generated/openapi_generated.go index 00f798ba2..501932b34 100644 --- a/pkg/storage/openapi/generated/openapi_generated.go +++ b/pkg/storage/openapi/generated/openapi_generated.go @@ -3615,6 +3615,12 @@ func schema_obot_platform_obot_apiclient_types_ToolManifest(ref common.Reference Format: "", }, }, + "image": { + SchemaProps: spec.SchemaProps{ + Type: []string{"string"}, + Format: "", + }, + }, "context": { SchemaProps: spec.SchemaProps{ Type: []string{"string"}, diff --git a/ui/user/package.json b/ui/user/package.json index 4694923ea..d6f06ac54 100644 --- a/ui/user/package.json +++ b/ui/user/package.json @@ -9,7 +9,8 @@ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", "lint": "prettier --check . && eslint .", - "format": "prettier --write ." + "format": "prettier --write .", + "ci": "npm run format && npm run lint && npm run check" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.3.1", diff --git a/ui/user/src/lib/actions/textarea.ts b/ui/user/src/lib/actions/textarea.ts index 43ebda390..363f314bf 100644 --- a/ui/user/src/lib/actions/textarea.ts +++ b/ui/user/src/lib/actions/textarea.ts @@ -1,19 +1,15 @@ -import { tick } from 'svelte'; - export function resize(node: HTMLTextAreaElement) { node.style.height = 'auto'; node.style.height = node.scrollHeight + 'px'; } export function autoHeight(node: HTMLTextAreaElement) { + if ('fieldSizing' in node.style) { + node.style.fieldSizing = 'content'; + } node.onkeyup = () => resize(node); node.onfocus = () => resize(node); node.oninput = () => resize(node); node.onresize = () => resize(node); node.onchange = () => resize(node); - tick().then(() => resize(node)); - - // I don't have a great solution when the textarea is loaded on demand because it doesn't - // seem to fire any event. I'm sure there is one. - setTimeout(() => resize(node), 500); } diff --git a/ui/user/src/lib/components/Editors.svelte b/ui/user/src/lib/components/Editors.svelte index fa15a9210..936fb13e2 100644 --- a/ui/user/src/lib/components/Editors.svelte +++ b/ui/user/src/lib/components/Editors.svelte @@ -8,10 +8,11 @@ import Controls from '$lib/components/editor/Controls.svelte'; import { currentAssistant } from '$lib/stores'; import Image from '$lib/components/editor/Image.svelte'; - import { CheckSquare, Table as TableIcon, Image as ImageIcon } from 'lucide-svelte'; + import { CheckSquare, Table as TableIcon, Image as ImageIcon, Wrench } from 'lucide-svelte'; import { isImage } from '$lib/image'; import Terminal from '$lib/components/Terminal.svelte'; import { term } from '$lib/stores'; + import Tool from '$lib/components/tool/Tool.svelte'; const editorVisible = EditorService.visible; @@ -33,7 +34,7 @@
{#if $editorVisible} - {#if EditorService.items.length > 1 || (!EditorService.items[0].task && !EditorService.items[0].table)} + {#if EditorService.items.length > 1 || (!EditorService.items[0].task && !EditorService.items[0].table && !EditorService.items[0].generic)}
    {#each EditorService.items as item} @@ -53,6 +54,8 @@ {:else if isImage(item.name)} + {:else if item.id.startsWith('tl1')} + {:else} {/if} @@ -94,6 +97,8 @@ /> {:else if isImage(file.name)} + {:else if file.id.startsWith('tl1')} + {:else} {/if} diff --git a/ui/user/src/lib/components/Terminal.svelte b/ui/user/src/lib/components/Terminal.svelte index e24c9def7..0c18fd735 100644 --- a/ui/user/src/lib/components/Terminal.svelte +++ b/ui/user/src/lib/components/Terminal.svelte @@ -2,16 +2,22 @@ import { onDestroy, onMount } from 'svelte'; import '@xterm/xterm/css/xterm.css'; import { currentAssistant } from '$lib/stores'; + import { RefreshCcw } from 'lucide-svelte'; let terminalContainer: HTMLElement; let close: () => void; + let connectState = $state('disconnected'); onDestroy(() => close?.()); - onMount(async () => { + onMount(connect); + + async function connect() { const { Terminal } = await import('@xterm/xterm'); const { FitAddon } = await import('@xterm/addon-fit'); + close?.(); + const term = new Terminal(); const fitAddon = new FitAddon(); term.loadAddon(fitAddon); @@ -19,11 +25,9 @@ term.open(terminalContainer); new ResizeObserver(() => { - console.log('div resized'); fitAddon.fit(); }).observe(terminalContainer); - // Example: Connect to a WebSocket server const url = window.location.protocol.replaceAll('http', 'ws') + '//' + @@ -31,15 +35,36 @@ '/api/assistants/' + $currentAssistant.id + '/shell'; + let gotData = false; const socket = new WebSocket(url); + connectState = 'connecting'; socket.onmessage = (event) => term.write(event.data); socket.onopen = () => { - term.write('\n'); + connectState = 'connected'; + fitAddon.fit(); term.focus(); + setTimeout(() => { + if (!gotData) { + socket.send('\n'); + } + }, 500); + }; + socket.onclose = () => { + connectState = 'disconnected'; + term.write('\r\nConnection closed.\r\n'); }; - term.onData((data) => socket.send(data)); + socket.onerror = () => { + connectState = 'disconnected'; + term.write('\r\nConnection error.\r\n'); + }; + term.options.theme = { + background: '#131313' + }; + term.onData((data) => { + gotData = true; + socket.send(data); + }); term.onResize(({ cols, rows }) => { - console.log('resize', cols, rows); const data = JSON.stringify({ cols, rows }); socket.send(new Blob([data], { type: 'application/json' })); }); @@ -48,11 +73,23 @@ socket.close(); term.dispose(); }; - }); + } -
    -
    +
    + {#if connectState !== 'connected'} +
    + {connectState} + {#if connectState === 'disconnected'} + + {/if} +
    + {/if} +
    +
    +