merge: resolve package.json conflict with upstream/main

Keep @radix-ui/react-popover from feature branch and adopt
upstream's @tabler/icons-react ^3.43.0 version bump.
This commit is contained in:
SiYue-ZO 2026-05-08 13:50:33 +08:00
commit bcf05e5522
22 changed files with 1503 additions and 190 deletions

6
go.mod
View file

@ -1,9 +1,9 @@
module github.com/sipeed/picoclaw
go 1.25.9
go 1.25.10
require (
fyne.io/systray v1.12.0
fyne.io/systray v1.12.1
github.com/SevereCloud/vksdk/v3 v3.3.1
github.com/adhocore/gronx v1.19.6
github.com/anthropics/anthropic-sdk-go v1.26.0
@ -118,7 +118,7 @@ require (
github.com/github/copilot-sdk/go v0.2.0
github.com/go-resty/resty/v2 v2.17.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/jsonschema-go v0.4.2
github.com/google/jsonschema-go v0.4.3
github.com/grbit/go-json v0.11.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect

8
go.sum
View file

@ -3,8 +3,8 @@ aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/SevereCloud/vksdk/v3 v3.3.1 h1:O86zsp5LQnHE+O5acvuXM/s6S1LyxzVTkF6+Lup0Jyg=
@ -142,8 +142,8 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+DQPd0=
github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

View file

@ -58,6 +58,7 @@ type AgentLoop struct {
hookRuntime hookRuntime
steering *steeringQueue
pendingSkills sync.Map
pendingStops sync.Map
mu sync.RWMutex
// workerSem limits concurrent turn processing workers.
@ -177,6 +178,10 @@ func (al *AgentLoop) Run(ctx context.Context) error {
phase: TurnPhaseSetup,
}
if _, loaded := al.activeTurnStates.LoadOrStore(sessionKey, placeholder); loaded {
if al.tryHandleStopCommand(ctx, msg, sessionKey) {
continue
}
// Another turn is already active (or reserved) for this session — enqueue
if err := al.enqueueSteeringMessage(sessionKey, agentID, providers.Message{
Role: "user",
@ -240,6 +245,24 @@ func (al *AgentLoop) Run(ctx context.Context) error {
defer al.channelManager.InvokeTypingStop(m.Channel, m.ChatID)
}
if al.takePendingStop(sessionKey) {
al.activeTurnStates.Delete(sessionKey)
target := &continuationTarget{
SessionKey: sessionKey,
Channel: m.Channel,
ChatID: m.ChatID,
}
continued, continueErr := al.drainQueuedSteeringContinuations(ctx, target)
if continueErr != nil {
al.maybePublishError(ctx, m.Channel, m.ChatID, sessionKey, continueErr)
return
}
if continued != "" {
al.PublishResponseIfNeeded(ctx, target.Channel, target.ChatID, target.SessionKey, continued)
}
return
}
al.runTurnWithSteering(ctx, m)
}(msg)

View file

@ -274,6 +274,12 @@ func (al *AgentLoop) buildCommandsRuntime(
return nil
},
}
rt.StopActiveTurn = func() (commands.StopResult, error) {
if opts == nil {
return commands.StopResult{}, fmt.Errorf("process options not available")
}
return al.stopActiveTurnForSession(opts.Dispatch.SessionKey)
}
if agent != nil && agent.ContextBuilder != nil {
rt.ListSkillNames = agent.ContextBuilder.ListSkillNames
}

View file

@ -337,5 +337,20 @@ func registerSharedTools(
} else if (spawnEnabled || spawnStatusEnabled) && !cfg.Tools.IsToolEnabled("subagent") {
logger.WarnCF("agent", "spawn/spawn_status tools require subagent to be enabled", nil)
}
// Register delegate tool for multi-agent setups.
// Auto-enabled when multiple agents exist. Delegation uses the SubTurn
// mechanism directly (not SubagentManager) and is independent of the
// subagent tool.
if len(registry.ListAgentIDs()) > 1 {
delegateTool := tools.NewDelegateTool()
delegateTool.SetSpawner(NewSubTurnSpawner(al))
currentAgentID := agentID
delegateTool.SetSelfAgentID(currentAgentID)
delegateTool.SetAllowlistChecker(func(targetAgentID string) bool {
return registry.CanSpawnSubagent(currentAgentID, targetAgentID)
})
agent.Tools.Register(delegateTool)
}
}
}

View file

@ -44,11 +44,36 @@ func (al *AgentLoop) runTurnWithSteering(ctx context.Context, initialMsg bus.Inb
return
}
// Drain steering queue using existing Continue mechanism
continued, continueErr := al.drainQueuedSteeringContinuations(ctx, target)
if continueErr != nil {
logger.WarnCF("agent", "Failed to continue queued steering",
map[string]any{
"channel": target.Channel,
"chat_id": target.ChatID,
"error": continueErr.Error(),
})
} else if continued != "" {
finalResponse = continued
}
// Publish final response
if finalResponse != "" {
al.PublishResponseIfNeeded(ctx, target.Channel, target.ChatID, target.SessionKey, finalResponse)
}
}
func (al *AgentLoop) drainQueuedSteeringContinuations(
ctx context.Context,
target *continuationTarget,
) (string, error) {
if target == nil {
return "", nil
}
finalResponse := ""
for al.pendingSteeringCountForScope(target.SessionKey) > 0 {
// Check for context cancellation between iterations
if ctx.Err() != nil {
return
if err := ctx.Err(); err != nil {
return finalResponse, err
}
logger.InfoCF("agent", "Continuing queued steering after turn end",
@ -61,13 +86,7 @@ func (al *AgentLoop) runTurnWithSteering(ctx context.Context, initialMsg bus.Inb
continued, continueErr := al.Continue(ctx, target.SessionKey, target.Channel, target.ChatID)
if continueErr != nil {
logger.WarnCF("agent", "Failed to continue queued steering",
map[string]any{
"channel": target.Channel,
"chat_id": target.ChatID,
"error": continueErr.Error(),
})
break
return finalResponse, continueErr
}
if continued == "" {
break
@ -75,10 +94,7 @@ func (al *AgentLoop) runTurnWithSteering(ctx context.Context, initialMsg bus.Inb
finalResponse = continued
}
// Publish final response
if finalResponse != "" {
al.PublishResponseIfNeeded(ctx, target.Channel, target.ChatID, target.SessionKey, finalResponse)
}
return finalResponse, nil
}
func (al *AgentLoop) resolveSteeringTarget(msg bus.InboundMessage) (string, string, bool) {

122
pkg/agent/agent_stop.go Normal file
View file

@ -0,0 +1,122 @@
package agent
import (
"context"
"fmt"
"strings"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/commands"
)
func (al *AgentLoop) tryHandleStopCommand(
ctx context.Context,
msg bus.InboundMessage,
sessionKey string,
) bool {
cmdName, ok := commands.CommandName(msg.Content)
if !ok || cmdName != "stop" {
return false
}
result, err := al.stopActiveTurnForSession(sessionKey)
// This function is only called when loaded=true (another turn already
// claimed this session). If stopActiveTurnForSession found a pending
// placeholder but didn't stop it, that placeholder belongs to the other
// message's worker which hasn't started yet — arm a pending stop so the
// worker will bail when it checks before running.
if err == nil && !result.Stopped {
if ts := al.getActiveTurnState(sessionKey); ts != nil {
snap := ts.snapshot()
if strings.HasPrefix(snap.TurnID, pendingTurnPrefix) {
al.markPendingStop(sessionKey)
result.Stopped = true
}
}
}
reply := commands.FormatStopReply(result)
if err != nil {
reply = "Failed to stop task: " + err.Error()
}
if al.channelManager != nil {
al.channelManager.InvokeTypingStop(msg.Channel, msg.ChatID)
}
al.resetMessageToolRound(sessionKey)
al.PublishResponseIfNeeded(ctx, msg.Channel, msg.ChatID, sessionKey, reply)
return true
}
func (al *AgentLoop) stopActiveTurnForSession(sessionKey string) (commands.StopResult, error) {
sessionKey = strings.TrimSpace(sessionKey)
if sessionKey == "" {
return commands.StopResult{}, fmt.Errorf("session key is required")
}
result := commands.StopResult{}
cleared := al.clearSteeringMessagesForScope(sessionKey)
al.clearPendingSkills(sessionKey)
ts := al.getActiveTurnState(sessionKey)
if ts == nil {
result.Stopped = cleared > 0
return result, nil
}
snap := ts.snapshot()
result.TaskName = snap.UserMessage
if strings.HasPrefix(snap.TurnID, pendingTurnPrefix) {
// A pending placeholder means this session is either idle (our own
// placeholder from the /stop command) or another message is queued but
// hasn't started yet. In both cases, we don't arm a pending stop here;
// the caller (tryHandleStopCommand) handles the "another message queued"
// case explicitly, since it knows loaded=true.
return result, nil
}
if err := al.HardAbort(sessionKey); err != nil {
if al.getActiveTurnState(sessionKey) == nil {
result.Stopped = cleared > 0
return result, nil
}
return commands.StopResult{}, err
}
result.Stopped = true
return result, nil
}
func (al *AgentLoop) markPendingStop(sessionKey string) {
sessionKey = strings.TrimSpace(sessionKey)
if sessionKey == "" {
return
}
al.pendingStops.Store(sessionKey, struct{}{})
}
func (al *AgentLoop) takePendingStop(sessionKey string) bool {
sessionKey = strings.TrimSpace(sessionKey)
if sessionKey == "" {
return false
}
_, ok := al.pendingStops.LoadAndDelete(sessionKey)
return ok
}
func (al *AgentLoop) resetMessageToolRound(sessionKey string) {
if strings.TrimSpace(sessionKey) == "" {
return
}
if registry := al.GetRegistry(); registry != nil {
if agent := registry.GetDefaultAgent(); agent != nil {
if tool, ok := agent.Tools.Get("message"); ok {
if resetter, ok := tool.(interface{ ResetSentInRound(sessionKey string) }); ok {
resetter.ResetSentInRound(sessionKey)
}
}
}
}
}

View file

@ -156,6 +156,18 @@ func (sq *steeringQueue) lenScope(scope string) int {
return len(sq.queues[normalizeSteeringScope(scope)])
}
func (sq *steeringQueue) clearScope(scope string) int {
sq.mu.Lock()
defer sq.mu.Unlock()
scope = normalizeSteeringScope(scope)
count := len(sq.queues[scope])
if count > 0 {
delete(sq.queues, scope)
}
return count
}
// setMode updates the steering mode.
func (sq *steeringQueue) setMode(mode SteeringMode) {
sq.mu.Lock()
@ -290,6 +302,13 @@ func (al *AgentLoop) pendingSteeringCountForScope(scope string) int {
return al.steering.lenScope(scope)
}
func (al *AgentLoop) clearSteeringMessagesForScope(scope string) int {
if al.steering == nil {
return 0
}
return al.steering.clearScope(scope)
}
func (al *AgentLoop) continueWithSteeringMessages(
ctx context.Context,
agent *AgentInstance,
@ -511,6 +530,10 @@ func (al *AgentLoop) HardAbort(sessionKey string) error {
"initial_history_length": ts.initialHistoryLength,
})
// Cancel the active provider/tool turn contexts immediately so long-running
// execution stops as soon as possible on the root turn.
_ = ts.requestHardAbort()
// IMPORTANT: Trigger cascading cancellation FIRST to stop all child SubTurns
// from adding more messages to the session. This prevents race conditions
// where rollback happens while children are still writing.

View file

@ -840,6 +840,191 @@ func TestAgentLoop_Run_AutoContinuesLateSteeringMessage(t *testing.T) {
}
}
func TestAgentLoop_Run_PendingStopStillContinuesQueuedFollowUp(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
ModelName: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
MaxParallelTurns: 1,
},
},
}
msgBus := bus.NewMessageBus()
provider := &lateSteeringProvider{
firstCallStarted: make(chan struct{}),
releaseFirstCall: make(chan struct{}),
}
al := NewAgentLoop(cfg, msgBus, provider)
runCtx, cancelRun := context.WithCancel(context.Background())
defer cancelRun()
runErrCh := make(chan error, 1)
go func() {
runErrCh <- al.Run(runCtx)
}()
defer func() {
cancelRun()
select {
case err := <-runErrCh:
if err != nil {
t.Fatalf("Run() error = %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for Run to stop")
}
}()
blockerSessionKey := session.BuildOpaqueSessionKey("agent:main:test:blocker")
targetSessionKey := session.BuildOpaqueSessionKey("agent:main:test:target")
blockerCtx := bus.InboundContext{
Channel: "test",
ChatID: "blocker-chat",
ChatType: "direct",
SenderID: "user1",
}
targetCtx := bus.InboundContext{
Channel: "test",
ChatID: "target-chat",
ChatType: "direct",
SenderID: "user1",
}
if err := msgBus.PublishInbound(context.Background(), bus.InboundMessage{
Context: blockerCtx,
Content: "block worker pool",
SessionKey: blockerSessionKey,
}); err != nil {
t.Fatalf("PublishInbound(blocker) error = %v", err)
}
select {
case <-provider.firstCallStarted:
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for blocker turn to start")
}
if err := msgBus.PublishInbound(context.Background(), bus.InboundMessage{
Context: targetCtx,
Content: "skip this turn",
SessionKey: targetSessionKey,
}); err != nil {
t.Fatalf("PublishInbound(target start) error = %v", err)
}
deadline := time.Now().Add(2 * time.Second)
for {
ts := al.getActiveTurnState(targetSessionKey)
if ts != nil && strings.HasPrefix(ts.turnID, pendingTurnPrefix) {
break
}
if time.Now().After(deadline) {
t.Fatal("timeout waiting for pending placeholder")
}
time.Sleep(10 * time.Millisecond)
}
if err := msgBus.PublishInbound(context.Background(), bus.InboundMessage{
Context: targetCtx,
Content: "/stop",
SessionKey: targetSessionKey,
}); err != nil {
t.Fatalf("PublishInbound(/stop) error = %v", err)
}
deadline = time.Now().Add(2 * time.Second)
stopSeen := false
for !stopSeen {
select {
case outbound := <-msgBus.OutboundChan():
if outbound.ChatID == "target-chat" && outbound.Content == "Task stopped. Current task was canceled." {
stopSeen = true
}
case <-time.After(10 * time.Millisecond):
if time.Now().After(deadline) {
t.Fatal("timeout waiting for /stop reply")
}
}
}
if err := msgBus.PublishInbound(context.Background(), bus.InboundMessage{
Context: targetCtx,
Content: "run this instead",
SessionKey: targetSessionKey,
}); err != nil {
t.Fatalf("PublishInbound(follow-up) error = %v", err)
}
deadline = time.Now().Add(2 * time.Second)
for al.pendingSteeringCountForScope(targetSessionKey) == 0 {
if time.Now().After(deadline) {
t.Fatal("timeout waiting for follow-up to enter scoped steering queue")
}
time.Sleep(10 * time.Millisecond)
}
close(provider.releaseFirstCall)
deadline = time.Now().Add(5 * time.Second)
followUpSeen := false
for !followUpSeen {
select {
case outbound := <-msgBus.OutboundChan():
if outbound.ChatID == "target-chat" && outbound.Content == "continued response" {
followUpSeen = true
}
case <-time.After(10 * time.Millisecond):
if time.Now().After(deadline) {
t.Fatal("timeout waiting for queued follow-up continuation")
}
}
}
deadline = time.Now().Add(2 * time.Second)
for {
if al.GetActiveTurnBySession(targetSessionKey) == nil &&
al.pendingSteeringCountForScope(targetSessionKey) == 0 {
break
}
if time.Now().After(deadline) {
t.Fatal("timeout waiting for target session to go idle")
}
time.Sleep(10 * time.Millisecond)
}
provider.mu.Lock()
calls := provider.calls
secondMessages := append([]providers.Message(nil), provider.secondCallMessages...)
provider.mu.Unlock()
if calls != 2 {
t.Fatalf("expected 2 provider calls (blocker + continuation), got %d", calls)
}
foundFollowUp := false
for _, msg := range secondMessages {
if msg.Role == "user" && msg.Content == "run this instead" {
foundFollowUp = true
}
if msg.Role == "user" && msg.Content == "skip this turn" {
t.Fatalf("unexpected canceled message in continuation context: %q", msg.Content)
}
}
if !foundFollowUp {
t.Fatal("expected queued follow-up to be processed after pending stop")
}
}
func TestAgentLoop_Steering_DirectResponseContinuesWithQueuedMessage(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
@ -1392,6 +1577,149 @@ func TestAgentLoop_InterruptHard_RestoresSession(t *testing.T) {
}
}
func TestAgentLoop_StopCommand_AbortsActiveTurnAndClearsQueuedSteering(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "agent-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
ModelName: "test-model",
MaxTokens: 4096,
MaxToolIterations: 10,
},
},
}
msgBus := bus.NewMessageBus()
provider := &toolCallProvider{
toolCalls: []providers.ToolCall{
{
ID: "call_1",
Type: "function",
Name: "cancel_tool",
Function: &providers.FunctionCall{
Name: "cancel_tool",
Arguments: "{}",
},
Arguments: map[string]any{},
},
},
finalResp: "should not continue",
}
al := NewAgentLoop(cfg, msgBus, provider)
started := make(chan struct{})
al.RegisterTool(&interruptibleTool{name: "cancel_tool", started: started})
sessionKey := session.BuildMainSessionKey(routing.DefaultAgentID)
runCtx, cancelRun := context.WithCancel(context.Background())
defer cancelRun()
runErrCh := make(chan error, 1)
go func() {
runErrCh <- al.Run(runCtx)
}()
defer func() {
cancelRun()
select {
case err := <-runErrCh:
if err != nil {
t.Fatalf("Run() error = %v", err)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for Run to stop")
}
}()
baseMsg := testInboundMessage(bus.InboundMessage{
Context: bus.InboundContext{
Channel: "test",
ChatID: "chat1",
ChatType: "direct",
SenderID: "user1",
},
SessionKey: sessionKey,
})
if err := msgBus.PublishInbound(context.Background(), bus.InboundMessage{
Context: baseMsg.Context,
Content: "do work",
SessionKey: sessionKey,
}); err != nil {
t.Fatalf("PublishInbound(start) error = %v", err)
}
select {
case <-started:
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for interruptible tool to start")
}
if err := msgBus.PublishInbound(context.Background(), bus.InboundMessage{
Context: baseMsg.Context,
Content: "follow up after cancel",
SessionKey: sessionKey,
}); err != nil {
t.Fatalf("PublishInbound(follow-up) error = %v", err)
}
deadline := time.Now().Add(2 * time.Second)
for al.pendingSteeringCountForScope(sessionKey) == 0 {
if time.Now().After(deadline) {
t.Fatal("timeout waiting for follow-up message to enter steering queue")
}
time.Sleep(10 * time.Millisecond)
}
if err := msgBus.PublishInbound(context.Background(), bus.InboundMessage{
Context: baseMsg.Context,
Content: "/stop",
SessionKey: sessionKey,
}); err != nil {
t.Fatalf("PublishInbound(/stop) error = %v", err)
}
select {
case outbound := <-msgBus.OutboundChan():
want := "Task stopped. \"do work\" was canceled."
if outbound.Content != want {
t.Fatalf("stop reply = %q, want %q", outbound.Content, want)
}
case <-time.After(2 * time.Second):
t.Fatal("timeout waiting for /stop reply")
}
deadline = time.Now().Add(5 * time.Second)
for al.GetActiveTurnBySession(sessionKey) != nil {
if time.Now().After(deadline) {
t.Fatal("timeout waiting for active turn to stop")
}
time.Sleep(10 * time.Millisecond)
}
if got := al.pendingSteeringCountForScope(sessionKey); got != 0 {
t.Fatalf("expected cleared steering queue, got %d pending message(s)", got)
}
select {
case outbound := <-msgBus.OutboundChan():
t.Fatalf("unexpected outbound after stop: %q", outbound.Content)
case <-time.After(300 * time.Millisecond):
}
provider.mu.Lock()
calls := provider.calls
provider.mu.Unlock()
if calls != 1 {
t.Fatalf("expected provider to stop before follow-up turn, got %d calls", calls)
}
}
// capturingMockProvider captures messages sent to Chat for inspection.
type capturingMockProvider struct {
response string

View file

@ -174,7 +174,10 @@ type SubTurnConfig struct {
// Used by team tool to enforce token limits across all team members.
InitialTokenBudget *atomic.Int64
// Can be extended with temperature, topP, etc.
// TargetAgentID, when set, runs the sub-turn as the specified agent.
// The target agent's workspace, model, tools, and system prompt are used
// instead of the caller's. If empty, the sub-turn runs as the parent agent.
TargetAgentID string
}
// ====================== Context Keys ======================
@ -232,6 +235,7 @@ func (s *AgentLoopSpawner) SpawnSubTurn(
Critical: cfg.Critical,
Timeout: cfg.Timeout,
MaxContextRunes: cfg.MaxContextRunes,
TargetAgentID: cfg.TargetAgentID,
}
return spawnSubTurn(ctx, s.al, parentTS, agentCfg)
@ -314,8 +318,9 @@ func spawnSubTurn(
return nil, ErrDepthLimitExceeded
}
// 2. Config validation
if cfg.Model == "" {
// 2. Config validation: Model is required unless TargetAgentID is set
// (the target agent provides its own model).
if cfg.Model == "" && cfg.TargetAgentID == "" {
return nil, ErrInvalidSubTurnConfig
}
@ -333,12 +338,22 @@ func spawnSubTurn(
childID := al.generateSubTurnID()
// Get the agent instance from parent, falling back to the default agent.
// Wrap it in a shallow copy that uses an ephemeral (in-memory only) session store
// so that child turns never pollute or persist to the parent's session history.
baseAgent := parentTS.agent
if baseAgent == nil {
baseAgent = al.registry.GetDefaultAgent()
// Resolve the agent instance for the child turn.
// When TargetAgentID is set, look up that agent from the registry so the
// child runs with the target's workspace, model, tools, and system prompt.
// Otherwise fall back to the parent's agent (existing behavior).
var baseAgent *AgentInstance
if cfg.TargetAgentID != "" {
var ok bool
baseAgent, ok = al.registry.GetAgent(cfg.TargetAgentID)
if !ok {
return nil, fmt.Errorf("target agent %q not found in registry", cfg.TargetAgentID)
}
} else {
baseAgent = parentTS.agent
if baseAgent == nil {
baseAgent = al.registry.GetDefaultAgent()
}
}
if baseAgent == nil {
return nil, errors.New("parent turnState has no agent instance")

View file

@ -4,6 +4,9 @@ import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"sync"
"testing"
"time"
@ -2122,3 +2125,206 @@ func TestSubTurn_IndependentContext(t *testing.T) {
t.Log("✓ SubTurn completed successfully (independent context)")
}
}
// ====================== TargetAgentID Tests ======================
// modelRecordingProvider captures the model passed to Chat for test assertions.
type modelRecordingProvider struct {
mu sync.Mutex
lastModel string
}
func (rp *modelRecordingProvider) Chat(
_ context.Context,
_ []providers.Message,
_ []providers.ToolDefinition,
model string,
_ map[string]any,
) (*providers.LLMResponse, error) {
rp.mu.Lock()
rp.lastModel = model
rp.mu.Unlock()
return &providers.LLMResponse{Content: "Mock response"}, nil
}
func (rp *modelRecordingProvider) GetDefaultModel() string { return "mock-model" }
func (rp *modelRecordingProvider) getLastModel() string {
rp.mu.Lock()
defer rp.mu.Unlock()
return rp.lastModel
}
// newMultiAgentLoop creates an AgentLoop with two named agents for testing
// cross-agent delegation via TargetAgentID.
func newMultiAgentLoop(t *testing.T, provider providers.LLMProvider) (*AgentLoop, func()) {
t.Helper()
tmpDir, err := os.MkdirTemp("", "multiagent-test-*")
if err != nil {
t.Fatalf("create temp dir: %v", err)
}
alphaDir := filepath.Join(tmpDir, "alpha")
betaDir := filepath.Join(tmpDir, "beta")
os.MkdirAll(alphaDir, 0o755)
os.MkdirAll(betaDir, 0o755)
cfg := &config.Config{
Agents: config.AgentsConfig{
Defaults: config.AgentDefaults{
Workspace: tmpDir,
ModelName: "default-model",
MaxTokens: 4096,
MaxToolIterations: 10,
},
List: []config.AgentConfig{
{
ID: "alpha",
Workspace: alphaDir,
Model: &config.AgentModelConfig{Primary: "model-alpha"},
},
{
ID: "beta",
Workspace: betaDir,
Model: &config.AgentModelConfig{Primary: "model-beta"},
},
},
},
}
msgBus := bus.NewMessageBus()
al := NewAgentLoop(cfg, msgBus, provider)
return al, func() { os.RemoveAll(tmpDir) }
}
func TestSpawnSubTurn_TargetAgentID_UsesTargetAgent(t *testing.T) {
rp := &modelRecordingProvider{}
al, cleanup := newMultiAgentLoop(t, rp)
defer cleanup()
alphaAgent, ok := al.registry.GetAgent("alpha")
if !ok {
t.Fatal("alpha agent not in registry")
}
// Parent is alpha, target is beta
parent := &turnState{
ctx: context.Background(),
turnID: "parent-alpha",
depth: 0,
childTurnIDs: []string{},
pendingResults: make(chan *tools.ToolResult, 4),
concurrencySem: make(chan struct{}, testMaxConcurrentSubTurns),
session: &ephemeralSessionStore{},
agent: alphaAgent,
}
result, err := spawnSubTurn(context.Background(), al, parent, SubTurnConfig{
TargetAgentID: "beta",
SystemPrompt: "task for beta",
})
if err != nil {
t.Fatalf("spawnSubTurn failed: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
// The recording provider captures the model passed to Chat().
// If TargetAgentID works correctly, the child turn should have
// used beta's model, not alpha's.
if got := rp.getLastModel(); got != "model-beta" {
t.Errorf("child turn used model %q, want %q", got, "model-beta")
}
}
func TestSpawnSubTurn_TargetAgentID_NotFound(t *testing.T) {
al, cleanup := newMultiAgentLoop(t, &mockProvider{})
defer cleanup()
alphaAgent, _ := al.registry.GetAgent("alpha")
parent := &turnState{
ctx: context.Background(),
turnID: "parent-alpha",
depth: 0,
childTurnIDs: []string{},
pendingResults: make(chan *tools.ToolResult, 4),
concurrencySem: make(chan struct{}, testMaxConcurrentSubTurns),
session: &ephemeralSessionStore{},
agent: alphaAgent,
}
_, err := spawnSubTurn(context.Background(), al, parent, SubTurnConfig{
TargetAgentID: "nonexistent",
SystemPrompt: "task",
})
if err == nil {
t.Fatal("expected error for nonexistent agent")
}
if !strings.Contains(err.Error(), "not found") {
t.Errorf("error should mention 'not found', got: %v", err)
}
}
func TestSpawnSubTurn_TargetAgentID_EmptyModelAccepted(t *testing.T) {
al, cleanup := newMultiAgentLoop(t, &mockProvider{})
defer cleanup()
alphaAgent, _ := al.registry.GetAgent("alpha")
parent := &turnState{
ctx: context.Background(),
turnID: "parent-alpha",
depth: 0,
childTurnIDs: []string{},
pendingResults: make(chan *tools.ToolResult, 4),
concurrencySem: make(chan struct{}, testMaxConcurrentSubTurns),
session: &ephemeralSessionStore{},
agent: alphaAgent,
}
// Model is empty but TargetAgentID is set — should NOT fail validation
result, err := spawnSubTurn(context.Background(), al, parent, SubTurnConfig{
Model: "", // intentionally empty
TargetAgentID: "beta",
SystemPrompt: "task for beta",
})
if err != nil {
t.Fatalf("should accept empty Model when TargetAgentID is set, got: %v", err)
}
if result == nil {
t.Fatal("expected non-nil result")
}
}
func TestDelegateToolNotRegistered_SingleAgent(t *testing.T) {
// Single-agent setup: delegate should not be registered
al, _, _, provider, cleanup := newTestAgentLoop(t)
_ = provider
defer cleanup()
agent := al.registry.GetDefaultAgent()
if agent == nil {
t.Fatal("default agent should exist")
}
if _, has := agent.Tools.Get("delegate"); has {
t.Error("delegate tool should not be registered in single-agent setup")
}
}
func TestDelegateToolRegistered_MultiAgent(t *testing.T) {
al, cleanup := newMultiAgentLoop(t, &mockProvider{})
defer cleanup()
// Both agents should have the delegate tool
for _, id := range []string{"alpha", "beta"} {
agent, ok := al.registry.GetAgent(id)
if !ok {
t.Fatalf("agent %q not found", id)
}
if _, has := agent.Tools.Get("delegate"); !has {
t.Errorf("agent %q should have delegate tool in multi-agent setup", id)
}
}
}

View file

@ -26,6 +26,10 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel
al.registerActiveTurn(ts)
defer al.clearActiveTurn(ts)
if al.takePendingStop(ts.sessionKey) {
_ = ts.requestHardAbort()
}
turnStatus := TurnEndStatusCompleted
defer func() {
al.emitEvent(
@ -40,6 +44,11 @@ func (al *AgentLoop) runTurn(ctx context.Context, ts *turnState, pipeline *Pipel
)
}()
if ts.hardAbortRequested() {
turnStatus = TurnEndStatusAborted
return al.abortTurn(ts)
}
al.emitEvent(
runtimeevents.KindAgentTurnStart,
ts.eventMeta("runTurn", "turn.start"),

View file

@ -256,7 +256,10 @@ func newTurnState(agent *AgentInstance, opts processOptions, scope turnEventScop
// Bind session store and capture initial history length for rollback logic
if agent != nil && agent.Sessions != nil {
ts.session = agent.Sessions
ts.initialHistoryLength = len(agent.Sessions.GetHistory(opts.Dispatch.SessionKey))
history := agent.Sessions.GetHistory(opts.Dispatch.SessionKey)
ts.initialHistoryLength = len(history)
ts.restorePointHistory = append([]providers.Message(nil), history...)
ts.restorePointSummary = agent.Sessions.GetSummary(opts.Dispatch.SessionKey)
}
return ts

View file

@ -8,6 +8,7 @@ func BuiltinDefinitions() []Definition {
return []Definition{
startCommand(),
helpCommand(),
stopCommand(),
showCommand(),
listCommand(),
useCommand(),

View file

@ -42,6 +42,9 @@ func TestBuiltinHelpHandler_ReturnsFormattedMessage(t *testing.T) {
if !strings.Contains(reply, "/list [models|channels|agents|skills|mcp]") {
t.Fatalf("/help reply missing /list usage, got %q", reply)
}
if !strings.Contains(reply, "/stop") {
t.Fatalf("/help reply missing /stop usage, got %q", reply)
}
if !strings.Contains(reply, "/use <skill> <message>") {
if !strings.Contains(reply, "/use <skill> [message]") {
t.Fatalf("/help reply missing /use usage, got %q", reply)
@ -49,6 +52,59 @@ func TestBuiltinHelpHandler_ReturnsFormattedMessage(t *testing.T) {
}
}
func TestBuiltinStop_UsesRuntimeStopper(t *testing.T) {
rt := &Runtime{
StopActiveTurn: func() (StopResult, error) {
return StopResult{
Stopped: true,
TaskName: "sync the long running job",
}, nil
},
}
defs := BuiltinDefinitions()
ex := NewExecutor(NewRegistry(defs), rt)
var reply string
res := ex.Execute(context.Background(), Request{
Text: "/stop",
Reply: func(text string) error {
reply = text
return nil
},
})
if res.Outcome != OutcomeHandled {
t.Fatalf("/stop: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
}
if reply != "Task stopped. \"sync the long running job\" was canceled." {
t.Fatalf("/stop reply=%q", reply)
}
}
func TestBuiltinStop_NoActiveTask(t *testing.T) {
rt := &Runtime{
StopActiveTurn: func() (StopResult, error) {
return StopResult{}, nil
},
}
defs := BuiltinDefinitions()
ex := NewExecutor(NewRegistry(defs), rt)
var reply string
res := ex.Execute(context.Background(), Request{
Text: "/stop",
Reply: func(text string) error {
reply = text
return nil
},
})
if res.Outcome != OutcomeHandled {
t.Fatalf("/stop: outcome=%v, want=%v", res.Outcome, OutcomeHandled)
}
if reply != "No active task to stop." {
t.Fatalf("/stop reply=%q, want no-active message", reply)
}
}
func TestBuiltinShowChannel_PreservesUserVisibleBehavior(t *testing.T) {
defs := BuiltinDefinitions()
ex := NewExecutor(NewRegistry(defs), nil)

52
pkg/commands/cmd_stop.go Normal file
View file

@ -0,0 +1,52 @@
package commands
import (
"context"
"fmt"
"strings"
)
func stopCommand() Definition {
return Definition{
Name: "stop",
Description: "Stop the current task",
Usage: "/stop",
Handler: func(_ context.Context, req Request, rt *Runtime) error {
if rt == nil || rt.StopActiveTurn == nil {
return req.Reply(unavailableMsg)
}
result, err := rt.StopActiveTurn()
if err != nil {
return req.Reply("Failed to stop task: " + err.Error())
}
return req.Reply(FormatStopReply(result))
},
}
}
// FormatStopReply renders a user-facing reply for a stop request.
func FormatStopReply(result StopResult) string {
if !result.Stopped {
return "No active task to stop."
}
taskName := compactStopTaskName(result.TaskName)
if taskName == "" {
return "Task stopped. Current task was canceled."
}
return fmt.Sprintf("Task stopped. %q was canceled.", taskName)
}
func compactStopTaskName(taskName string) string {
taskName = strings.Join(strings.Fields(strings.TrimSpace(taskName)), " ")
if taskName == "" {
return ""
}
if len(taskName) > 80 {
return taskName[:77] + "..."
}
return taskName
}

View file

@ -36,6 +36,12 @@ type ContextStats struct {
MessageCount int
}
// StopResult describes the outcome of a stop request for the current session.
type StopResult struct {
Stopped bool
TaskName string
}
// Runtime provides runtime dependencies to command handlers. It is constructed
// per-request by the agent loop so that per-request state (like session scope)
// can coexist with long-lived callbacks (like GetModelInfo).
@ -55,4 +61,5 @@ type Runtime struct {
SwitchChannel func(value string) error
ClearHistory func() error
ReloadConfig func() error
StopActiveTurn func() (StopResult, error)
}

104
pkg/tools/delegate.go Normal file
View file

@ -0,0 +1,104 @@
package tools
import (
"context"
"fmt"
"strings"
"github.com/sipeed/picoclaw/pkg/routing"
)
// DelegateTool delegates a task to a specific named agent and waits for
// the result. Unlike spawn (async, fire-and-forget) or subagent (sync but
// generic), delegate targets a named agent and runs the task using that
// agent's own workspace, model, and tools.
type DelegateTool struct {
spawner SubTurnSpawner
allowlistCheck func(targetAgentID string) bool
selfAgentID string
}
func NewDelegateTool() *DelegateTool {
return &DelegateTool{}
}
func (t *DelegateTool) SetSpawner(spawner SubTurnSpawner) {
t.spawner = spawner
}
func (t *DelegateTool) SetAllowlistChecker(check func(targetAgentID string) bool) {
t.allowlistCheck = check
}
func (t *DelegateTool) SetSelfAgentID(id string) {
t.selfAgentID = id
}
func (t *DelegateTool) Name() string {
return "delegate"
}
func (t *DelegateTool) Description() string {
return "Delegate a task to another agent and wait for the result. " +
"Use this when another agent is better suited to handle a specific task " +
"based on their capabilities. The target agent runs with its own workspace, " +
"model, and tools."
}
func (t *DelegateTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"agent_id": map[string]any{
"type": "string",
"description": "The ID of the target agent to delegate the task to",
},
"task": map[string]any{
"type": "string",
"description": "Clear description of the task to delegate",
},
},
"required": []string{"agent_id", "task"},
}
}
func (t *DelegateTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
rawAgentID, _ := args["agent_id"].(string)
if strings.TrimSpace(rawAgentID) == "" {
return ErrorResult("agent_id is required and must be a non-empty string")
}
agentID := routing.NormalizeAgentID(rawAgentID)
task, _ := args["task"].(string)
if strings.TrimSpace(task) == "" {
return ErrorResult("task is required and must be a non-empty string")
}
if t.selfAgentID != "" && agentID == t.selfAgentID {
return ErrorResult("cannot delegate to self")
}
if t.allowlistCheck != nil && !t.allowlistCheck(agentID) {
return ErrorResult(fmt.Sprintf("not allowed to delegate to agent %q", agentID))
}
if t.spawner == nil {
return ErrorResult("delegate tool not configured")
}
result, err := t.spawner.SpawnSubTurn(ctx, SubTurnConfig{
TargetAgentID: agentID,
SystemPrompt: task,
Async: false,
})
if err != nil {
return ErrorResult(fmt.Sprintf("delegation to agent %q failed: %v", agentID, err)).WithError(err)
}
if result == nil {
return ErrorResult(fmt.Sprintf("delegation to agent %q returned no result", agentID))
}
result.ForLLM = fmt.Sprintf("[Response from agent %q]\n%s", agentID, result.ForLLM)
return result
}

300
pkg/tools/delegate_test.go Normal file
View file

@ -0,0 +1,300 @@
package tools
import (
"context"
"fmt"
"strings"
"testing"
)
// delegateMockSpawner records the config and returns a canned result.
type delegateMockSpawner struct {
lastCfg SubTurnConfig
result *ToolResult
err error
}
func (m *delegateMockSpawner) SpawnSubTurn(_ context.Context, cfg SubTurnConfig) (*ToolResult, error) {
m.lastCfg = cfg
if m.err != nil {
return nil, m.err
}
if m.result != nil {
return m.result, nil
}
return &ToolResult{
ForLLM: "completed: " + cfg.SystemPrompt,
ForUser: "completed",
}, nil
}
func TestDelegateTool_Name(t *testing.T) {
tool := NewDelegateTool()
if tool.Name() != "delegate" {
t.Errorf("Name() = %q, want %q", tool.Name(), "delegate")
}
}
func TestDelegateTool_Parameters(t *testing.T) {
tool := NewDelegateTool()
params := tool.Parameters()
props, ok := params["properties"].(map[string]any)
if !ok {
t.Fatal("properties should be a map")
}
_, hasAgentID := props["agent_id"]
if !hasAgentID {
t.Error("agent_id parameter should exist")
}
_, hasTask := props["task"]
if !hasTask {
t.Error("task parameter should exist")
}
required, ok := params["required"].([]string)
if !ok {
t.Fatal("required should be a string array")
}
if len(required) != 2 {
t.Fatalf("required should have 2 entries, got %d", len(required))
}
}
func TestDelegateTool_Execute_Success(t *testing.T) {
spawner := &delegateMockSpawner{}
tool := NewDelegateTool()
tool.SetSpawner(spawner)
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "researcher",
"task": "summarize the logs",
})
if result.IsError {
t.Fatalf("expected success, got error: %s", result.ForLLM)
}
if !strings.Contains(result.ForLLM, `[Response from agent "researcher"]`) {
t.Errorf("result should contain attribution, got: %s", result.ForLLM)
}
if !strings.Contains(result.ForLLM, "summarize the logs") {
t.Errorf("result should contain task output, got: %s", result.ForLLM)
}
// Verify spawner received correct config
if spawner.lastCfg.TargetAgentID != "researcher" {
t.Errorf("TargetAgentID = %q, want %q", spawner.lastCfg.TargetAgentID, "researcher")
}
if spawner.lastCfg.Async {
t.Error("delegate should be synchronous (Async=false)")
}
if spawner.lastCfg.SystemPrompt != "summarize the logs" {
t.Errorf("SystemPrompt = %q, want %q", spawner.lastCfg.SystemPrompt, "summarize the logs")
}
}
func TestDelegateTool_Execute_EmptyAgentID(t *testing.T) {
tests := []struct {
name string
args map[string]any
}{
{"missing", map[string]any{"task": "test"}},
{"empty string", map[string]any{"agent_id": "", "task": "test"}},
{"whitespace only", map[string]any{"agent_id": " ", "task": "test"}},
{"wrong type", map[string]any{"agent_id": 123, "task": "test"}},
}
tool := NewDelegateTool()
tool.SetSpawner(&delegateMockSpawner{})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tool.Execute(context.Background(), tt.args)
if !result.IsError {
t.Error("expected error for invalid agent_id")
}
if !strings.Contains(result.ForLLM, "agent_id is required") {
t.Errorf("error should mention agent_id, got: %s", result.ForLLM)
}
})
}
}
func TestDelegateTool_Execute_EmptyTask(t *testing.T) {
tests := []struct {
name string
args map[string]any
}{
{"missing", map[string]any{"agent_id": "a"}},
{"empty string", map[string]any{"agent_id": "a", "task": ""}},
{"whitespace only", map[string]any{"agent_id": "a", "task": "\t\n"}},
}
tool := NewDelegateTool()
tool.SetSpawner(&delegateMockSpawner{})
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := tool.Execute(context.Background(), tt.args)
if !result.IsError {
t.Error("expected error for invalid task")
}
if !strings.Contains(result.ForLLM, "task is required") {
t.Errorf("error should mention task, got: %s", result.ForLLM)
}
})
}
}
func TestDelegateTool_Execute_PermissionDenied(t *testing.T) {
tool := NewDelegateTool()
tool.SetSpawner(&delegateMockSpawner{})
tool.SetAllowlistChecker(func(targetAgentID string) bool {
return targetAgentID == "allowed-agent"
})
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "forbidden-agent",
"task": "test",
})
if !result.IsError {
t.Error("expected error for denied agent")
}
if !strings.Contains(result.ForLLM, "not allowed to delegate") {
t.Errorf("error should mention permission, got: %s", result.ForLLM)
}
}
func TestDelegateTool_Execute_PermissionAllowed(t *testing.T) {
tool := NewDelegateTool()
tool.SetSpawner(&delegateMockSpawner{})
tool.SetAllowlistChecker(func(targetAgentID string) bool {
return targetAgentID == "allowed-agent"
})
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "allowed-agent",
"task": "test",
})
if result.IsError {
t.Errorf("expected success for allowed agent, got error: %s", result.ForLLM)
}
}
func TestDelegateTool_Execute_NoSpawner(t *testing.T) {
tool := NewDelegateTool()
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "a",
"task": "test",
})
if !result.IsError {
t.Error("expected error when spawner is nil")
}
if !strings.Contains(result.ForLLM, "not configured") {
t.Errorf("error should mention not configured, got: %s", result.ForLLM)
}
}
func TestDelegateTool_Execute_SpawnerError(t *testing.T) {
spawner := &delegateMockSpawner{
err: fmt.Errorf("context deadline exceeded"),
}
tool := NewDelegateTool()
tool.SetSpawner(spawner)
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "researcher",
"task": "test",
})
if !result.IsError {
t.Error("expected error when spawner fails")
}
if !strings.Contains(result.ForLLM, "delegation to agent") {
t.Errorf("error should mention delegation failure, got: %s", result.ForLLM)
}
if !strings.Contains(result.ForLLM, "context deadline exceeded") {
t.Errorf("error should propagate cause, got: %s", result.ForLLM)
}
}
func TestDelegateTool_Execute_NoAllowlistCheck(t *testing.T) {
// When no allowlist checker is set, all agents are allowed
tool := NewDelegateTool()
tool.SetSpawner(&delegateMockSpawner{})
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "any-agent",
"task": "test",
})
if result.IsError {
t.Errorf("expected success without allowlist, got error: %s", result.ForLLM)
}
}
func TestDelegateTool_Execute_NilResult(t *testing.T) {
tool := NewDelegateTool()
tool.SetSpawner(&nilResultSpawner{})
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "researcher",
"task": "test",
})
if !result.IsError {
t.Error("expected error for nil result")
}
if !strings.Contains(result.ForLLM, "returned no result") {
t.Errorf("error should mention no result, got: %s", result.ForLLM)
}
}
func TestDelegateTool_Execute_SelfDelegation(t *testing.T) {
tool := NewDelegateTool()
tool.SetSpawner(&delegateMockSpawner{})
tool.SetSelfAgentID("alpha")
result := tool.Execute(context.Background(), map[string]any{
"agent_id": "alpha",
"task": "test",
})
if !result.IsError {
t.Error("expected error for self-delegation")
}
if !strings.Contains(result.ForLLM, "cannot delegate to self") {
t.Errorf("error should mention self-delegation, got: %s", result.ForLLM)
}
}
func TestDelegateTool_Execute_SelfDelegation_Normalized(t *testing.T) {
tool := NewDelegateTool()
tool.SetSpawner(&delegateMockSpawner{})
tool.SetSelfAgentID("alpha") // stored normalized
// Case-insensitive and whitespace variants should still be caught
variants := []string{"ALPHA", " Alpha ", " alpha "}
for _, v := range variants {
t.Run(v, func(t *testing.T) {
result := tool.Execute(context.Background(), map[string]any{
"agent_id": v,
"task": "test",
})
if !result.IsError {
t.Errorf("agent_id=%q should be caught as self-delegation", v)
}
})
}
}
// nilResultSpawner always returns (nil, nil).
type nilResultSpawner struct{}
func (m *nilResultSpawner) SpawnSubTurn(_ context.Context, _ SubTurnConfig) (*ToolResult, error) {
return nil, nil
}

