feat: shell selector (#27)

This commit is contained in:
Carlos Florêncio
2026-01-17 02:00:34 +00:00
committed by GitHub
parent 8c17c01e55
commit 64a540bc8a
21 changed files with 324 additions and 18 deletions
+1 -1
View File
@@ -417,7 +417,7 @@ func main() {
}
serviceCfg := orchestrator.DefaultServiceConfig()
orchestratorSvc := orchestrator.NewService(serviceCfg, eventBus, agentManagerClient, taskRepoAdapter, taskRepo, log)
orchestratorSvc := orchestrator.NewService(serviceCfg, eventBus, agentManagerClient, taskRepoAdapter, taskRepo, userSvc, log)
taskSvc.SetExecutionStopper(orchestratorSvc)
// Set up comment creator for saving agent responses as comments
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"os/exec"
"strings"
"sync"
"sync/atomic"
"time"
@@ -221,6 +222,7 @@ func (m *Manager) Start(ctx context.Context) error {
// Auto-create shell session if enabled
if m.cfg.ShellEnabled {
shellCfg := shell.DefaultConfig(m.cfg.WorkDir)
shellCfg.ShellCommand = preferredShellCommand(m.cfg.AgentEnv)
shellSession, err := shell.NewSession(shellCfg, m.logger)
if err != nil {
m.logger.Warn("failed to create shell session", zap.Error(err))
@@ -237,6 +239,23 @@ func (m *Manager) Start(ctx context.Context) error {
return nil
}
func preferredShellCommand(env []string) string {
if value := lookupEnvValue(env, "AGENTCTL_SHELL_COMMAND"); value != "" {
return value
}
return lookupEnvValue(env, "SHELL")
}
func lookupEnvValue(env []string, key string) string {
prefix := key + "="
for _, entry := range env {
if strings.HasPrefix(entry, prefix) {
return strings.TrimPrefix(entry, prefix)
}
}
return ""
}
// Configure sets the agent command and optional environment variables.
// This must be called before Start() if the instance was created without a command.
func (m *Manager) Configure(command string, env map[string]string) error {
@@ -8,6 +8,7 @@ import (
"os"
"os/exec"
"runtime"
"strings"
"sync"
"time"
@@ -53,9 +54,11 @@ const maxOutputBufferSize = 16 * 1024
// Config holds shell session configuration
type Config struct {
WorkDir string // Working directory (default: workspace)
Cols int // Initial terminal columns (default: 80)
Rows int // Initial terminal rows (default: 24)
WorkDir string // Working directory (default: workspace)
Cols int // Initial terminal columns (default: 80)
Rows int // Initial terminal rows (default: 24)
ShellCommand string // Optional shell command override
ShellArgs []string // Optional shell args override
}
// DefaultConfig returns the default shell configuration
@@ -109,6 +112,14 @@ func detectShell() (string, []string) {
// This is called automatically when agentctl starts.
func NewSession(cfg Config, log *logger.Logger) (*Session, error) {
shell, args := detectShell()
if cfg.ShellCommand != "" {
shell = cfg.ShellCommand
if len(cfg.ShellArgs) > 0 {
args = cfg.ShellArgs
} else {
args = defaultShellArgs(shell)
}
}
s := &Session{
logger: log.WithFields(zap.String("component", "shell")),
@@ -128,6 +139,17 @@ func NewSession(cfg Config, log *logger.Logger) (*Session, error) {
return s, nil
}
func defaultShellArgs(shellCommand string) []string {
if runtime.GOOS == "windows" {
lower := strings.ToLower(shellCommand)
if strings.Contains(lower, "pwsh") || strings.Contains(lower, "powershell") {
return []string{"-NoLogo", "-NoExit"}
}
return nil
}
return []string{"-l"}
}
// start initializes and starts the shell process
func (s *Session) start(cfg Config) error {
s.mu.Lock()
@@ -400,4 +422,3 @@ func buildShellEnv(workDir string) []string {
return env
}
@@ -522,7 +522,7 @@ func NewOrchestratorTestServer(t *testing.T) *OrchestratorTestServer {
// Create orchestrator service
cfg := orchestrator.DefaultServiceConfig()
cfg.Scheduler.ProcessInterval = 50 * time.Millisecond // Faster for tests
orchestratorSvc := orchestrator.NewService(cfg, eventBus, agentManager, taskRepoAdapter, taskRepo, log)
orchestratorSvc := orchestrator.NewService(cfg, eventBus, agentManager, taskRepoAdapter, taskRepo, nil, log)
// Create WebSocket gateway
gateway := gateways.NewGateway(log)
@@ -143,6 +143,7 @@ func agentSessionStateToV1(state models.TaskSessionState) v1.TaskSessionState {
type Executor struct {
agentManager AgentManagerClient
repo repository.Repository
shellPrefs ShellPreferenceProvider
logger *logger.Logger
// In-memory cache for fast lookups (synced with database)
@@ -160,6 +161,11 @@ type Executor struct {
type ExecutorConfig struct {
MaxConcurrent int // Maximum concurrent agent executions
WorktreeEnabled bool // Whether to use Git worktrees for agent isolation
ShellPrefs ShellPreferenceProvider
}
type ShellPreferenceProvider interface {
PreferredShell(ctx context.Context) (string, error)
}
// NewExecutor creates a new executor
@@ -171,6 +177,7 @@ func NewExecutor(agentManager AgentManagerClient, repo repository.Repository, lo
return &Executor{
agentManager: agentManager,
repo: repo,
shellPrefs: cfg.ShellPrefs,
logger: log.WithFields(zap.String("component", "executor")),
executions: make(map[string]*TaskExecution),
maxConcurrent: maxConcurrent,
@@ -185,6 +192,26 @@ func (e *Executor) SetWorktreeEnabled(enabled bool) {
e.worktreeEnabled = enabled
}
func (e *Executor) applyPreferredShellEnv(ctx context.Context, env map[string]string) map[string]string {
if e.shellPrefs == nil {
return env
}
preferred, err := e.shellPrefs.PreferredShell(ctx)
if err != nil {
return env
}
preferred = strings.TrimSpace(preferred)
if preferred == "" {
return env
}
if env == nil {
env = make(map[string]string)
}
env["AGENTCTL_SHELL_COMMAND"] = preferred
env["SHELL"] = preferred
return env
}
// LoadActiveSessionsFromDB loads active agent sessions from the database into memory
// This should be called on startup to restore state after a restart
func (e *Executor) LoadActiveSessionsFromDB(ctx context.Context) error {
@@ -354,6 +381,8 @@ func (e *Executor) ExecuteWithProfile(ctx context.Context, task *v1.Task, agentP
zap.String("agent_profile_id", agentProfileID),
zap.Bool("use_worktree", req.UseWorktree))
req.Env = e.applyPreferredShellEnv(ctx, req.Env)
// Call the AgentManager to launch the container
resp, err := e.agentManager.LaunchAgent(ctx, req)
if err != nil {
@@ -544,6 +573,8 @@ func (e *Executor) ResumeSession(ctx context.Context, task *v1.Task, session *mo
zap.String("acp_session_id", req.ACPSessionID),
zap.Bool("use_worktree", req.UseWorktree))
req.Env = e.applyPreferredShellEnv(ctx, req.Env)
resp, err := e.agentManager.LaunchAgent(ctx, req)
if err != nil {
e.logger.Error("failed to relaunch agent for session",
@@ -116,6 +116,7 @@ func NewService(
agentManager executor.AgentManagerClient,
taskRepo scheduler.TaskRepository,
repo repository.Repository,
shellPrefs executor.ShellPreferenceProvider,
log *logger.Logger,
) *Service {
svcLogger := log.WithFields(zap.String("component", "orchestrator"))
@@ -127,6 +128,7 @@ func NewService(
execCfg := executor.ExecutorConfig{
MaxConcurrent: cfg.Scheduler.MaxConcurrent,
WorktreeEnabled: cfg.WorktreeEnabled,
ShellPrefs: shellPrefs,
}
exec := executor.NewExecutor(agentManager, repo, log, execCfg)
@@ -2,6 +2,7 @@ package controller
import (
"context"
"runtime"
"github.com/kandev/kandev/internal/user/dto"
"github.com/kandev/kandev/internal/user/service"
@@ -35,7 +36,10 @@ func (c *Controller) GetUserSettings(ctx context.Context) (dto.UserSettingsRespo
if err != nil {
return dto.UserSettingsResponse{}, err
}
return dto.UserSettingsResponse{Settings: dto.FromUserSettings(settings)}, nil
return dto.UserSettingsResponse{
Settings: dto.FromUserSettings(settings),
ShellOptions: shellOptionsForOS(),
}, nil
}
func (c *Controller) UpdateUserSettings(ctx context.Context, req dto.UpdateUserSettingsRequest) (dto.UserSettingsResponse, error) {
@@ -44,9 +48,34 @@ func (c *Controller) UpdateUserSettings(ctx context.Context, req dto.UpdateUserS
BoardID: req.BoardID,
RepositoryIDs: req.RepositoryIDs,
InitialSetupComplete: req.InitialSetupComplete,
PreferredShell: req.PreferredShell,
})
if err != nil {
return dto.UserSettingsResponse{}, err
}
return dto.UserSettingsResponse{Settings: dto.FromUserSettings(settings)}, nil
return dto.UserSettingsResponse{
Settings: dto.FromUserSettings(settings),
ShellOptions: shellOptionsForOS(),
}, nil
}
func shellOptionsForOS() []dto.ShellOption {
switch runtime.GOOS {
case "windows":
return []dto.ShellOption{
{Value: "auto", Label: "System default"},
{Value: "pwsh.exe", Label: "PowerShell (pwsh)"},
{Value: "powershell.exe", Label: "Windows PowerShell"},
{Value: "cmd.exe", Label: "Command Prompt"},
{Value: "custom", Label: "Custom"},
}
default:
return []dto.ShellOption{
{Value: "auto", Label: "System default"},
{Value: "/bin/zsh", Label: "zsh"},
{Value: "/bin/bash", Label: "bash"},
{Value: "/bin/sh", Label: "sh"},
{Value: "custom", Label: "Custom"},
}
}
}
+10 -1
View File
@@ -19,6 +19,7 @@ type UserSettingsDTO struct {
BoardID string `json:"board_id"`
RepositoryIDs []string `json:"repository_ids"`
InitialSetupComplete bool `json:"initial_setup_complete"`
PreferredShell string `json:"preferred_shell"`
UpdatedAt string `json:"updated_at"`
}
@@ -28,7 +29,13 @@ type UserResponse struct {
}
type UserSettingsResponse struct {
Settings UserSettingsDTO `json:"settings"`
Settings UserSettingsDTO `json:"settings"`
ShellOptions []ShellOption `json:"shell_options"`
}
type ShellOption struct {
Value string `json:"value"`
Label string `json:"label"`
}
type UpdateUserSettingsRequest struct {
@@ -36,6 +43,7 @@ type UpdateUserSettingsRequest struct {
BoardID *string `json:"board_id,omitempty"`
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"`
PreferredShell *string `json:"preferred_shell,omitempty"`
}
func FromUser(user *models.User) UserDTO {
@@ -54,6 +62,7 @@ func FromUserSettings(settings *models.UserSettings) UserSettingsDTO {
BoardID: settings.BoardID,
RepositoryIDs: settings.RepositoryIDs,
InitialSetupComplete: settings.InitialSetupComplete,
PreferredShell: settings.PreferredShell,
UpdatedAt: settings.UpdatedAt.Format(time.RFC3339),
}
}
@@ -68,6 +68,7 @@ type httpUpdateUserSettingsRequest struct {
BoardID *string `json:"board_id,omitempty"`
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"`
PreferredShell *string `json:"preferred_shell,omitempty"`
}
func (h *Handlers) httpUpdateUserSettings(c *gin.Context) {
@@ -81,6 +82,7 @@ func (h *Handlers) httpUpdateUserSettings(c *gin.Context) {
BoardID: body.BoardID,
RepositoryIDs: body.RepositoryIDs,
InitialSetupComplete: body.InitialSetupComplete,
PreferredShell: body.PreferredShell,
})
if err != nil {
h.logger.Error("failed to update user settings", zap.Error(err))
@@ -103,6 +105,7 @@ type wsUpdateUserSettingsRequest struct {
BoardID *string `json:"board_id,omitempty"`
RepositoryIDs *[]string `json:"repository_ids,omitempty"`
InitialSetupComplete *bool `json:"initial_setup_complete,omitempty"`
PreferredShell *string `json:"preferred_shell,omitempty"`
}
func (h *Handlers) wsUpdateUserSettings(ctx context.Context, msg *ws.Message) (*ws.Message, error) {
@@ -115,6 +118,7 @@ func (h *Handlers) wsUpdateUserSettings(ctx context.Context, msg *ws.Message) (*
BoardID: req.BoardID,
RepositoryIDs: req.RepositoryIDs,
InitialSetupComplete: req.InitialSetupComplete,
PreferredShell: req.PreferredShell,
})
if err != nil {
return ws.NewError(msg.ID, msg.Action, ws.ErrorCodeInternalError, "Failed to update user settings", nil)
@@ -15,6 +15,7 @@ type UserSettings struct {
BoardID string `json:"board_id"`
RepositoryIDs []string `json:"repository_ids"`
InitialSetupComplete bool `json:"initial_setup_complete"`
PreferredShell string `json:"preferred_shell"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
@@ -3,6 +3,7 @@ package service
import (
"context"
"errors"
"strings"
"time"
"github.com/kandev/kandev/internal/common/logger"
@@ -27,6 +28,7 @@ type UpdateUserSettingsRequest struct {
BoardID *string
RepositoryIDs *[]string
InitialSetupComplete *bool
PreferredShell *string
}
func NewService(repo store.Repository, eventBus bus.EventBus, log *logger.Logger) *Service {
@@ -54,6 +56,14 @@ func (s *Service) GetUserSettings(ctx context.Context) (*models.UserSettings, er
return settings, nil
}
func (s *Service) PreferredShell(ctx context.Context) (string, error) {
settings, err := s.repo.GetUserSettings(ctx, s.defaultUser)
if err != nil {
return "", err
}
return settings.PreferredShell, nil
}
func (s *Service) UpdateUserSettings(ctx context.Context, req *UpdateUserSettingsRequest) (*models.UserSettings, error) {
settings, err := s.repo.GetUserSettings(ctx, s.defaultUser)
if err != nil {
@@ -71,6 +81,9 @@ func (s *Service) UpdateUserSettings(ctx context.Context, req *UpdateUserSetting
if req.InitialSetupComplete != nil {
settings.InitialSetupComplete = *req.InitialSetupComplete
}
if req.PreferredShell != nil {
settings.PreferredShell = strings.TrimSpace(*req.PreferredShell)
}
settings.UpdatedAt = time.Now().UTC()
if err := s.repo.UpsertUserSettings(ctx, settings); err != nil {
return nil, err
@@ -89,6 +102,7 @@ func (s *Service) publishUserSettingsEvent(ctx context.Context, settings *models
"board_id": settings.BoardID,
"repository_ids": settings.RepositoryIDs,
"initial_setup_complete": settings.InitialSetupComplete,
"preferred_shell": settings.PreferredShell,
"updated_at": settings.UpdatedAt.Format(time.RFC3339),
}
if err := s.eventBus.Publish(ctx, events.UserSettingsUpdated, bus.NewEvent(events.UserSettingsUpdated, "user-service", data)); err != nil {
@@ -191,6 +191,7 @@ func (r *SQLiteRepository) UpsertUserSettings(ctx context.Context, settings *mod
"board_id": settings.BoardID,
"repository_ids": settings.RepositoryIDs,
"initial_setup_complete": settings.InitialSetupComplete,
"preferred_shell": settings.PreferredShell,
})
if err != nil {
return err
@@ -233,6 +234,7 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin
BoardID string `json:"board_id"`
RepositoryIDs []string `json:"repository_ids"`
InitialSetupComplete bool `json:"initial_setup_complete"`
PreferredShell string `json:"preferred_shell"`
}
if err := json.Unmarshal([]byte(settingsRaw), &payload); err != nil {
return nil, err
@@ -241,5 +243,6 @@ func scanUserSettings(scanner interface{ Scan(dest ...any) error }, userID strin
settings.BoardID = payload.BoardID
settings.RepositoryIDs = payload.RepositoryIDs
settings.InitialSetupComplete = payload.InitialSetupComplete
settings.PreferredShell = payload.PreferredShell
return settings, nil
}
+6 -5
View File
@@ -11,9 +11,10 @@ type User struct {
}
type UserSettings struct {
UserID string `json:"user_id"`
WorkspaceID string `json:"workspace_id"`
BoardID string `json:"board_id"`
RepositoryIDs []string `json:"repository_ids"`
UpdatedAt time.Time `json:"updated_at"`
UserID string `json:"user_id"`
WorkspaceID string `json:"workspace_id"`
BoardID string `json:"board_id"`
RepositoryIDs []string `json:"repository_ids"`
PreferredShell string `json:"preferred_shell"`
UpdatedAt time.Time `json:"updated_at"`
}
+1
View File
@@ -53,6 +53,7 @@ export default async function Page({ searchParams }: PageProps) {
workspaceId: settingsWorkspaceId,
boardId: settingsBoardId,
repositoryIds: settingsRepositoryIds,
preferredShell: userSettings?.preferred_shell || null,
loaded: Boolean(userSettings),
},
};
+147 -1
View File
@@ -1,14 +1,18 @@
'use client';
import { useState, useSyncExternalStore } from 'react';
import { useEffect, useState, useSyncExternalStore } from 'react';
import { useTheme } from 'next-themes';
import { IconPalette, IconCode, IconServer } from '@tabler/icons-react';
import { Card, CardContent, CardHeader, CardTitle } from '@kandev/ui/card';
import { Button } from '@kandev/ui/button';
import { Label } from '@kandev/ui/label';
import { Input } from '@kandev/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@kandev/ui/select';
import { Separator } from '@kandev/ui/separator';
import { SettingsSection } from '@/components/settings/settings-section';
import { useAppStore, useAppStoreApi } from '@/components/state-provider';
import { fetchUserSettings, updateUserSettings } from '@/lib/http';
import { useRequest } from '@/lib/http/use-request';
import { SETTINGS_DATA } from '@/lib/settings/dummy-data';
import { getBackendConfig } from '@/lib/config';
import type { Theme, Editor } from '@/lib/settings/types';
@@ -20,6 +24,30 @@ export default function GeneralSettingsPage() {
SETTINGS_DATA.general.customEditorCommand || ''
);
const [backendUrl] = useState<string>(() => getBackendConfig().apiBaseUrl);
const [preferredShell, setPreferredShell] = useState('');
const [customShell, setCustomShell] = useState('');
const [shellSelection, setShellSelection] = useState('auto');
const [baselineShell, setBaselineShell] = useState('');
const [shellLoaded, setShellLoaded] = useState(false);
const [shellOptions, setShellOptions] = useState<Array<{ value: string; label: string }>>([]);
const setUserSettings = useAppStore((state) => state.setUserSettings);
const storeApi = useAppStoreApi();
const saveShellRequest = useRequest(async () => {
const trimmed = preferredShell.trim();
const currentSettings = storeApi.getState().userSettings;
await updateUserSettings({
workspace_id: currentSettings.workspaceId ?? '',
board_id: currentSettings.boardId ?? '',
repository_ids: currentSettings.repositoryIds,
preferred_shell: trimmed,
});
setBaselineShell(trimmed);
setUserSettings({
...currentSettings,
preferredShell: trimmed || null,
loaded: true,
});
});
const mounted = useSyncExternalStore(
() => () => undefined,
() => true,
@@ -27,6 +55,45 @@ export default function GeneralSettingsPage() {
);
const displayBackendUrl = backendUrl.replace(/^https?:\/\//, '').replace(/\/$/, '');
const shellDirty = preferredShell.trim() !== baselineShell.trim();
useEffect(() => {
let isActive = true;
fetchUserSettings({ cache: 'no-store' })
.then((data) => {
if (!isActive || !data?.settings) return;
const shellValue = data.settings.preferred_shell || '';
setPreferredShell(shellValue);
setBaselineShell(shellValue);
const nextShellOptions = data.shell_options ?? [];
setShellOptions(nextShellOptions);
if (shellValue === '') {
setShellSelection('auto');
setCustomShell('');
} else if (nextShellOptions.some((option) => option.value === shellValue)) {
setShellSelection(shellValue);
setCustomShell('');
} else {
setShellSelection('custom');
setCustomShell(shellValue);
}
setShellLoaded(true);
const currentSettings = storeApi.getState().userSettings;
setUserSettings({
...currentSettings,
preferredShell: shellValue || null,
loaded: true,
});
})
.catch(() => {
if (isActive) {
setShellLoaded(true);
}
});
return () => {
isActive = false;
};
}, [setUserSettings, storeApi]);
if (!mounted) {
return null;
@@ -121,6 +188,85 @@ export default function GeneralSettingsPage() {
<Separator />
<SettingsSection
icon={<IconCode className="h-5 w-5" />}
title="Shell"
description="Pick the default shell for task sessions"
>
<Card>
<CardHeader className="flex flex-row items-center justify-between gap-4">
<CardTitle className="text-base">Preferred Shell</CardTitle>
<Button
type="button"
onClick={() => saveShellRequest.run()}
disabled={!shellLoaded || !shellDirty || saveShellRequest.isLoading}
>
{saveShellRequest.isLoading ? 'Saving...' : 'Save'}
</Button>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Select
value={shellSelection}
onValueChange={(value) => {
setShellSelection(value);
if (value === 'auto') {
setPreferredShell('');
setCustomShell('');
return;
}
if (value === 'custom') {
setPreferredShell(customShell);
return;
}
setPreferredShell(value);
setCustomShell('');
}}
disabled={!shellLoaded || shellOptions.length === 0}
>
<SelectTrigger>
<SelectValue
placeholder={
shellOptions.length === 0
? 'Shell options unavailable'
: 'Select a shell'
}
/>
</SelectTrigger>
<SelectContent>
{shellOptions.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{shellSelection === 'custom' && (
<div className="space-y-2">
<Input
value={customShell}
onChange={(event) => {
const nextValue = event.target.value;
setCustomShell(nextValue);
setPreferredShell(nextValue);
}}
placeholder="/bin/zsh"
/>
<p className="text-xs text-muted-foreground">
Enter a shell path or command available in the agent environment.
</p>
</div>
)}
<p className="text-xs text-muted-foreground">
New task sessions will use this shell. Existing sessions keep their current shell.
</p>
</CardContent>
</Card>
</SettingsSection>
<Separator />
<SettingsSection
icon={<IconServer className="h-5 w-5" />}
title="Backend Connection"
+10 -1
View File
@@ -9,6 +9,7 @@ type DisplaySettings = {
workspaceId: string | null;
boardId: string | null;
repositoryIds: string[];
preferredShell: string | null;
loaded: boolean;
};
@@ -23,6 +24,7 @@ type CommitPayload = {
workspaceId: string | null;
boardId: string | null;
repositoryIds: string[];
preferredShell?: string | null;
};
export function useUserDisplaySettings({
@@ -38,7 +40,13 @@ export function useUserDisplaySettings({
const commitSettings = useCallback(
(next: CommitPayload) => {
const repositoryIds = Array.from(new Set(next.repositoryIds)).sort();
const normalized: DisplaySettings = { ...next, repositoryIds, loaded: true };
const normalized: DisplaySettings = {
workspaceId: next.workspaceId,
boardId: next.boardId,
repositoryIds,
preferredShell: next.preferredShell ?? userSettings.preferredShell ?? null,
loaded: true,
};
const sameWorkspace = normalized.workspaceId === userSettings.workspaceId;
const sameBoard = normalized.boardId === userSettings.boardId;
const sameRepos =
@@ -77,6 +85,7 @@ export function useUserDisplaySettings({
workspaceId: data.settings.workspace_id || null,
boardId: data.settings.board_id || null,
repositoryIds,
preferredShell: data.settings.preferred_shell || null,
loaded: true,
});
})
+6 -1
View File
@@ -156,7 +156,12 @@ export async function fetchUserSettings(options?: ApiRequestOptions) {
}
export async function updateUserSettings(
payload: { workspace_id: string; board_id: string; repository_ids: string[] },
payload: {
workspace_id: string;
board_id: string;
repository_ids: string[];
preferred_shell?: string;
},
options?: ApiRequestOptions
) {
return fetchJson<UserSettingsResponse>('/api/v1/user/settings', {
+8 -1
View File
@@ -105,6 +105,7 @@ export type UserSettingsState = {
workspaceId: string | null;
boardId: string | null;
repositoryIds: string[];
preferredShell: string | null;
loaded: boolean;
};
@@ -281,7 +282,13 @@ const defaultState: AppState = {
tasks: { activeTaskId: null },
agents: { agents: [] },
agentProfiles: { items: [], version: 0 },
userSettings: { workspaceId: null, boardId: null, repositoryIds: [], loaded: false },
userSettings: {
workspaceId: null,
boardId: null,
repositoryIds: [],
preferredShell: null,
loaded: false,
},
terminal: { terminals: [] },
shell: { outputs: {}, statuses: {} },
diffs: { files: [] },
+1
View File
@@ -231,6 +231,7 @@ export type UserSettingsUpdatedPayload = {
board_id: string;
repository_ids: string[];
initial_setup_complete?: boolean;
preferred_shell?: string;
updated_at?: string;
};
+2
View File
@@ -172,11 +172,13 @@ export type UserSettings = {
board_id: string;
repository_ids: string[];
initial_setup_complete?: boolean;
preferred_shell?: string;
updated_at: string;
};
export type UserSettingsResponse = {
settings: UserSettings;
shell_options?: Array<{ value: string; label: string }>;
};
export type UserResponse = {
+1
View File
@@ -12,6 +12,7 @@ export function registerUsersHandlers(store: StoreApi<AppState>): WsHandlers {
workspaceId: message.payload.workspace_id || null,
boardId: message.payload.board_id || null,
repositoryIds,
preferredShell: message.payload.preferred_shell || null,
loaded: true,
},
}));