Changeset 0.21.5 (#91)

This commit is contained in:
2026-03-01 20:35:10 +00:00
parent d67cfd37c2
commit aadba77887
15 changed files with 1499 additions and 14 deletions

View File

@@ -60,6 +60,9 @@ var routeTable = map[string]Direction{
"notification.new": DirToClient, // targeted via Hub.SendToUser, not room-based
"notification.read": DirToClient, // badge sync across tabs
// Workspace (v0.21.5)
"workspace.file.": DirToClient, // file changed events for live editor updates
// Plugin hooks — never cross the wire
"plugin.hook.": DirLocal,
"internal.": DirLocal,

View File

@@ -38,7 +38,8 @@ type updateChannelRequest struct {
IsPinned *bool `json:"is_pinned,omitempty"`
Folder *string `json:"folder,omitempty"`
Tags []string `json:"tags,omitempty"`
Settings *json.RawMessage `json:"settings,omitempty"` // JSONB merge into existing settings
Settings *json.RawMessage `json:"settings,omitempty"` // JSONB merge into existing settings
WorkspaceID *string `json:"workspace_id,omitempty"` // bind workspace (v0.21.5)
}
type channelResponse struct {
@@ -54,6 +55,7 @@ type channelResponse struct {
IsPinned bool `json:"is_pinned"`
Folder *string `json:"folder"`
ProjectID *string `json:"project_id,omitempty"`
WorkspaceID *string `json:"workspace_id,omitempty"`
Tags []string `json:"tags"`
Settings json.RawMessage `json:"settings,omitempty"`
MessageCount int `json:"message_count"`
@@ -178,7 +180,7 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
// Fetch channels with message count
query := `
SELECT c.id, c.user_id, c.title, c.type, c.description, c.model, c.provider_config_id,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.project_id,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.project_id, c.workspace_id,
c.tags, c.settings,
COALESCE(mc.cnt, 0) AS message_count,
c.created_at, c.updated_at
@@ -230,7 +232,7 @@ func (h *ChannelHandler) ListChannels(c *gin.Context) {
var tags []string
err := rows.Scan(
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
scanTags(&tags), scanJSON(&ch.Settings),
&ch.MessageCount, &ch.CreatedAt, &ch.UpdatedAt,
)
@@ -377,7 +379,7 @@ func (h *ChannelHandler) GetChannel(c *gin.Context) {
var tags []string
err := database.DB.QueryRow(database.Q(`
SELECT c.id, c.user_id, c.title, c.type, c.description, c.model, c.provider_config_id,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.project_id,
c.system_prompt, c.is_archived, c.is_pinned, c.folder, c.project_id, c.workspace_id,
c.tags, c.settings,
COALESCE(mc.cnt, 0) AS message_count,
c.created_at, c.updated_at
@@ -388,7 +390,7 @@ func (h *ChannelHandler) GetChannel(c *gin.Context) {
WHERE c.id = $1 AND c.user_id = $2
`), channelID, userID).Scan(
&ch.ID, &ch.UserID, &ch.Title, &ch.Type, &ch.Description, &ch.Model, &ch.APIConfigID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID,
&ch.SystemPrompt, &ch.IsArchived, &ch.IsPinned, &ch.Folder, &ch.ProjectID, &ch.WorkspaceID,
scanTags(&tags), scanJSON(&ch.Settings),
&ch.MessageCount, &ch.CreatedAt, &ch.UpdatedAt,
)
@@ -484,6 +486,13 @@ func (h *ChannelHandler) UpdateChannel(c *gin.Context) {
}
args = append(args, []byte(*req.Settings))
}
if req.WorkspaceID != nil {
if *req.WorkspaceID == "" {
addClause("workspace_id", nil) // unbind
} else {
addClause("workspace_id", *req.WorkspaceID)
}
}
if len(setClauses) == 0 {
c.JSON(http.StatusBadRequest, gin.H{"error": "no fields to update"})

View File

@@ -207,6 +207,22 @@ func streamWithToolLoop(
})
sendEvent("tool_result", string(resultJSON))
// Emit workspace.file.changed for live editor updates (v0.21.5)
if hub != nil && !toolResult.IsError && workspaceID != "" {
if call.Name == "workspace_write" || call.Name == "workspace_patch" {
var toolArgs struct{ Path string `json:"path"` }
if json.Unmarshal([]byte(call.Arguments), &toolArgs) == nil && toolArgs.Path != "" {
hub.SendToUser(userID, events.Event{
Label: "workspace.file.changed",
Payload: events.MustJSON(map[string]string{
"workspace_id": workspaceID,
"path": toolArgs.Path,
}),
})
}
}
}
// Collect for persistence
result.ToolActivity = append(result.ToolActivity, map[string]interface{}{
"id": tc.ID,

View File

@@ -101,6 +101,34 @@ func (h *WorkspaceHandler) Get(c *gin.Context) {
c.JSON(http.StatusOK, w)
}
// List returns all workspaces accessible to the current user.
// Includes user-owned workspaces and those owned by the user's teams.
func (h *WorkspaceHandler) List(c *gin.Context) {
userID := getUserID(c)
ctx := c.Request.Context()
// Fetch user-owned workspaces
userWs, err := h.stores.Workspaces.ListByOwner(ctx, "user", userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
all := make([]models.Workspace, 0, len(userWs))
all = append(all, userWs...)
// Also include team-owned workspaces
teamIDs, _ := h.stores.Teams.GetUserTeamIDs(ctx, userID)
for _, tid := range teamIDs {
teamWs, err := h.stores.Workspaces.ListByOwner(ctx, "team", tid)
if err == nil {
all = append(all, teamWs...)
}
}
c.JSON(http.StatusOK, all)
}
func (h *WorkspaceHandler) Update(c *gin.Context) {
w, ok := h.loadAndAuthorize(c)
if !ok {

View File

@@ -454,6 +454,7 @@ func main() {
if wfs != nil {
wsH := handlers.NewWorkspaceHandler(stores, wfs)
protected.POST("/workspaces", wsH.Create)
protected.GET("/workspaces", wsH.List)
protected.GET("/workspaces/:id", wsH.Get)
protected.PATCH("/workspaces/:id", wsH.Update)
protected.DELETE("/workspaces/:id", wsH.Delete)