View file

@ -30,6 +30,7 @@ type SubTurnConfig struct {
ActualSystemPrompt string
InitialMessages []providers.Message
InitialTokenBudget *atomic.Int64 // Shared token budget for team members; nil if no budget
TargetAgentID string // If set, run as this agent (its workspace, model, tools)
}
type SubagentTask struct {

View file

@ -19,7 +19,7 @@
"dependencies": {
"@fontsource-variable/inter": "^5.2.8",
"@radix-ui/react-popover": "^1.1.15",
"@tabler/icons-react": "^3.40.0",
"@tabler/icons-react": "^3.43.0",
"@tailwindcss/vite": "^4.2.4",
"@tanstack/react-query": "^5.99.0",
"@tanstack/react-router": "^1.169.2",
@ -29,20 +29,20 @@
"cmdk": "^1.1.1",
"dayjs": "^1.11.20",
"highlight.js": "^11.11.1",
"i18next": "^26.0.8",
"i18next": "^26.0.10",
"i18next-browser-languagedetector": "^8.2.1",
"jotai": "^2.19.1",
"radix-ui": "^1.4.3",
"react": "19.2.5",
"react-dom": "19.2.5",
"react-i18next": "^17.0.4",
"react-i18next": "^17.0.6",
"react-markdown": "^10.1.0",
"react-textarea-autosize": "^8.5.9",
"rehype-highlight": "^7.0.2",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^4.0.1",
"shadcn": "^4.3.0",
"shadcn": "^4.7.0",
"sonner": "^2.0.7",
"tailwind-merge": "^3.5.0",
"tailwindcss": "^4.2.4",
@ -63,7 +63,7 @@
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.5.0",
"globals": "^17.6.0",
"prettier": "^3.8.3",
"prettier-plugin-tailwindcss": "^0.7.2",
"typescript": "~5.9.3",

View file

@ -15,8 +15,8 @@ importers:
specifier: ^1.1.15
version: 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
'@tabler/icons-react':
specifier: ^3.40.0
version: 3.41.1(react@19.2.5)
specifier: ^3.43.0
version: 3.43.0(react@19.2.5)
'@tailwindcss/vite':
specifier: ^4.2.4
version: 4.2.4(vite@8.0.10(@types/node@25.6.0)(esbuild@0.27.4)(jiti@2.7.0)(tsx@4.21.0))
@ -45,8 +45,8 @@ importers:
specifier: ^11.11.1
version: 11.11.1
i18next:
specifier: ^26.0.8
version: 26.0.8(typescript@5.9.3)
specifier: ^26.0.10
version: 26.0.10(typescript@5.9.3)
i18next-browser-languagedetector:
specifier: ^8.2.1
version: 8.2.1
@ -63,8 +63,8 @@ importers:
specifier: 19.2.5
version: 19.2.5(react@19.2.5)
react-i18next:
specifier: ^17.0.4
version: 17.0.4(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3)
specifier: ^17.0.6
version: 17.0.6(i18next@26.0.10(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3)
react-markdown:
specifier: ^10.1.0
version: 10.1.0(@types/react@19.2.14)(react@19.2.5)
@ -84,8 +84,8 @@ importers:
specifier: ^4.0.1
version: 4.0.1
shadcn:
specifier: ^4.3.0
version: 4.3.0(@types/node@25.6.0)(typescript@5.9.3)
specifier: ^4.7.0
version: 4.7.0(@types/node@25.6.0)(typescript@5.9.3)
sonner:
specifier: ^2.0.7
version: 2.0.7(react-dom@19.2.5(react@19.2.5))(react@19.2.5)
@ -142,8 +142,8 @@ importers:
specifier: ^0.5.2
version: 0.5.2(eslint@10.2.1(jiti@2.7.0))
globals:
specifier: ^17.5.0
version: 17.5.0
specifier: ^17.6.0
version: 17.6.0
prettier:
specifier: ^3.8.3
version: 3.8.3
@ -166,8 +166,8 @@ packages:
resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==}
engines: {node: '>=6.9.0'}
'@babel/compat-data@7.29.0':
resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==}
'@babel/compat-data@7.29.3':
resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==}
engines: {node: '>=6.9.0'}
'@babel/core@7.29.0':
@ -186,8 +186,8 @@ packages:
resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==}
engines: {node: '>=6.9.0'}
'@babel/helper-create-class-features-plugin@7.28.6':
resolution: {integrity: sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==}
'@babel/helper-create-class-features-plugin@7.29.3':
resolution: {integrity: sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==}
engines: {node: '>=6.9.0'}
peerDependencies:
'@babel/core': ^7.0.0
@ -249,6 +249,11 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/parser@7.29.3':
resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==}
engines: {node: '>=6.0.0'}
hasBin: true
'@babel/plugin-syntax-jsx@7.28.6':
resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==}
engines: {node: '>=6.9.0'}
@ -295,8 +300,8 @@ packages:
resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==}
engines: {node: '>=6.9.0'}
'@dotenvx/dotenvx@1.61.0':
resolution: {integrity: sha512-utL3cpZoFzflyqUkjYbxYujI6STBTmO5LFn4bbin/NZnRWN6wQ7eErhr3/Vpa5h/jicPFC6kTa42r940mQftJQ==}
'@dotenvx/dotenvx@1.65.0':
resolution: {integrity: sha512-v4FA/Lw3pTEloLxBqTOaYDX6MNo0Jo7lGBsPZhwnJBqRJp0AzQg1ZZNxrFsh6HVC6QWeWrfIKLn0y2eyIXaVDg==}
hasBin: true
'@ecies/ciphers@0.2.6':
@ -553,8 +558,8 @@ packages:
resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
'@inquirer/confirm@6.0.11':
resolution: {integrity: sha512-pTpHjg0iEIRMYV/7oCZUMf27/383E6Wyhfc/MY+AVQGEoUobffIYWOK9YLP2XFRGz/9i6WlTQh1CkFVIo2Y7XA==}
'@inquirer/confirm@6.0.12':
resolution: {integrity: sha512-h9FgGun3QwVYNj5TWIZZ+slii73bMoBFjPfVIGtnFuL4t8gBiNDV9PcSfIzkuxvgquJKt9nr1QzszpBzTbH8Og==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
@ -562,8 +567,8 @@ packages:
'@types/node':
optional: true
'@inquirer/core@11.1.8':
resolution: {integrity: sha512-/u+yJk2pOKNDOh1ZgdUH2RQaRx6OOH4I0uwL95qPvTFTIL38YBsuSC4r1yXBB3Q6JvNqFFc202gk0Ew79rrcjA==}
'@inquirer/core@11.1.9':
resolution: {integrity: sha512-BDE4fG22uYh1bGSifcj7JSx119TVYNViMhMu85usp4Fswrzh6M0DV3yld64jA98uOAa2GSQ4Bg4bZRm2d2cwSg==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
@ -610,8 +615,8 @@ packages:
'@cfworker/json-schema':
optional: true
'@mswjs/interceptors@0.41.3':
resolution: {integrity: sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==}
'@mswjs/interceptors@0.41.8':
resolution: {integrity: sha512-pRLMNKTSGRoLq+KnEB/7OY5vijw1XmcheAAOiv6pj7W1FG32kAGqj1C/RK/cqxRGr1Fh+zBi8sDur8kj3EQv6A==}
engines: {node: '>=18'}
'@napi-rs/wasm-runtime@1.1.4':
@ -1457,13 +1462,13 @@ packages:
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
engines: {node: '>=18'}
'@tabler/icons-react@3.41.1':
resolution: {integrity: sha512-kUgweE+DJtAlMZVIns1FTDdcbpRVnkK7ZpUOXmoxy3JAF0rSHj0TcP4VHF14+gMJGnF+psH2Zt26BLT6owetBA==}
'@tabler/icons-react@3.43.0':
resolution: {integrity: sha512-rXUuCQEeRbEk3lJxs3gwzdtaaITSwc/JUbp+AkqsGff5uBpzZw7eKPDk53xKoKLyjrbj82Ai4GuVG0kO89Jf5g==}
peerDependencies:
react: '>= 16'
'@tabler/icons@3.41.1':
resolution: {integrity: sha512-OaRnVbRmH2nHtFeg+RmMJ/7m2oBIF9XCJAUD5gQnMrpK9f05ydj8MZrAf3NZQqOXyxGN1UBL0D5IKLLEUfr74Q==}
'@tabler/icons@3.43.0':
resolution: {integrity: sha512-qXwS17Op9jqr3Asvu31fejyw8+OnRDKH7oR8nQXyUgW1pI44ET8OKG9kssy+XIvvAIyej6gZdGmviNUn1VMfPw==}
'@tailwindcss/node@4.2.4':
resolution: {integrity: sha512-Ai7+yQPxz3ddrDQzFfBKdHEVBg0w3Zl83jnjuwxnZOsnH9pGn93QHQtpU0p/8rYWxvbFZHneni6p1BSLK4DkGA==}
@ -1890,8 +1895,8 @@ packages:
ajv@6.14.0:
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
ajv@8.18.0:
resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==}
ajv@8.20.0:
resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@ -1941,8 +1946,8 @@ packages:
resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==}
engines: {node: 18 || 20 || >=22}
baseline-browser-mapping@2.10.17:
resolution: {integrity: sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==}
baseline-browser-mapping@2.10.27:
resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==}
engines: {node: '>=6.0.0'}
hasBin: true
@ -1990,8 +1995,8 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
caniuse-lite@1.0.30001787:
resolution: {integrity: sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==}
caniuse-lite@1.0.30001792:
resolution: {integrity: sha512-hVLMUZFgR4JJ6ACt1uEESvQN1/dBVqPAKY0hgrV70eN3391K6juAfTjKZLKvOMsx8PxA7gsY1/tLMMTcfFLLpw==}
ccount@2.0.1:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
@ -2203,8 +2208,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.334:
resolution: {integrity: sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==}
electron-to-chromium@1.5.352:
resolution: {integrity: sha512-9wHk8x6dyuimoe18EdiDPWKExNdxYqo4fn4FwOVVper6RxT3cmpBwBkWWfSOCYJjQdIco/nPhJhNLmn4Ufg1Yg==}
emoji-regex@10.6.0:
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
@ -2334,8 +2339,8 @@ packages:
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
engines: {node: '>= 0.6'}
eventsource-parser@3.0.6:
resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==}
eventsource-parser@3.0.8:
resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==}
engines: {node: '>=18.0.0'}
eventsource@3.0.7:
@ -2350,8 +2355,8 @@ packages:
resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==}
engines: {node: ^18.19.0 || >=20.5.0}
express-rate-limit@8.3.2:
resolution: {integrity: sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==}
express-rate-limit@8.5.1:
resolution: {integrity: sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==}
engines: {node: '>= 16'}
peerDependencies:
express: '>= 4.11'
@ -2382,8 +2387,8 @@ packages:
fast-string-width@3.0.2:
resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==}
fast-uri@3.1.0:
resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==}
fast-uri@3.1.2:
resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==}
fast-wrap-ansi@0.2.0:
resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==}
@ -2443,8 +2448,8 @@ packages:
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
engines: {node: '>= 0.8'}
fs-extra@11.3.4:
resolution: {integrity: sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==}
fs-extra@11.3.5:
resolution: {integrity: sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==}
engines: {node: '>=14.14'}
fsevents@2.3.3:
@ -2505,8 +2510,8 @@ packages:
resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
engines: {node: '>=10.13.0'}
globals@17.5.0:
resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==}
globals@17.6.0:
resolution: {integrity: sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==}
engines: {node: '>=18'}
goober@2.1.18:
@ -2521,16 +2526,16 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
graphql@16.13.2:
resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==}
graphql@16.14.0:
resolution: {integrity: sha512-BBvQ/406p+4CZbTpCbVPSxfzrZrbnuWSP1ELYgyS6B+hNeKzgrdB4JczCa5VZUBQrDa9hUngm0KnexY6pJRN5Q==}
engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0}
has-symbols@1.1.0:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'}
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
hasown@2.0.3:
resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==}
engines: {node: '>= 0.4'}
hast-util-from-parse5@8.0.3:
@ -2576,8 +2581,8 @@ packages:
resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==}
engines: {node: '>=12.0.0'}
hono@4.12.14:
resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==}
hono@4.12.18:
resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==}
engines: {node: '>=16.9.0'}
html-parse-stringify@3.0.1:
@ -2608,8 +2613,8 @@ packages:
i18next-browser-languagedetector@8.2.1:
resolution: {integrity: sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw==}
i18next@26.0.8:
resolution: {integrity: sha512-BRzLom0mhDhV9v0QhgUUHWQJuwFmnr1194xEcNLYD6ym8y8s542n4jXUvRLnhNTbh9PmpU6kGZamyuGHQMsGjw==}
i18next@26.0.10:
resolution: {integrity: sha512-k3yGPAlWR2RdMYoVXJoDZDT87qeHIWKH7gVksdZMpRty7QX/D9QZeYGvN08KGbKHke9wn01eYT+EEsrqX/YTlw==}
peerDependencies:
typescript: ^5 || ^6
peerDependenciesMeta:
@ -2642,8 +2647,8 @@ packages:
inline-style-parser@0.2.7:
resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==}
ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
ip-address@10.2.0:
resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==}
engines: {node: '>= 12'}
ipaddr.js@1.9.1:
@ -2759,8 +2764,8 @@ packages:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
jose@6.2.2:
resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==}
jose@6.2.3:
resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
jotai@2.19.1:
resolution: {integrity: sha512-sqm9lVZiqBHZH8aSRk32DSiZDHY3yUIlulXYn9GQj7/LvoUdYXSMti7ZPJGo+6zjzKFt5a25k/I6iBCi43PJcw==}
@ -2815,8 +2820,8 @@ packages:
engines: {node: '>=6'}
hasBin: true
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
jsonfile@6.2.1:
resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==}
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
@ -3118,8 +3123,8 @@ packages:
ms@2.1.3:
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
msw@2.13.4:
resolution: {integrity: sha512-fPlKBeFe+8rpcyR3umUmmHuNwu6gc6T3STvkgEa9WDX/HEgal9wDeflpCUAIRtmvaLZM2igfI5y1bZ9G5J26KA==}
msw@2.14.4:
resolution: {integrity: sha512-HVPZJ9Rx4nDCWhjNQ57lKQGSE+0zDHw0xWE2IN2rLOUTLkagEBWNlvWuKYNwG2pQWq96TMd8NiSK/6vO1udnWQ==}
engines: {node: '>=18'}
hasBin: true
peerDependencies:
@ -3137,6 +3142,11 @@ packages:
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@ -3153,8 +3163,8 @@ packages:
resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
node-releases@2.0.37:
resolution: {integrity: sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==}
node-releases@2.0.38:
resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==}
normalize-path@3.0.0:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
@ -3297,6 +3307,10 @@ packages:
resolution: {integrity: sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==}
engines: {node: ^10 || ^12 || >=14}
postcss@8.5.14:
resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==}
engines: {node: ^10 || ^12 || >=14}
powershell-utils@0.1.0:
resolution: {integrity: sha512-dM0jVuXJPsDN6DvRpea484tCUaMiXWjuCn++HGTqUWzGDjv5tZkEZldAJ/UMlqRYGFrD/etByo4/xOuC/snX2A==}
engines: {node: '>=20'}
@ -3417,8 +3431,8 @@ packages:
peerDependencies:
react: ^19.2.5
react-i18next@17.0.4:
resolution: {integrity: sha512-hQipmK4EF0y6RO6tt6WuqnmWpWYEXmQUUzecmMBuNsIgYd3smXcG4GtYPWhvgxn0pqMOItKlEO8H24HCs5hc3g==}
react-i18next@17.0.6:
resolution: {integrity: sha512-WzJ6SMKF+GTD7JZZqxSR1AKKmXjaSu39sClUrNlwxS4Tl7a99O+ltFy6yhPMO+wgZuxpQjJ2PZkfrQKmAqrLhw==}
peerDependencies:
i18next: '>= 26.0.1'
react: '>= 16.8.0'
@ -3527,8 +3541,8 @@ packages:
resolution: {integrity: sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==}
engines: {node: '>=18'}
rettime@0.11.7:
resolution: {integrity: sha512-DoAm1WjR1eH7z8sHPtvvUMIZh4/CSKkGCz6CxPqOrEAnOGtOuHSnSE9OC+razqxKuf4ub7pAYyl/vZV0vGs5tg==}
rettime@0.11.11:
resolution: {integrity: sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==}
reusify@1.1.0:
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
@ -3599,8 +3613,8 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
shadcn@4.3.0:
resolution: {integrity: sha512-7vhnBh2LVLyxOd1ZQWwXv7OATCnQcxdqc8FbZdNigZriNOwDsHklQmPpvPt1jcrFK5mzMI+cyuAYv8WzERx2Og==}
shadcn@4.7.0:
resolution: {integrity: sha512-70fwnesNrY1GgeD7Kdzn+3SsYeyfibm8immsA5L68+OusoPTvYF01oWExl8/latKpMpvVXcbgdbbE6VFBJQ38w==}
hasBin: true
shebang-command@2.0.0:
@ -3735,11 +3749,11 @@ packages:
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
engines: {node: '>=12.0.0'}
tldts-core@7.0.28:
resolution: {integrity: sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ==}
tldts-core@7.0.30:
resolution: {integrity: sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==}
tldts@7.0.28:
resolution: {integrity: sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw==}
tldts@7.0.30:
resolution: {integrity: sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==}
hasBin: true
to-regex-range@5.0.1:
@ -3788,8 +3802,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
type-fest@5.5.0:
resolution: {integrity: sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==}
type-fest@5.6.0:
resolution: {integrity: sha512-8ZiHFm91orbSAe2PSAiSVBVko18pbhbiB3U9GglSzF/zCGkR+rxpHx6sEMCUm4kxY4LjDIUGgCfUMtwfZfjfUA==}
engines: {node: '>=20'}
type-is@2.0.1:
@ -4037,8 +4051,8 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
yocto-spinner@1.1.0:
resolution: {integrity: sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==}
yocto-spinner@1.2.0:
resolution: {integrity: sha512-Yw0hUB6UA3o4YUgKy3oSe9a4cxoaZ9sBfYDw+JSxo6Id0KoJGoxzPA24qqUXYKBWABs/zDSGTz9kww7t3F0XGw==}
engines: {node: '>=18.19'}
yoctocolors@2.1.2:
@ -4073,7 +4087,7 @@ snapshots:
js-tokens: 4.0.0
picocolors: 1.1.1
'@babel/compat-data@7.29.0': {}
'@babel/compat-data@7.29.3': {}
'@babel/core@7.29.0':
dependencies:
@ -4082,7 +4096,7 @@ snapshots:
'@babel/helper-compilation-targets': 7.28.6
'@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0)
'@babel/helpers': 7.29.2
'@babel/parser': 7.29.2
'@babel/parser': 7.29.3
'@babel/template': 7.28.6
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
@ -4097,7 +4111,7 @@ snapshots:
'@babel/generator@7.29.1':
dependencies:
'@babel/parser': 7.29.2
'@babel/parser': 7.29.3
'@babel/types': 7.29.0
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
@ -4109,13 +4123,13 @@ snapshots:
'@babel/helper-compilation-targets@7.28.6':
dependencies:
'@babel/compat-data': 7.29.0
'@babel/compat-data': 7.29.3
'@babel/helper-validator-option': 7.27.1
browserslist: 4.28.2
lru-cache: 5.1.1
semver: 6.3.1
'@babel/helper-create-class-features-plugin@7.28.6(@babel/core@7.29.0)':
'@babel/helper-create-class-features-plugin@7.29.3(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
@ -4190,6 +4204,10 @@ snapshots:
dependencies:
'@babel/types': 7.29.0
'@babel/parser@7.29.3':
dependencies:
'@babel/types': 7.29.0
'@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)':
dependencies:
'@babel/core': 7.29.0
@ -4212,7 +4230,7 @@ snapshots:
dependencies:
'@babel/core': 7.29.0
'@babel/helper-annotate-as-pure': 7.27.3
'@babel/helper-create-class-features-plugin': 7.28.6(@babel/core@7.29.0)
'@babel/helper-create-class-features-plugin': 7.29.3(@babel/core@7.29.0)
'@babel/helper-plugin-utils': 7.28.6
'@babel/helper-skip-transparent-expression-wrappers': 7.27.1
'@babel/plugin-syntax-typescript': 7.28.6(@babel/core@7.29.0)
@ -4235,7 +4253,7 @@ snapshots:
'@babel/template@7.28.6':
dependencies:
'@babel/code-frame': 7.29.0
'@babel/parser': 7.29.2
'@babel/parser': 7.29.3
'@babel/types': 7.29.0
'@babel/traverse@7.29.0':
@ -4243,7 +4261,7 @@ snapshots:
'@babel/code-frame': 7.29.0
'@babel/generator': 7.29.1
'@babel/helper-globals': 7.28.0
'@babel/parser': 7.29.2
'@babel/parser': 7.29.3
'@babel/template': 7.28.6
'@babel/types': 7.29.0
debug: 4.4.3
@ -4255,7 +4273,7 @@ snapshots:
'@babel/helper-string-parser': 7.27.1
'@babel/helper-validator-identifier': 7.28.5
'@dotenvx/dotenvx@1.61.0':
'@dotenvx/dotenvx@1.65.0':
dependencies:
commander: 11.1.0
dotenv: 17.4.2
@ -4266,7 +4284,7 @@ snapshots:
object-treeify: 1.1.33
picomatch: 4.0.4
which: 4.0.0
yocto-spinner: 1.1.0
yocto-spinner: 1.2.0
'@ecies/ciphers@0.2.6(@noble/ciphers@1.3.0)':
dependencies:
@ -4419,9 +4437,9 @@ snapshots:
'@fontsource-variable/inter@5.2.8': {}
'@hono/node-server@1.19.14(hono@4.12.14)':
'@hono/node-server@1.19.14(hono@4.12.18)':
dependencies:
hono: 4.12.14
hono: 4.12.18
'@humanfs/core@0.19.1': {}
@ -4436,14 +4454,14 @@ snapshots:
'@inquirer/ansi@2.0.5': {}
'@inquirer/confirm@6.0.11(@types/node@25.6.0)':
'@inquirer/confirm@6.0.12(@types/node@25.6.0)':
dependencies:
'@inquirer/core': 11.1.8(@types/node@25.6.0)
'@inquirer/core': 11.1.9(@types/node@25.6.0)
'@inquirer/type': 4.0.5(@types/node@25.6.0)
optionalDependencies:
'@types/node': 25.6.0
'@inquirer/core@11.1.8(@types/node@25.6.0)':
'@inquirer/core@11.1.9(@types/node@25.6.0)':
dependencies:
'@inquirer/ansi': 2.0.5
'@inquirer/figures': 2.0.5
@ -4482,18 +4500,18 @@ snapshots:
'@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)':
dependencies:
'@hono/node-server': 1.19.14(hono@4.12.14)
ajv: 8.18.0
ajv-formats: 3.0.1(ajv@8.18.0)
'@hono/node-server': 1.19.14(hono@4.12.18)
ajv: 8.20.0
ajv-formats: 3.0.1(ajv@8.20.0)
content-type: 1.0.5
cors: 2.8.6
cross-spawn: 7.0.6
eventsource: 3.0.7
eventsource-parser: 3.0.6
eventsource-parser: 3.0.8
express: 5.2.1
express-rate-limit: 8.3.2(express@5.2.1)
hono: 4.12.14
jose: 6.2.2
express-rate-limit: 8.5.1(express@5.2.1)
hono: 4.12.18
jose: 6.2.3
json-schema-typed: 8.0.2
pkce-challenge: 5.0.1
raw-body: 3.0.2
@ -4502,7 +4520,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@mswjs/interceptors@0.41.3':
'@mswjs/interceptors@0.41.8':
dependencies:
'@open-draft/deferred-promise': 2.2.0
'@open-draft/logger': 0.3.0
@ -5355,12 +5373,12 @@ snapshots:
'@sindresorhus/merge-streams@4.0.0': {}
'@tabler/icons-react@3.41.1(react@19.2.5)':
'@tabler/icons-react@3.43.0(react@19.2.5)':
dependencies:
'@tabler/icons': 3.41.1
'@tabler/icons': 3.43.0
react: 19.2.5
'@tabler/icons@3.41.1': {}
'@tabler/icons@3.43.0': {}
'@tailwindcss/node@4.2.4':
dependencies:
@ -5531,7 +5549,7 @@ snapshots:
dependencies:
'@babel/core': 7.29.0
'@babel/generator': 7.29.1
'@babel/parser': 7.29.2
'@babel/parser': 7.29.3
'@babel/types': 7.29.0
ansis: 4.2.0
babel-dead-code-elimination: 1.0.12
@ -5808,9 +5826,9 @@ snapshots:
agent-base@7.1.4: {}
ajv-formats@3.0.1(ajv@8.18.0):
ajv-formats@3.0.1(ajv@8.20.0):
optionalDependencies:
ajv: 8.18.0
ajv: 8.20.0
ajv@6.14.0:
dependencies:
@ -5819,10 +5837,10 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
ajv@8.18.0:
ajv@8.20.0:
dependencies:
fast-deep-equal: 3.1.3
fast-uri: 3.1.0
fast-uri: 3.1.2
json-schema-traverse: 1.0.0
require-from-string: 2.0.2
@ -5856,7 +5874,7 @@ snapshots:
babel-dead-code-elimination@1.0.12:
dependencies:
'@babel/core': 7.29.0
'@babel/parser': 7.29.2
'@babel/parser': 7.29.3
'@babel/traverse': 7.29.0
'@babel/types': 7.29.0
transitivePeerDependencies:
@ -5868,7 +5886,7 @@ snapshots:
balanced-match@4.0.4: {}
baseline-browser-mapping@2.10.17: {}
baseline-browser-mapping@2.10.27: {}
binary-extensions@2.3.0: {}
@ -5900,10 +5918,10 @@ snapshots:
browserslist@4.28.2:
dependencies:
baseline-browser-mapping: 2.10.17
caniuse-lite: 1.0.30001787
electron-to-chromium: 1.5.334
node-releases: 2.0.37
baseline-browser-mapping: 2.10.27
caniuse-lite: 1.0.30001792
electron-to-chromium: 1.5.352
node-releases: 2.0.38
update-browserslist-db: 1.2.3(browserslist@4.28.2)
bundle-name@4.1.0:
@ -5924,7 +5942,7 @@ snapshots:
callsites@3.1.0: {}
caniuse-lite@1.0.30001787: {}
caniuse-lite@1.0.30001792: {}
ccount@2.0.1: {}
@ -6094,7 +6112,7 @@ snapshots:
ee-first@1.1.1: {}
electron-to-chromium@1.5.334: {}
electron-to-chromium@1.5.352: {}
emoji-regex@10.6.0: {}
@ -6251,11 +6269,11 @@ snapshots:
etag@1.8.1: {}
eventsource-parser@3.0.6: {}
eventsource-parser@3.0.8: {}
eventsource@3.0.7:
dependencies:
eventsource-parser: 3.0.6
eventsource-parser: 3.0.8
execa@5.1.1:
dependencies:
@ -6284,10 +6302,10 @@ snapshots:
strip-final-newline: 4.0.0
yoctocolors: 2.1.2
express-rate-limit@8.3.2(express@5.2.1):
express-rate-limit@8.5.1(express@5.2.1):
dependencies:
express: 5.2.1
ip-address: 10.1.0
ip-address: 10.2.0
express@5.2.1:
dependencies:
@ -6344,7 +6362,7 @@ snapshots:
dependencies:
fast-string-truncated-width: 3.0.3
fast-uri@3.1.0: {}
fast-uri@3.1.2: {}
fast-wrap-ansi@0.2.0:
dependencies:
@ -6406,10 +6424,10 @@ snapshots:
fresh@2.0.0: {}
fs-extra@11.3.4:
fs-extra@11.3.5:
dependencies:
graceful-fs: 4.2.11
jsonfile: 6.2.0
jsonfile: 6.2.1
universalify: 2.0.1
fsevents@2.3.3:
@ -6435,7 +6453,7 @@ snapshots:
get-proto: 1.0.1
gopd: 1.2.0
has-symbols: 1.1.0
hasown: 2.0.2
hasown: 2.0.3
math-intrinsics: 1.1.0
get-nonce@1.0.1: {}
@ -6466,7 +6484,7 @@ snapshots:
dependencies:
is-glob: 4.0.3
globals@17.5.0: {}
globals@17.6.0: {}
goober@2.1.18(csstype@3.2.3):
dependencies:
@ -6476,11 +6494,11 @@ snapshots:
graceful-fs@4.2.11: {}
graphql@16.13.2: {}
graphql@16.14.0: {}
has-symbols@1.1.0: {}
hasown@2.0.2:
hasown@2.0.3:
dependencies:
function-bind: 1.1.2
@ -6587,7 +6605,7 @@ snapshots:
highlight.js@11.11.1: {}
hono@4.12.14: {}
hono@4.12.18: {}
html-parse-stringify@3.0.1:
dependencies:
@ -6620,7 +6638,7 @@ snapshots:
dependencies:
'@babel/runtime': 7.29.2
i18next@26.0.8(typescript@5.9.3):
i18next@26.0.10(typescript@5.9.3):
optionalDependencies:
typescript: 5.9.3
@ -6643,7 +6661,7 @@ snapshots:
inline-style-parser@0.2.7: {}
ip-address@10.1.0: {}
ip-address@10.2.0: {}
ipaddr.js@1.9.1: {}
@ -6716,7 +6734,7 @@ snapshots:
jiti@2.7.0: {}
jose@6.2.2: {}
jose@6.2.3: {}
jotai@2.19.1(@babel/core@7.29.0)(@babel/template@7.28.6)(@types/react@19.2.14)(react@19.2.5):
optionalDependencies:
@ -6747,7 +6765,7 @@ snapshots:
json5@2.2.3: {}
jsonfile@6.2.0:
jsonfile@6.2.1:
dependencies:
universalify: 2.0.1
optionalDependencies:
@ -7227,24 +7245,24 @@ snapshots:
ms@2.1.3: {}
msw@2.13.4(@types/node@25.6.0)(typescript@5.9.3):
msw@2.14.4(@types/node@25.6.0)(typescript@5.9.3):
dependencies:
'@inquirer/confirm': 6.0.11(@types/node@25.6.0)
'@mswjs/interceptors': 0.41.3
'@inquirer/confirm': 6.0.12(@types/node@25.6.0)
'@mswjs/interceptors': 0.41.8
'@open-draft/deferred-promise': 3.0.0
'@types/statuses': 2.0.6
cookie: 1.1.1
graphql: 16.13.2
graphql: 16.14.0
headers-polyfill: 5.0.1
is-node-process: 1.2.0
outvariant: 1.4.3
path-to-regexp: 6.3.0
picocolors: 1.1.1
rettime: 0.11.7
rettime: 0.11.11
statuses: 2.0.2
strict-event-emitter: 0.5.1
tough-cookie: 6.0.1
type-fest: 5.5.0
type-fest: 5.6.0
until-async: 3.0.2
yargs: 17.7.2
optionalDependencies:
@ -7256,6 +7274,8 @@ snapshots:
nanoid@3.3.11: {}
nanoid@3.3.12: {}
natural-compare@1.4.0: {}
negotiator@1.0.0: {}
@ -7268,7 +7288,7 @@ snapshots:
fetch-blob: 3.2.0
formdata-polyfill: 4.0.10
node-releases@2.0.37: {}
node-releases@2.0.38: {}
normalize-path@3.0.0: {}
@ -7416,6 +7436,12 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
postcss@8.5.14:
dependencies:
nanoid: 3.3.12
picocolors: 1.1.1
source-map-js: 1.2.1
powershell-utils@0.1.0: {}
prelude-ls@1.2.1: {}
@ -7529,11 +7555,11 @@ snapshots:
react: 19.2.5
scheduler: 0.27.0
react-i18next@17.0.4(i18next@26.0.8(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3):
react-i18next@17.0.6(i18next@26.0.10(typescript@5.9.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(typescript@5.9.3):
dependencies:
'@babel/runtime': 7.29.2
html-parse-stringify: 3.0.1
i18next: 26.0.8(typescript@5.9.3)
i18next: 26.0.10(typescript@5.9.3)
react: 19.2.5
use-sync-external-store: 1.6.0(react@19.2.5)
optionalDependencies:
@ -7674,7 +7700,7 @@ snapshots:
onetime: 7.0.0
signal-exit: 4.1.0
rettime@0.11.7: {}
rettime@0.11.11: {}
reusify@1.1.0: {}
@ -7764,13 +7790,13 @@ snapshots:
setprototypeof@1.2.0: {}
shadcn@4.3.0(@types/node@25.6.0)(typescript@5.9.3):
shadcn@4.7.0(@types/node@25.6.0)(typescript@5.9.3):
dependencies:
'@babel/core': 7.29.0
'@babel/parser': 7.29.2
'@babel/parser': 7.29.3
'@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0)
'@babel/preset-typescript': 7.28.5(@babel/core@7.29.0)
'@dotenvx/dotenvx': 1.61.0
'@dotenvx/dotenvx': 1.65.0
'@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76)
'@types/validate-npm-package-name': 4.0.2
browserslist: 4.28.2
@ -7781,15 +7807,15 @@ snapshots:
diff: 8.0.4
execa: 9.6.1
fast-glob: 3.3.3
fs-extra: 11.3.4
fs-extra: 11.3.5
fuzzysort: 3.1.0
https-proxy-agent: 7.0.6
kleur: 4.1.5
msw: 2.13.4(@types/node@25.6.0)(typescript@5.9.3)
msw: 2.14.4(@types/node@25.6.0)(typescript@5.9.3)
node-fetch: 3.3.2
open: 11.0.0
ora: 8.2.0
postcss: 8.5.10
postcss: 8.5.14
postcss-selector-parser: 7.1.1
prompts: 2.4.2
recast: 0.23.11
@ -7931,11 +7957,11 @@ snapshots:
fdir: 6.5.0(picomatch@4.0.4)
picomatch: 4.0.4
tldts-core@7.0.28: {}
tldts-core@7.0.30: {}
tldts@7.0.28:
tldts@7.0.30:
dependencies:
tldts-core: 7.0.28
tldts-core: 7.0.30
to-regex-range@5.0.1:
dependencies:
@ -7945,7 +7971,7 @@ snapshots:
tough-cookie@6.0.1:
dependencies:
tldts: 7.0.28
tldts: 7.0.30
trim-lines@3.0.1: {}
@ -7981,7 +8007,7 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
type-fest@5.5.0:
type-fest@5.6.0:
dependencies:
tagged-tag: 1.0.0
@ -8197,7 +8223,7 @@ snapshots:
yocto-queue@0.1.0: {}
yocto-spinner@1.1.0:
yocto-spinner@1.2.0:
dependencies:
yoctocolors: 2.1.2