@agentprojectcontext/apx 1.81.0 → 1.89.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +9 -8
- package/skills/apc-context/SKILL.md +14 -7
- package/skills/apx/SKILL.md +62 -2
- package/skills/apx-mcp/SKILL.md +42 -15
- package/src/core/agent/a2a/peers.js +161 -0
- package/src/core/agent/a2a/reply.js +379 -27
- package/src/core/agent/abort.js +25 -0
- package/src/core/agent/agent-model.js +58 -0
- package/src/core/agent/agent-tools.js +101 -0
- package/src/core/agent/author-line.js +110 -0
- package/src/core/agent/build-agent-system.js +52 -18
- package/src/core/agent/channels/voice-context.js +17 -2
- package/src/core/agent/closing-floor.js +117 -0
- package/src/core/agent/constants.js +119 -11
- package/src/core/agent/engine-call.js +56 -0
- package/src/core/agent/group/run-group-turn.js +218 -0
- package/src/core/agent/group/turn-resolver.js +165 -0
- package/src/core/agent/judge.js +138 -11
- package/src/core/agent/loop/side-effects.js +106 -3
- package/src/core/agent/memory.js +31 -18
- package/src/core/agent/model-capabilities.js +47 -0
- package/src/core/agent/model-router.js +11 -40
- package/src/core/agent/prompt-builder.js +47 -0
- package/src/core/agent/prompts/channels/a2a.md +12 -0
- package/src/core/agent/prompts/channels/code.md +3 -1
- package/src/core/agent/prompts/channels/telegram.md +2 -0
- package/src/core/agent/prompts/channels/web_code.md +1 -1
- package/src/core/agent/prompts/channels/whatsapp.md +37 -0
- package/src/core/agent/prompts/core/agent-base.md +2 -0
- package/src/core/agent/prompts/discipline/action.md +1 -0
- package/src/core/agent/prompts/discipline/two-segment.md +2 -1
- package/src/core/agent/prompts/modes/mobility.md +36 -0
- package/src/core/agent/retry.js +42 -0
- package/src/core/agent/run-agent.js +322 -25
- package/src/core/agent/run-turn.js +253 -0
- package/src/core/agent/self-memory.js +18 -52
- package/src/core/agent/skills/agent-skills.js +118 -0
- package/src/core/agent/skills/catalog.js +1 -1
- package/src/core/agent/skills/declared.js +13 -0
- package/src/core/agent/skills/index-store.js +8 -1
- package/src/core/agent/skills/inspector.js +20 -3
- package/src/core/agent/skills/loader.js +18 -3
- package/src/core/agent/skills/media.js +121 -0
- package/src/core/agent/skills/origin.js +61 -0
- package/src/core/agent/skills/sections.js +136 -0
- package/src/core/agent/super-agent.js +54 -14
- package/src/core/agent/tools/handlers/_calendar.js +50 -0
- package/src/core/agent/tools/handlers/add-mcp.js +103 -0
- package/src/core/agent/tools/handlers/asana-create-task.js +3 -1
- package/src/core/agent/tools/handlers/attach-media.js +56 -0
- package/src/core/agent/tools/handlers/calendar-create-event.js +38 -0
- package/src/core/agent/tools/handlers/calendar-find-slot.js +39 -0
- package/src/core/agent/tools/handlers/calendar-list-events.js +39 -0
- package/src/core/agent/tools/handlers/calendar-update-event.js +35 -0
- package/src/core/agent/tools/handlers/call-agent.js +14 -6
- package/src/core/agent/tools/handlers/call-runtime.js +27 -49
- package/src/core/agent/tools/handlers/comment-task.js +78 -0
- package/src/core/agent/tools/handlers/complete-task.js +60 -0
- package/src/core/agent/tools/handlers/configure-agent.js +70 -0
- package/src/core/agent/tools/handlers/create-agent.js +68 -0
- package/src/core/agent/tools/handlers/create-task.js +41 -3
- package/src/core/agent/tools/handlers/import-agent.js +5 -5
- package/src/core/agent/tools/handlers/list-mcp-tools.js +83 -0
- package/src/core/agent/tools/handlers/list-routines.js +46 -0
- package/src/core/agent/tools/handlers/list-tasks.js +8 -1
- package/src/core/agent/tools/handlers/mark-commitment.js +57 -0
- package/src/core/agent/tools/handlers/read-skill.js +64 -0
- package/src/core/agent/tools/handlers/remember-routine.js +2 -8
- package/src/core/agent/tools/handlers/remember.js +82 -6
- package/src/core/agent/tools/handlers/remove-agent.js +42 -0
- package/src/core/agent/tools/handlers/run-routine.js +94 -0
- package/src/core/agent/tools/handlers/run-shell.js +44 -2
- package/src/core/agent/tools/handlers/send-telegram.js +41 -19
- package/src/core/agent/tools/handlers/set-agent-prompt.js +44 -0
- package/src/core/agent/tools/handlers/view-media.js +69 -0
- package/src/core/agent/tools/handlers/write-agent-memory.js +52 -0
- package/src/core/agent/tools/helpers.js +36 -4
- package/src/core/agent/tools/names.js +64 -0
- package/src/core/agent/tools/registry-bridge.js +24 -1
- package/src/core/agent/tools/registry.js +79 -0
- package/src/core/agent/tools/tool-call-parser.js +136 -13
- package/src/core/agent/vision-bridge.js +69 -0
- package/src/core/apc/agent-identity.js +74 -0
- package/src/core/apc/agent-rename-refs.js +187 -0
- package/src/core/apc/agent-write.js +312 -0
- package/src/core/apc/agents-vault.js +1 -1
- package/src/core/apc/blob-keys.js +42 -0
- package/src/core/apc/frontmatter.js +166 -10
- package/src/core/apc/parser.js +33 -83
- package/src/core/apc/paths.js +4 -6
- package/src/core/apc/project-memory.js +44 -0
- package/src/core/apc/projects-helpers.js +33 -0
- package/src/core/apc/scaffold.js +36 -16
- package/src/core/channels/telegram/ask-callbacks.js +103 -0
- package/src/core/channels/telegram/dispatch.js +154 -30
- package/src/core/channels/telegram/helpers.js +59 -7
- package/src/core/channels/telegram/inbound/audio.js +26 -30
- package/src/core/channels/telegram/inbound/file.js +22 -32
- package/src/core/channels/telegram/inbound/photo.js +24 -28
- package/src/core/channels/telegram/progress-gate.js +76 -0
- package/src/core/channels/telegram/reply.js +151 -43
- package/src/core/channels/telegram/voice-note.js +150 -0
- package/src/core/config/index.js +53 -54
- package/src/core/config/paths.js +117 -31
- package/src/core/config/redact.js +89 -4
- package/src/core/config/secret-values.js +2 -4
- package/src/core/constants/actors.js +5 -0
- package/src/core/constants/channels.js +3 -1
- package/src/core/constants/task-categories.js +73 -0
- package/src/core/constants/task-fields.js +28 -0
- package/src/core/engines/_globs.js +33 -0
- package/src/core/engines/anthropic.js +19 -1
- package/src/core/engines/catalog.js +20 -0
- package/src/core/engines/gemini.js +84 -16
- package/src/core/engines/index.js +19 -3
- package/src/core/engines/mock.js +145 -1
- package/src/core/engines/openai-compatible.js +150 -15
- package/src/core/engines/presets.js +36 -9
- package/src/core/engines/zen.js +131 -0
- package/src/core/events/bus.js +140 -0
- package/src/core/events/mascot-notify.js +156 -0
- package/src/core/http-tools/browser.js +366 -36
- package/src/core/http-tools/catalog.js +206 -22
- package/src/core/http-tools/grep.js +62 -7
- package/src/core/http-tools/registry.js +3 -1
- package/src/core/i18n/en.js +34 -3
- package/src/core/i18n/es.js +36 -4
- package/src/core/i18n/index.js +5 -5
- package/src/core/i18n/pt.js +32 -3
- package/src/core/identity/self.js +14 -1
- package/src/core/images/engines/a1111.js +219 -0
- package/src/core/images/engines/index.js +187 -0
- package/src/core/images/engines/mock.js +74 -0
- package/src/core/images/engines/openai.js +130 -0
- package/src/core/images/engines/sdcpp.js +204 -0
- package/src/core/images/engines/shared.js +218 -0
- package/src/core/images/generate.js +260 -0
- package/src/core/integrations/catalog.js +8 -3
- package/src/core/integrations/plugins/_google-oauth.js +179 -0
- package/src/core/integrations/plugins/asana.js +7 -3
- package/src/core/integrations/plugins/calendar.js +404 -0
- package/src/core/integrations/store.js +2 -0
- package/src/core/memory/broker.js +16 -1
- package/src/core/memory/compactor.js +19 -3
- package/src/core/memory/consolidate.js +2 -2
- package/src/core/memory/dated-log.js +66 -0
- package/src/core/memory/embed-engines/custom.js +59 -0
- package/src/core/memory/embed-engines/gemini.js +9 -3
- package/src/core/memory/embed-engines/index.js +87 -15
- package/src/core/memory/embeddings.js +52 -24
- package/src/core/memory/index.js +13 -3
- package/src/core/memory/indexer.js +83 -27
- package/src/core/memory/prune.js +182 -0
- package/src/core/memory/store.js +22 -0
- package/src/core/memory/summarizer.js +72 -8
- package/src/core/mobility/answer.js +80 -0
- package/src/core/mobility/geofence.js +667 -0
- package/src/core/mobility/osm-route.js +282 -0
- package/src/core/mobility/preferences.js +91 -0
- package/src/core/mobility/state.js +245 -0
- package/src/core/mobility/trip-event.js +384 -0
- package/src/core/net/tailscale.js +168 -0
- package/src/core/profiles/bundled/secretary/PROFILE.md +32 -42
- package/src/core/profiles/bundled/secretary/config.schema.es.json +68 -0
- package/src/core/profiles/bundled/secretary/config.schema.json +30 -2
- package/src/core/profiles/bundled/secretary/profile.json +3 -1
- package/src/core/profiles/bundled/secretary/routines/a2a-sweep.json +20 -0
- package/src/core/profiles/bundled/secretary/routines/day-close.json +4 -1
- package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -1
- package/src/core/profiles/bundled/secretary/routines/watch.json +13 -2
- package/src/core/profiles/lifecycle.js +243 -19
- package/src/core/profiles/paths.js +12 -0
- package/src/core/profiles/store.js +47 -0
- package/src/core/routines/active-runs.js +213 -0
- package/src/core/routines/delivery.js +612 -0
- package/src/core/routines/header.js +88 -0
- package/src/core/routines/index.js +26 -0
- package/src/core/routines/run-log.js +78 -0
- package/src/core/routines/runner.js +629 -66
- package/src/core/routines/signals.js +75 -0
- package/src/core/runtime-skills/apc-context/SKILL.md +14 -7
- package/src/core/runtime-skills/apx/SKILL.md +93 -1
- package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
- package/src/core/runtime-skills/apx-agent/SKILL.md +134 -19
- package/src/core/runtime-skills/apx-commitment/SKILL.md +21 -11
- package/src/core/runtime-skills/apx-image/SKILL.md +60 -0
- package/src/core/runtime-skills/apx-integrations/SKILL.md +82 -0
- package/src/core/runtime-skills/apx-mcp/SKILL.md +45 -14
- package/src/core/runtime-skills/apx-mcp-builder/SKILL.md +4 -4
- package/src/core/runtime-skills/apx-profile/SKILL.md +27 -2
- package/src/core/runtime-skills/apx-project/SKILL.md +23 -2
- package/src/core/runtime-skills/apx-routine/SKILL.md +40 -17
- package/src/core/runtime-skills/apx-runtime/SKILL.md +124 -14
- package/src/core/runtime-skills/apx-sessions/SKILL.md +7 -6
- package/src/core/runtime-skills/apx-skill-builder/SKILL.md +15 -13
- package/src/core/runtime-skills/apx-task/SKILL.md +79 -26
- package/src/core/runtime-skills/apx-telegram/SKILL.md +12 -12
- package/src/core/runtime-skills/apx-transcribe/SKILL.md +56 -0
- package/src/core/runtime-skills/apx-voice/SKILL.md +27 -8
- package/src/core/runtime-skills/claude-code/SKILL.md +8 -0
- package/src/core/runtime-skills/openrouter/SKILL.md +8 -2
- package/src/core/runtimes/_spawn.js +9 -1
- package/src/core/runtimes/claude-code.js +11 -2
- package/src/core/runtimes/codex.js +44 -10
- package/src/core/runtimes/index.js +17 -2
- package/src/core/runtimes/opencode.js +88 -9
- package/src/core/runtimes/outcome.js +119 -0
- package/src/core/sessions/index.js +193 -1
- package/src/core/stores/agent-inbox.js +109 -23
- package/src/core/stores/code-sessions.js +21 -0
- package/src/core/stores/commitments.js +27 -3
- package/src/core/stores/conversations.js +220 -20
- package/src/core/stores/deliveries.js +177 -0
- package/src/core/stores/media-archive.js +228 -0
- package/src/core/stores/messages.js +1096 -112
- package/src/core/stores/organization.js +34 -0
- package/src/core/stores/project-memory.js +71 -0
- package/src/core/stores/routine-memory.js +16 -8
- package/src/core/stores/routines.js +27 -49
- package/src/core/stores/sessions.js +4 -8
- package/src/core/stores/tasks.js +212 -18
- package/src/core/stores/turn-record.js +209 -0
- package/src/core/tasks/columns.js +136 -0
- package/src/core/tasks/comment-turn.js +258 -0
- package/src/core/util/time.js +13 -0
- package/src/core/voice/engines/index.js +54 -1
- package/src/core/voice/engines/openai.js +50 -4
- package/src/core/voice/opus.js +67 -0
- package/src/core/voice/pronounceable.js +43 -0
- package/src/core/voice/transcription.js +4 -0
- package/src/core/voice/tts.js +114 -24
- package/src/host/daemon/active-turns.js +188 -0
- package/src/host/daemon/api/admin-config.js +9 -0
- package/src/host/daemon/api/agents.js +99 -90
- package/src/host/daemon/api/artifact-preview.js +7 -6
- package/src/host/daemon/api/artifacts.js +3 -2
- package/src/host/daemon/api/code.js +55 -9
- package/src/host/daemon/api/commitments.js +11 -0
- package/src/host/daemon/api/confirm.js +3 -2
- package/src/host/daemon/api/conversations.js +543 -35
- package/src/host/daemon/api/deck.js +9 -8
- package/src/host/daemon/api/deliveries.js +33 -0
- package/src/host/daemon/api/desktop.js +5 -4
- package/src/host/daemon/api/embeddings.js +15 -6
- package/src/host/daemon/api/engines.js +59 -8
- package/src/host/daemon/api/exec.js +437 -121
- package/src/host/daemon/api/groups.js +215 -0
- package/src/host/daemon/api/images.js +107 -0
- package/src/host/daemon/api/inbox.js +123 -2
- package/src/host/daemon/api/integrations.js +64 -2
- package/src/host/daemon/api/mcps.js +7 -6
- package/src/host/daemon/api/media.js +283 -0
- package/src/host/daemon/api/mobility.js +177 -0
- package/src/host/daemon/api/net.js +182 -0
- package/src/host/daemon/api/pairing.js +38 -5
- package/src/host/daemon/api/profiles.js +30 -2
- package/src/host/daemon/api/routines.js +36 -4
- package/src/host/daemon/api/runtimes.js +74 -39
- package/src/host/daemon/api/sessions-search.js +7 -2
- package/src/host/daemon/api/sessions.js +44 -2
- package/src/host/daemon/api/shared.js +127 -20
- package/src/host/daemon/api/skills.js +11 -4
- package/src/host/daemon/api/super-agent.js +384 -53
- package/src/host/daemon/api/tasks.js +180 -10
- package/src/host/daemon/api/telegram.js +11 -10
- package/src/host/daemon/api/thread-faces.js +159 -0
- package/src/host/daemon/api/top-level.js +3 -3
- package/src/host/daemon/api/transcribe.js +11 -10
- package/src/host/daemon/api/tts.js +97 -4
- package/src/host/daemon/api/turn-abort.js +23 -0
- package/src/host/daemon/api/turns.js +49 -0
- package/src/host/daemon/api/voice.js +5 -4
- package/src/host/daemon/api/web.js +98 -0
- package/src/host/daemon/api.js +39 -0
- package/src/host/daemon/delivery-sweep.js +58 -0
- package/src/host/daemon/desktop-ws.js +5 -23
- package/src/host/daemon/events-ws.js +273 -0
- package/src/host/daemon/index.js +59 -15
- package/src/host/daemon/plugins/desktop/index.js +17 -4
- package/src/host/daemon/plugins/telegram/index.js +78 -40
- package/src/host/daemon/pty-bridge.py +99 -0
- package/src/host/daemon/routines-scheduler.js +1 -0
- package/src/host/daemon/smoke.js +1 -2
- package/src/host/daemon/terminal-ws.js +132 -0
- package/src/host/daemon/tts-keepwarm.js +87 -0
- package/src/host/daemon/wakeup.js +11 -6
- package/src/host/daemon/whisper-server.js +44 -0
- package/src/host/daemon/whisper-server.py +70 -13
- package/src/host/daemon/ws-auth.js +28 -0
- package/src/interfaces/android/app/build.gradle +42 -0
- package/src/interfaces/android/app/proguard-rules.pro +4 -0
- package/src/interfaces/android/app/src/main/AndroidManifest.xml +139 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/AlertScreen.java +139 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/AndroidAutoDetector.java +106 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ApxCarAppService.java +57 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ApxPreferences.java +165 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarAlertStore.java +84 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarMessageActionReceiver.java +47 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/CarMessageNotification.java +152 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DaemonAddress.java +35 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DaemonClient.java +339 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/DeviceLocation.java +38 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ErrandScreen.java +127 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MainActivity.java +872 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsNavigationDetector.java +63 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsNavigationListenerService.java +453 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MapsShareIntentParser.java +93 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotBlobCatalog.java +66 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotOverlayService.java +448 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MascotView.java +249 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/MessageFrameParser.java +240 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/NotifyChannels.java +69 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ProximityActionReceiver.java +78 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/ProximityNotification.java +161 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/StartupReceiver.java +57 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TravelEventGate.java +62 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TravelStatusBanner.java +116 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripLocationService.java +272 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripPlacesSheet.java +203 -0
- package/src/interfaces/android/app/src/main/java/dev/agentprojectcontext/apx/TripScreen.java +182 -0
- package/src/interfaces/android/app/src/main/res/drawable/app_background.xml +12 -0
- package/src/interfaces/android/app/src/main/res/drawable/ic_apx_app.xml +10 -0
- package/src/interfaces/android/app/src/main/res/drawable/ic_apx_launcher_foreground.xml +12 -0
- package/src/interfaces/android/app/src/main/res/drawable/ic_apx_notification.xml +22 -0
- package/src/interfaces/android/app/src/main/res/drawable/ic_trip_car.xml +37 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/apx_logo.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_amatista.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_aqua.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_brasa.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_campana.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_caracol.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_carbon.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cobalto.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cometa.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_coral.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_cubi.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_eclipse.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_faro.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_gajo.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_iris.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_kiwi.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_laguna.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_lila.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_menta.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_nimbo.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_noche.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_onyx.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_orbita.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_ovni.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_papa.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_parche.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_peon.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_perla.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_pino.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_quad.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_rubi.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_saturno.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_trebol.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_trino.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_volcan.png +0 -0
- package/src/interfaces/android/app/src/main/res/drawable-nodpi/mascot_zafiro.png +0 -0
- package/src/interfaces/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +5 -0
- package/src/interfaces/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +5 -0
- package/src/interfaces/android/app/src/main/res/raw/apx_notification.mp3 +0 -0
- package/src/interfaces/android/app/src/main/res/values/colors.xml +4 -0
- package/src/interfaces/android/app/src/main/res/values/strings.xml +25 -0
- package/src/interfaces/android/app/src/main/res/values/styles.xml +10 -0
- package/src/interfaces/android/app/src/main/res/values-v31/styles.xml +14 -0
- package/src/interfaces/android/app/src/main/res/xml/automotive_app_desc.xml +9 -0
- package/src/interfaces/android/app/src/main/res/xml/network_security_config.xml +10 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/AndroidAutoDetectorTest.java +85 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/CarAlertStoreTest.java +104 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/DaemonAddressTest.java +23 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MapsNavigationDetectorTest.java +69 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MapsShareIntentParserTest.java +31 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/MessageFrameParserTest.java +180 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/TravelEventGateTest.java +64 -0
- package/src/interfaces/android/app/src/test/java/dev/agentprojectcontext/apx/TripPlacesSheetTest.java +66 -0
- package/src/interfaces/android/build.gradle +3 -0
- package/src/interfaces/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/src/interfaces/android/gradle/wrapper/gradle-wrapper.properties +9 -0
- package/src/interfaces/android/gradle.properties +3 -0
- package/src/interfaces/android/gradlew +248 -0
- package/src/interfaces/android/gradlew.bat +82 -0
- package/src/interfaces/android/settings.gradle +18 -0
- package/src/interfaces/cli/commands/a2a.js +145 -4
- package/src/interfaces/cli/commands/agent.js +247 -22
- package/src/interfaces/cli/commands/code.js +1 -1
- package/src/interfaces/cli/commands/commitment.js +5 -0
- package/src/interfaces/cli/commands/config.js +85 -6
- package/src/interfaces/cli/commands/exec.js +90 -1
- package/src/interfaces/cli/commands/image.js +314 -0
- package/src/interfaces/cli/commands/mcp.js +87 -23
- package/src/interfaces/cli/commands/memory.js +35 -22
- package/src/interfaces/cli/commands/panel.js +94 -0
- package/src/interfaces/cli/commands/profile.js +14 -0
- package/src/interfaces/cli/commands/project.js +29 -0
- package/src/interfaces/cli/commands/routine.js +45 -10
- package/src/interfaces/cli/commands/runtime.js +30 -7
- package/src/interfaces/cli/commands/session.js +90 -38
- package/src/interfaces/cli/commands/sessions.js +35 -2
- package/src/interfaces/cli/commands/skills.js +15 -9
- package/src/interfaces/cli/commands/status.js +2 -3
- package/src/interfaces/cli/commands/task.js +54 -5
- package/src/interfaces/cli/commands/telegram.js +12 -4
- package/src/interfaces/cli/commands/transcribe.js +177 -0
- package/src/interfaces/cli/help/index.js +311 -72
- package/src/interfaces/cli/http.js +61 -13
- package/src/interfaces/cli/index.js +1 -1
- package/src/interfaces/cli/routes/agent.js +5 -4
- package/src/interfaces/cli/routes/commitment.js +3 -2
- package/src/interfaces/cli/routes/config.js +7 -2
- package/src/interfaces/cli/routes/image.js +11 -0
- package/src/interfaces/cli/routes/index.js +2 -0
- package/src/interfaces/cli/routes/log.js +1 -1
- package/src/interfaces/cli/routes/memory.js +3 -2
- package/src/interfaces/cli/routes/panel.js +15 -2
- package/src/interfaces/cli/routes/profile.js +3 -2
- package/src/interfaces/cli/routes/transcribe.js +10 -0
- package/src/interfaces/cli/stdin.js +29 -0
- package/src/interfaces/desktop/assets/blobs/amatista.png +0 -0
- package/src/interfaces/desktop/assets/blobs/aqua.png +0 -0
- package/src/interfaces/desktop/assets/blobs/brasa.png +0 -0
- package/src/interfaces/desktop/assets/blobs/campana.png +0 -0
- package/src/interfaces/desktop/assets/blobs/caracol.png +0 -0
- package/src/interfaces/desktop/assets/blobs/carbon.png +0 -0
- package/src/interfaces/desktop/assets/blobs/cobalto.png +0 -0
- package/src/interfaces/desktop/assets/blobs/cometa.png +0 -0
- package/src/interfaces/desktop/assets/blobs/coral.png +0 -0
- package/src/interfaces/desktop/assets/blobs/cubi.png +0 -0
- package/src/interfaces/desktop/assets/blobs/eclipse.png +0 -0
- package/src/interfaces/desktop/assets/blobs/faro.png +0 -0
- package/src/interfaces/desktop/assets/blobs/gajo.png +0 -0
- package/src/interfaces/desktop/assets/blobs/iris.png +0 -0
- package/src/interfaces/desktop/assets/blobs/kiwi.png +0 -0
- package/src/interfaces/desktop/assets/blobs/laguna.png +0 -0
- package/src/interfaces/desktop/assets/blobs/lila.png +0 -0
- package/src/interfaces/desktop/assets/blobs/menta.png +0 -0
- package/src/interfaces/desktop/assets/blobs/nimbo.png +0 -0
- package/src/interfaces/desktop/assets/blobs/noche.png +0 -0
- package/src/interfaces/desktop/assets/blobs/onyx.png +0 -0
- package/src/interfaces/desktop/assets/blobs/orbita.png +0 -0
- package/src/interfaces/desktop/assets/blobs/ovni.png +0 -0
- package/src/interfaces/desktop/assets/blobs/papa.png +0 -0
- package/src/interfaces/desktop/assets/blobs/parche.png +0 -0
- package/src/interfaces/desktop/assets/blobs/peon.png +0 -0
- package/src/interfaces/desktop/assets/blobs/perla.png +0 -0
- package/src/interfaces/desktop/assets/blobs/pino.png +0 -0
- package/src/interfaces/desktop/assets/blobs/quad.png +0 -0
- package/src/interfaces/desktop/assets/blobs/rubi.png +0 -0
- package/src/interfaces/desktop/assets/blobs/saturno.png +0 -0
- package/src/interfaces/desktop/assets/blobs/trebol.png +0 -0
- package/src/interfaces/desktop/assets/blobs/trino.png +0 -0
- package/src/interfaces/desktop/assets/blobs/volcan.png +0 -0
- package/src/interfaces/desktop/assets/blobs/zafiro.png +0 -0
- package/src/interfaces/desktop/assets/notification.mp3 +0 -0
- package/src/interfaces/desktop/main.js +473 -29
- package/src/interfaces/desktop/mascot-blobs.js +41 -0
- package/src/interfaces/desktop/mascot-preload.js +32 -0
- package/src/interfaces/desktop/mascot.css +186 -0
- package/src/interfaces/desktop/mascot.html +40 -0
- package/src/interfaces/desktop/mascot.js +163 -0
- package/src/interfaces/desktop/preload.js +10 -0
- package/src/interfaces/desktop/renderer.js +308 -48
- package/src/interfaces/desktop/style.css +34 -0
- package/src/interfaces/mcp-server/index.js +87 -0
- package/src/interfaces/tui/_shims/util-locale.ts +0 -2
- package/src/interfaces/tui/app.tsx +0 -6
- package/src/interfaces/tui/attach.ts +0 -1
- package/src/interfaces/tui/context/sdk-apx.tsx +0 -4
- package/src/interfaces/tui/plugin/api.tsx +0 -3
- package/src/interfaces/tui/plugin/runtime.ts +0 -3
- package/src/interfaces/tui/thread.ts +0 -16
- package/src/interfaces/web/README.md +22 -14
- package/src/interfaces/web/dist/assets/claude-XtpLmGtF.webp +0 -0
- package/src/interfaces/web/dist/assets/codex-DExPUgvh.webp +0 -0
- package/src/interfaces/web/dist/assets/index-BEoRgIqd.css +32 -0
- package/src/interfaces/web/dist/assets/index-BEoRgIqd.css.br +0 -0
- package/src/interfaces/web/dist/assets/index-BEoRgIqd.css.gz +0 -0
- package/src/interfaces/web/dist/assets/index-C6fXHc1p.js +1074 -0
- package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.br +0 -0
- package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.gz +0 -0
- package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map.br +0 -0
- package/src/interfaces/web/dist/assets/index-C6fXHc1p.js.map.gz +0 -0
- package/src/interfaces/web/dist/favicon/dark/site.webmanifest +12 -6
- package/src/interfaces/web/dist/favicon/white/site.webmanifest +12 -6
- package/src/interfaces/web/dist/index.html +22 -6
- package/src/interfaces/web/dist/index.html.br +0 -0
- package/src/interfaces/web/dist/index.html.gz +0 -0
- package/src/interfaces/web/dist/manifest.webmanifest +25 -0
- package/src/interfaces/web/dist/modules/blobs/amatista.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/aqua.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/brasa.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/campana.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/caracol.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/carbon.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/cobalto.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/cometa.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/coral.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/cubi.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/eclipse.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/faro.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/gajo.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/iris.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/kiwi.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/laguna.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/lila.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/menta.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/nimbo.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/noche.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/onyx.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/orbita.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/ovni.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/papa.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/parche.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/peon.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/perla.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/pino.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/quad.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/rubi.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/saturno.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/trebol.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/trino.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/volcan.png +0 -0
- package/src/interfaces/web/dist/modules/blobs/zafiro.png +0 -0
- package/src/interfaces/web/dist/sw.js +106 -0
- package/src/interfaces/web/dist/sw.js.br +0 -0
- package/src/interfaces/web/dist/sw.js.gz +0 -0
- package/src/interfaces/web/eslint.config.js +83 -0
- package/src/interfaces/web/index.html +20 -4
- package/src/interfaces/web/package-lock.json +1651 -1280
- package/src/interfaces/web/package.json +9 -10
- package/src/interfaces/web/pnpm-lock.yaml +867 -869
- package/src/interfaces/web/public/favicon/dark/site.webmanifest +12 -6
- package/src/interfaces/web/public/favicon/white/site.webmanifest +12 -6
- package/src/interfaces/web/public/manifest.webmanifest +25 -0
- package/src/interfaces/web/public/modules/blobs/amatista.png +0 -0
- package/src/interfaces/web/public/modules/blobs/aqua.png +0 -0
- package/src/interfaces/web/public/modules/blobs/brasa.png +0 -0
- package/src/interfaces/web/public/modules/blobs/campana.png +0 -0
- package/src/interfaces/web/public/modules/blobs/caracol.png +0 -0
- package/src/interfaces/web/public/modules/blobs/carbon.png +0 -0
- package/src/interfaces/web/public/modules/blobs/cobalto.png +0 -0
- package/src/interfaces/web/public/modules/blobs/cometa.png +0 -0
- package/src/interfaces/web/public/modules/blobs/coral.png +0 -0
- package/src/interfaces/web/public/modules/blobs/cubi.png +0 -0
- package/src/interfaces/web/public/modules/blobs/eclipse.png +0 -0
- package/src/interfaces/web/public/modules/blobs/faro.png +0 -0
- package/src/interfaces/web/public/modules/blobs/gajo.png +0 -0
- package/src/interfaces/web/public/modules/blobs/iris.png +0 -0
- package/src/interfaces/web/public/modules/blobs/kiwi.png +0 -0
- package/src/interfaces/web/public/modules/blobs/laguna.png +0 -0
- package/src/interfaces/web/public/modules/blobs/lila.png +0 -0
- package/src/interfaces/web/public/modules/blobs/menta.png +0 -0
- package/src/interfaces/web/public/modules/blobs/nimbo.png +0 -0
- package/src/interfaces/web/public/modules/blobs/noche.png +0 -0
- package/src/interfaces/web/public/modules/blobs/onyx.png +0 -0
- package/src/interfaces/web/public/modules/blobs/orbita.png +0 -0
- package/src/interfaces/web/public/modules/blobs/ovni.png +0 -0
- package/src/interfaces/web/public/modules/blobs/papa.png +0 -0
- package/src/interfaces/web/public/modules/blobs/parche.png +0 -0
- package/src/interfaces/web/public/modules/blobs/peon.png +0 -0
- package/src/interfaces/web/public/modules/blobs/perla.png +0 -0
- package/src/interfaces/web/public/modules/blobs/pino.png +0 -0
- package/src/interfaces/web/public/modules/blobs/quad.png +0 -0
- package/src/interfaces/web/public/modules/blobs/rubi.png +0 -0
- package/src/interfaces/web/public/modules/blobs/saturno.png +0 -0
- package/src/interfaces/web/public/modules/blobs/trebol.png +0 -0
- package/src/interfaces/web/public/modules/blobs/trino.png +0 -0
- package/src/interfaces/web/public/modules/blobs/volcan.png +0 -0
- package/src/interfaces/web/public/modules/blobs/zafiro.png +0 -0
- package/src/interfaces/web/public/sw.js +106 -0
- package/src/interfaces/web/src/App.tsx +106 -10
- package/src/interfaces/web/src/assets/cli/claude.webp +0 -0
- package/src/interfaces/web/src/assets/cli/codex.webp +0 -0
- package/src/interfaces/web/src/assets/cli/cursor.svg +1 -0
- package/src/interfaces/web/src/assets/cli/opencode.png +0 -0
- package/src/interfaces/web/src/components/Combobox.tsx +201 -0
- package/src/interfaces/web/src/components/MobileHint.tsx +95 -0
- package/src/interfaces/web/src/components/MobileLinkDialog.tsx +156 -0
- package/src/interfaces/web/src/components/ModelCombobox.tsx +21 -113
- package/src/interfaces/web/src/components/{routines/ReadOnlyBlock.tsx → ReadOnlyBlock.tsx} +13 -4
- package/src/interfaces/web/src/components/Roby.tsx +5 -3
- package/src/interfaces/web/src/components/RobyBubble.tsx +23 -5
- package/src/interfaces/web/src/components/RowMenu.tsx +46 -0
- package/src/interfaces/web/src/components/TimezoneSelect.tsx +4 -1
- package/src/interfaces/web/src/components/Toast.tsx +50 -14
- package/src/interfaces/web/src/components/UiSelect.tsx +12 -5
- package/src/interfaces/web/src/components/agents/AgentAvatar.tsx +171 -0
- package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +42 -11
- package/src/interfaces/web/src/components/agents/AgentModelBadge.tsx +157 -0
- package/src/interfaces/web/src/components/agents/AgentModelSelect.tsx +57 -0
- package/src/interfaces/web/src/components/agents/AgentSkillsPicker.tsx +115 -0
- package/src/interfaces/web/src/components/agents/BlobAvatar.tsx +50 -0
- package/src/interfaces/web/src/components/agents/blobPresets.ts +50 -0
- package/src/interfaces/web/src/components/agents/modelCatalog.ts +66 -0
- package/src/interfaces/web/src/components/chat/ActionGroup.tsx +162 -0
- package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +4 -3
- package/src/interfaces/web/src/components/chat/AskQuestionsCard.tsx +1 -1
- package/src/interfaces/web/src/components/chat/Attachment.tsx +220 -0
- package/src/interfaces/web/src/components/chat/ChatList.tsx +226 -51
- package/src/interfaces/web/src/components/chat/ChatRowActivity.tsx +45 -0
- package/src/interfaces/web/src/components/chat/Composer.tsx +672 -23
- package/src/interfaces/web/src/components/chat/ComposerCommands.tsx +175 -0
- package/src/interfaces/web/src/components/chat/ComposerSuggest.tsx +215 -0
- package/src/interfaces/web/src/components/chat/ContextBar.tsx +133 -65
- package/src/interfaces/web/src/components/chat/InlineAskPanel.tsx +107 -47
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +460 -119
- package/src/interfaces/web/src/components/chat/MessageList.tsx +165 -18
- package/src/interfaces/web/src/components/chat/ReasoningBlock.tsx +58 -0
- package/src/interfaces/web/src/components/chat/SessionPicker.tsx +182 -0
- package/src/interfaces/web/src/components/chat/SkillTrace.tsx +183 -0
- package/src/interfaces/web/src/components/chat/ToolCall.tsx +11 -11
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +87 -53
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +11 -52
- package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +5 -4
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +31 -7
- package/src/interfaces/web/src/components/code/CodeSessionList.tsx +32 -11
- package/src/interfaces/web/src/components/code/CodeSidePanel.tsx +33 -3
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +5 -4
- package/src/interfaces/web/src/components/code/NewCodeSessionDialog.tsx +164 -0
- package/src/interfaces/web/src/components/commitments/CommitmentDetail.tsx +162 -0
- package/src/interfaces/web/src/components/commitments/CommitmentFormDialog.tsx +158 -0
- package/src/interfaces/web/src/components/commitments/CommitmentList.tsx +194 -0
- package/src/interfaces/web/src/components/commitments/commitmentState.tsx +46 -0
- package/src/interfaces/web/src/components/common/BulkActionBar.tsx +60 -0
- package/src/interfaces/web/src/components/common/ConfirmDialog.tsx +4 -1
- package/src/interfaces/web/src/components/common/SelectCheckbox.tsx +40 -0
- package/src/interfaces/web/src/components/common/TabNav.tsx +1 -1
- package/src/interfaces/web/src/components/cron/CronPicker.tsx +1 -1
- package/src/interfaces/web/src/components/files/FileBrowser.tsx +1 -1
- package/src/interfaces/web/src/components/files/FileTree.tsx +6 -4
- package/src/interfaces/web/src/components/files/FileViewer.tsx +9 -5
- package/src/interfaces/web/src/components/files/MarkdownPreview.tsx +45 -9
- package/src/interfaces/web/src/components/images/ImageDefaultsCard.tsx +93 -0
- package/src/interfaces/web/src/components/images/ImageProviderList.tsx +142 -0
- package/src/interfaces/web/src/components/images/ImageProviderModal.tsx +263 -0
- package/src/interfaces/web/src/components/images/ImageTestCard.tsx +142 -0
- package/src/interfaces/web/src/components/inbox/ChannelFilter.tsx +178 -0
- package/src/interfaces/web/src/components/inbox/InboxList.tsx +77 -81
- package/src/interfaces/web/src/components/inbox/InboxRowItem.tsx +140 -0
- package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +2 -2
- package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +208 -30
- package/src/interfaces/web/src/components/integrations/PluginToolsSection.tsx +1 -1
- package/src/interfaces/web/src/components/layout/ProjectAvatar.tsx +39 -24
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +253 -58
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +94 -47
- package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +202 -90
- package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +28 -6
- package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +16 -5
- package/src/interfaces/web/src/components/routines/RoutineList.tsx +49 -9
- package/src/interfaces/web/src/components/routines/VarTextarea.tsx +1 -1
- package/src/interfaces/web/src/components/routines/runChat.ts +121 -0
- package/src/interfaces/web/src/components/sessions/SessionDetail.tsx +148 -0
- package/src/interfaces/web/src/components/sessions/SessionTerminal.tsx +98 -0
- package/src/interfaces/web/src/components/settings/AccessPanel.tsx +205 -0
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +246 -101
- package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +36 -2
- package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +19 -6
- package/src/interfaces/web/src/components/settings/EmbedProviderList.tsx +274 -0
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +26 -6
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +118 -119
- package/src/interfaces/web/src/components/settings/NudgePanel.tsx +4 -3
- package/src/interfaces/web/src/components/settings/PairDeviceDialog.tsx +15 -4
- package/src/interfaces/web/src/components/settings/PanelPrefs.tsx +374 -0
- package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +70 -2
- package/src/interfaces/web/src/components/settings/RoutingPanel.tsx +2 -1
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +102 -43
- package/src/interfaces/web/src/components/settings/SkillsManager.tsx +76 -21
- package/src/interfaces/web/src/components/settings/SkillsSettings.tsx +10 -3
- package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +8 -0
- package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +11 -2
- package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +19 -8
- package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +12 -2
- package/src/interfaces/web/src/components/settings/WebPanel.tsx +22 -31
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +15 -3
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +97 -66
- package/src/interfaces/web/src/components/settings/providers/ProviderTestDialog.tsx +150 -0
- package/src/interfaces/web/src/components/settings/providers/typeStyles.ts +67 -25
- package/src/interfaces/web/src/components/settings/providers/types.ts +3 -0
- package/src/interfaces/web/src/components/tasks/TaskBoard.tsx +201 -0
- package/src/interfaces/web/src/components/tasks/TaskColumnsDialog.tsx +243 -0
- package/src/interfaces/web/src/components/tasks/TaskComments.tsx +167 -0
- package/src/interfaces/web/src/components/tasks/TaskDetail.tsx +335 -0
- package/src/interfaces/web/src/components/tasks/TaskFormDialog.tsx +406 -0
- package/src/interfaces/web/src/components/tasks/TaskList.tsx +258 -0
- package/src/interfaces/web/src/components/tasks/TaskSubtasks.tsx +126 -0
- package/src/interfaces/web/src/components/tasks/columns.ts +51 -0
- package/src/interfaces/web/src/components/tasks/taskStatus.tsx +159 -34
- package/src/interfaces/web/src/components/tasks/taskViewPrefs.ts +73 -0
- package/src/interfaces/web/src/components/tasks/useTaskColumns.ts +31 -0
- package/src/interfaces/web/src/components/ui/chat-input.tsx +136 -12
- package/src/interfaces/web/src/components/ui/context-menu.tsx +15 -198
- package/src/interfaces/web/src/components/ui/dialog.tsx +3 -2
- package/src/interfaces/web/src/components/ui/filter-chips.tsx +4 -0
- package/src/interfaces/web/src/components/ui/popover.tsx +14 -49
- package/src/interfaces/web/src/components/ui/sheet.tsx +2 -1
- package/src/interfaces/web/src/components/ui/spinner.tsx +2 -1
- package/src/interfaces/web/src/components/ui.tsx +62 -23
- package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +2 -1
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +66 -8
- package/src/interfaces/web/src/components/voice/VoiceQvoxInstallCard.tsx +63 -0
- package/src/interfaces/web/src/components/voice/VoiceQvoxStatusCard.tsx +51 -0
- package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +4 -3
- package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +38 -4
- package/src/interfaces/web/src/constants/index.ts +1 -0
- package/src/interfaces/web/src/hooks/useChannelPrefs.ts +39 -0
- package/src/interfaces/web/src/hooks/useChat.ts +1092 -85
- package/src/interfaces/web/src/hooks/useChatActivity.ts +24 -0
- package/src/interfaces/web/src/hooks/useInbox.ts +26 -4
- package/src/interfaces/web/src/hooks/useLiveMessages.ts +16 -0
- package/src/interfaces/web/src/hooks/useOllamaModels.ts +79 -0
- package/src/interfaces/web/src/hooks/useRecorder.ts +198 -0
- package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +23 -6
- package/src/interfaces/web/src/i18n/en.ts +832 -175
- package/src/interfaces/web/src/i18n/es.ts +841 -188
- package/src/interfaces/web/src/lib/api/agents.ts +23 -4
- package/src/interfaces/web/src/lib/api/code.ts +17 -0
- package/src/interfaces/web/src/lib/api/commitments.ts +7 -1
- package/src/interfaces/web/src/lib/api/conversations.ts +38 -11
- package/src/interfaces/web/src/lib/api/embeddings.ts +3 -0
- package/src/interfaces/web/src/lib/api/engines.ts +14 -0
- package/src/interfaces/web/src/lib/api/groups.ts +93 -0
- package/src/interfaces/web/src/lib/api/images.ts +193 -0
- package/src/interfaces/web/src/lib/api/inbox.ts +29 -5
- package/src/interfaces/web/src/lib/api/integrations.ts +9 -0
- package/src/interfaces/web/src/lib/api/media.ts +126 -0
- package/src/interfaces/web/src/lib/api/net.ts +44 -0
- package/src/interfaces/web/src/lib/api/profiles.ts +12 -0
- package/src/interfaces/web/src/lib/api/projects.ts +9 -1
- package/src/interfaces/web/src/lib/api/routines.ts +9 -1
- package/src/interfaces/web/src/lib/api/sessions.ts +18 -0
- package/src/interfaces/web/src/lib/api/skills.ts +8 -0
- package/src/interfaces/web/src/lib/api/super_agent.ts +3 -0
- package/src/interfaces/web/src/lib/api/tasks.ts +46 -6
- package/src/interfaces/web/src/lib/api/tools.ts +2 -0
- package/src/interfaces/web/src/lib/api/transcribe.ts +31 -0
- package/src/interfaces/web/src/lib/api/turns.ts +20 -0
- package/src/interfaces/web/src/lib/api/voice.ts +72 -2
- package/src/interfaces/web/src/lib/api.ts +3 -0
- package/src/interfaces/web/src/lib/channels.ts +214 -0
- package/src/interfaces/web/src/lib/chat-activity.ts +175 -0
- package/src/interfaces/web/src/lib/chat-prefs.ts +48 -0
- package/src/interfaces/web/src/lib/code-context.ts +0 -2
- package/src/interfaces/web/src/lib/http.ts +51 -17
- package/src/interfaces/web/src/lib/inbox-selection.ts +29 -0
- package/src/interfaces/web/src/lib/live.ts +252 -0
- package/src/interfaces/web/src/lib/net.ts +211 -0
- package/src/interfaces/web/src/lib/notify.ts +331 -0
- package/src/interfaces/web/src/lib/projectNav.ts +40 -0
- package/src/interfaces/web/src/lib/pwa.ts +115 -0
- package/src/interfaces/web/src/lib/qvox.ts +24 -0
- package/src/interfaces/web/src/lib/slug.ts +3 -0
- package/src/interfaces/web/src/lib/tone.ts +150 -0
- package/src/interfaces/web/src/main.tsx +15 -4
- package/src/interfaces/web/src/screens/ApxAdminScreen.tsx +33 -11
- package/src/interfaces/web/src/screens/InboxScreen.tsx +126 -71
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -4
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +7 -4
- package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +30 -9
- package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +150 -168
- package/src/interfaces/web/src/screens/base/LogsTab.tsx +8 -7
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +99 -37
- package/src/interfaces/web/src/screens/base/WorkspacesTab.tsx +1 -1
- package/src/interfaces/web/src/screens/mobile/MobileChat.tsx +85 -0
- package/src/interfaces/web/src/screens/mobile/MobileChatList.tsx +244 -0
- package/src/interfaces/web/src/screens/mobile/MobileCommitments.tsx +350 -0
- package/src/interfaces/web/src/screens/mobile/MobileScreen.tsx +138 -0
- package/src/interfaces/web/src/screens/mobile/MobileTabBar.tsx +105 -0
- package/src/interfaces/web/src/screens/mobile/MobileTasks.tsx +407 -0
- package/src/interfaces/web/src/screens/mobile/NewChatSheet.tsx +313 -0
- package/src/interfaces/web/src/screens/mobile/mobileList.tsx +194 -0
- package/src/interfaces/web/src/screens/mobile/routes.ts +239 -0
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +135 -64
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +0 -1
- package/src/interfaces/web/src/screens/modules/ImagesScreen.tsx +172 -0
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +36 -4
- package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +117 -30
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +537 -92
- package/src/interfaces/web/src/screens/project/AgentsTab.tsx +344 -93
- package/src/interfaces/web/src/screens/project/ArtifactsTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +1160 -94
- package/src/interfaces/web/src/screens/project/IntegrationsTab.tsx +3 -2
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +62 -14
- package/src/interfaces/web/src/screens/project/Overview.tsx +36 -15
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +93 -8
- package/src/interfaces/web/src/screens/project/SkillsTab.tsx +4 -1
- package/src/interfaces/web/src/screens/project/StructureTab.tsx +5 -4
- package/src/interfaces/web/src/screens/project/TasksTab.tsx +351 -129
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +17 -5
- package/src/interfaces/web/src/styles.css +106 -31
- package/src/interfaces/web/src/types/daemon.ts +246 -2
- package/src/interfaces/web/src/vite-env.d.ts +20 -0
- package/src/interfaces/tui/plugin/command-shim.ts +0 -109
- package/src/interfaces/web/components.json +0 -25
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +0 -849
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +0 -1
- package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +0 -142
- package/src/interfaces/web/src/components/ui/accordion.tsx +0 -72
- package/src/interfaces/web/src/components/ui/alert-dialog.tsx +0 -187
- package/src/interfaces/web/src/components/ui/alert.tsx +0 -76
- package/src/interfaces/web/src/components/ui/aspect-ratio.tsx +0 -22
- package/src/interfaces/web/src/components/ui/avatar.tsx +0 -107
- package/src/interfaces/web/src/components/ui/breadcrumb.tsx +0 -125
- package/src/interfaces/web/src/components/ui/button-group.tsx +0 -87
- package/src/interfaces/web/src/components/ui/calendar.tsx +0 -221
- package/src/interfaces/web/src/components/ui/card.tsx +0 -103
- package/src/interfaces/web/src/components/ui/carousel.tsx +0 -242
- package/src/interfaces/web/src/components/ui/chart.tsx +0 -371
- package/src/interfaces/web/src/components/ui/checkbox.tsx +0 -29
- package/src/interfaces/web/src/components/ui/collapsible.tsx +0 -19
- package/src/interfaces/web/src/components/ui/combobox.tsx +0 -295
- package/src/interfaces/web/src/components/ui/command.tsx +0 -196
- package/src/interfaces/web/src/components/ui/direction.tsx +0 -4
- package/src/interfaces/web/src/components/ui/drawer.tsx +0 -134
- package/src/interfaces/web/src/components/ui/empty.tsx +0 -104
- package/src/interfaces/web/src/components/ui/field.tsx +0 -236
- package/src/interfaces/web/src/components/ui/hover-card.tsx +0 -51
- package/src/interfaces/web/src/components/ui/input-otp.tsx +0 -85
- package/src/interfaces/web/src/components/ui/item.tsx +0 -201
- package/src/interfaces/web/src/components/ui/menubar.tsx +0 -280
- package/src/interfaces/web/src/components/ui/native-select.tsx +0 -61
- package/src/interfaces/web/src/components/ui/navigation-menu.tsx +0 -168
- package/src/interfaces/web/src/components/ui/pagination.tsx +0 -130
- package/src/interfaces/web/src/components/ui/progress.tsx +0 -83
- package/src/interfaces/web/src/components/ui/radio-group.tsx +0 -36
- package/src/interfaces/web/src/components/ui/resizable.tsx +0 -50
- package/src/interfaces/web/src/components/ui/scroll-area.tsx +0 -53
- package/src/interfaces/web/src/components/ui/sidebar.tsx +0 -726
- package/src/interfaces/web/src/components/ui/slider.tsx +0 -52
- package/src/interfaces/web/src/components/ui/sonner.tsx +0 -49
- package/src/interfaces/web/src/components/ui/table.tsx +0 -116
- package/src/interfaces/web/src/components/ui/toggle-group.tsx +0 -87
- package/src/interfaces/web/src/components/ui/toggle.tsx +0 -45
- package/src/interfaces/web/src/hooks/use-mobile.ts +0 -19
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +0 -161
|
@@ -1,19 +1,45 @@
|
|
|
1
|
-
import { useEffect, useMemo, useState } from "react";
|
|
2
|
-
import { useSearchParams } from "react-router-dom";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { useNavigate, useSearchParams } from "react-router-dom";
|
|
3
3
|
import useSWR, { mutate } from "swr";
|
|
4
|
-
import { Plus, RotateCcw, Trash2 } from "lucide-react";
|
|
5
|
-
import { Agents, Conversations } from "../../lib/api";
|
|
6
|
-
import {
|
|
4
|
+
import { Archive, ArchiveRestore, ArrowDown, ArrowUpRight, ChevronLeft, Eye, MessageSquareDashed, MoreVertical, Pencil, Plus, RotateCcw, Trash2, UserPlus, Wrench, X } from "lucide-react";
|
|
5
|
+
import { Agents, Conversations, Groups } from "../../lib/api";
|
|
6
|
+
import { Button, Dialog, Empty, Field, Input, Loading, Switch, Tip } from "../../components/ui";
|
|
7
7
|
import { Composer } from "../../components/chat/Composer";
|
|
8
8
|
import { MessageList } from "../../components/chat/MessageList";
|
|
9
9
|
import { ContextBar } from "../../components/chat/ContextBar";
|
|
10
10
|
import { InlineAskPanel, pendingAskQuestions } from "../../components/chat/InlineAskPanel";
|
|
11
|
-
import { ChatList, type ChatKey, type ChatSelectionMeta } from "../../components/chat/ChatList";
|
|
12
|
-
import {
|
|
11
|
+
import { ChatList, chatKeyToString, type ChatKey, type ChatSelectionMeta } from "../../components/chat/ChatList";
|
|
12
|
+
import { queryForChat } from "../mobile/routes";
|
|
13
|
+
import { SessionPicker } from "../../components/chat/SessionPicker";
|
|
14
|
+
import { messageForInvite, parseInviteCommand } from "../../components/chat/ComposerCommands";
|
|
15
|
+
import {
|
|
16
|
+
DropdownMenu,
|
|
17
|
+
DropdownMenuContent,
|
|
18
|
+
DropdownMenuGroup,
|
|
19
|
+
DropdownMenuItem,
|
|
20
|
+
DropdownMenuLabel,
|
|
21
|
+
DropdownMenuSeparator,
|
|
22
|
+
DropdownMenuTrigger,
|
|
23
|
+
} from "../../components/ui/dropdown-menu";
|
|
24
|
+
import { attachmentsOf, useChat, type ChatMsg } from "../../hooks/useChat";
|
|
25
|
+
import { useLiveMessages } from "../../hooks/useLiveMessages";
|
|
26
|
+
import { concernsConversation, concernsThread, type LiveEvent } from "../../lib/live";
|
|
27
|
+
import type { UploadedMedia } from "../../lib/api/media";
|
|
13
28
|
import { useToast } from "../../components/Toast";
|
|
29
|
+
import { cn } from "../../lib/cn";
|
|
30
|
+
import { isNativeShell } from "../../lib/net";
|
|
14
31
|
import { t } from "../../i18n";
|
|
32
|
+
import { toneChip } from "../../lib/tone";
|
|
15
33
|
import { usePersonaName } from "../../hooks/usePersonaName";
|
|
16
|
-
import
|
|
34
|
+
import { useSuperAgentConfig } from "../../hooks/useGlobalConfig";
|
|
35
|
+
import { AgentAvatar, AgentAvatarGroup, SUPER_AGENT_ICON, type AgentFace } from "../../components/agents/AgentAvatar";
|
|
36
|
+
import type { AgentEntry, ConversationListEntry } from "../../types/daemon";
|
|
37
|
+
import { useChatVisibility } from "../../hooks/useChatActivity";
|
|
38
|
+
import {
|
|
39
|
+
conversationActivityKey,
|
|
40
|
+
liveActivityKey,
|
|
41
|
+
threadActivityKey,
|
|
42
|
+
} from "../../lib/chat-activity";
|
|
17
43
|
|
|
18
44
|
// Virtual entry slug used in the agent dropdown to address the daemon-level
|
|
19
45
|
// super-agent (persona "Roby" for the owner). Picked so it can't collide
|
|
@@ -28,22 +54,110 @@ const ROBY_SLUG = "__super_agent__";
|
|
|
28
54
|
export function ChatTab({
|
|
29
55
|
pid,
|
|
30
56
|
hideSidebar = false,
|
|
57
|
+
hideHeader = false,
|
|
31
58
|
initialSelection,
|
|
59
|
+
bare = false,
|
|
60
|
+
onOpenInProject,
|
|
61
|
+
compact = false,
|
|
62
|
+
onBack,
|
|
63
|
+
onSelectionChange,
|
|
64
|
+
channelScope,
|
|
32
65
|
}: {
|
|
33
66
|
pid: string;
|
|
34
67
|
hideSidebar?: boolean;
|
|
68
|
+
/** Drop the thread header. The phone surface draws its own — with the back
|
|
69
|
+
* button and the session switcher on it — and two stacked headers naming the
|
|
70
|
+
* same agent is a rendering bug, not a layout. */
|
|
71
|
+
hideHeader?: boolean;
|
|
35
72
|
/** Open a specific conversation on mount instead of a fresh live session. */
|
|
36
73
|
initialSelection?: ChatKey;
|
|
74
|
+
/** Drop the card chrome. An embedding screen already draws a panel around
|
|
75
|
+
* this, and a card inside a card reads as a rendering bug. */
|
|
76
|
+
bare?: boolean;
|
|
77
|
+
/** Structural way out from the inbox axis (leave inbox → project / agent card).
|
|
78
|
+
* When omitted, a 1:1 project-agent chat defaults to opening that agent's
|
|
79
|
+
* ficha (`/p/:pid/agents/:slug`) via the face and the ⋯ menu. */
|
|
80
|
+
onOpenInProject?: () => void;
|
|
81
|
+
/** Phone shaping: no avatar column, wider bubbles, and a header sized for a
|
|
82
|
+
* thumb with its actions folded behind one ⋯. The phone surface sets it; the
|
|
83
|
+
* desktop pane never does. */
|
|
84
|
+
compact?: boolean;
|
|
85
|
+
/** Renders the back arrow in the header. Only the phone has anywhere to go
|
|
86
|
+
* back TO — inside a project the chat is a tab, not a screen. */
|
|
87
|
+
onBack?: () => void;
|
|
88
|
+
/** The host owns the URL. Passed by the phone surface, where a session is a
|
|
89
|
+
* path segment rather than a query string, so picking one has to navigate
|
|
90
|
+
* instead of writing `?conv=` that a reload would not read back. */
|
|
91
|
+
onSelectionChange?: (key: ChatKey) => void;
|
|
92
|
+
/** Limit the session switcher to one channel. The inbox and the phone pass
|
|
93
|
+
* "web" so their switcher never offers a Telegram thread; project-first
|
|
94
|
+
* navigation omits it and keeps every channel. */
|
|
95
|
+
channelScope?: string;
|
|
37
96
|
}) {
|
|
38
97
|
const toast = useToast();
|
|
98
|
+
const navigate = useNavigate();
|
|
39
99
|
const [params, setSearchParams] = useSearchParams();
|
|
40
100
|
const agents = useSWR(`/api/projects/${pid}/agents`, () => Agents.list(pid));
|
|
41
101
|
const [creating, setCreating] = useState(false);
|
|
42
102
|
const [model, setModel] = useState("");
|
|
43
103
|
const [dismissedAskKey, setDismissedAskKey] = useState<string | null>(null);
|
|
44
|
-
const { msgs, send: sendChat, stop, clear, load, loadThread, streaming } =
|
|
104
|
+
const { msgs, send: sendChat, sendGroup, regenerate, editAndResend, stop, clear, load, loadThread, streaming, queued, unqueue, conversationId, conversationMeta } =
|
|
45
105
|
useChat(pid, (m) => toast.error(m));
|
|
46
106
|
const persona = usePersonaName();
|
|
107
|
+
const { superAgent } = useSuperAgentConfig();
|
|
108
|
+
const superAgentIcon = superAgent?.icon || SUPER_AGENT_ICON;
|
|
109
|
+
|
|
110
|
+
// How tall the floating dock currently is. It moves constantly — a draft
|
|
111
|
+
// wrapping to a third line, an attachment strip appearing, the context panel
|
|
112
|
+
// opening — and the thread needs the live number, not a guess, or the last
|
|
113
|
+
// message ends up behind the field exactly when you are reading it.
|
|
114
|
+
//
|
|
115
|
+
// A callback ref, not an effect on a ref object: this component renders
|
|
116
|
+
// <Loading/> until the agent list arrives, so a mount effect would look for a
|
|
117
|
+
// dock that does not exist yet, find null, and never run again — leaving the
|
|
118
|
+
// inset at zero and the field parked on top of the last three lines.
|
|
119
|
+
const [dockH, setDockH] = useState(0);
|
|
120
|
+
// While the context detail is open the dock is temporarily much taller, and
|
|
121
|
+
// the thread must NOT reserve that: opening a panel for two seconds should
|
|
122
|
+
// cover the conversation, not shove it up and drop it back down. So the inset
|
|
123
|
+
// is the last height measured with the panel shut.
|
|
124
|
+
const [ctxOpen, setCtxOpen] = useState(false);
|
|
125
|
+
const restingDock = useRef(0);
|
|
126
|
+
if (!ctxOpen) restingDock.current = dockH;
|
|
127
|
+
const bottomInset = ctxOpen ? restingDock.current : dockH;
|
|
128
|
+
|
|
129
|
+
// Whether the reader is following the end of the thread, and what they have
|
|
130
|
+
// missed while they were not. Nothing scrolls on its own any more, so the
|
|
131
|
+
// count is the only way to know something arrived while you were reading
|
|
132
|
+
// further up — an arrow that appears silently is easy to look straight past.
|
|
133
|
+
const scrollerRef = useRef<HTMLDivElement | null>(null);
|
|
134
|
+
const [atBottom, setAtBottom] = useState(true);
|
|
135
|
+
const [missed, setMissed] = useState(0);
|
|
136
|
+
const seenCount = useRef(0);
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (atBottom) {
|
|
139
|
+
seenCount.current = msgs.length;
|
|
140
|
+
setMissed(0);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setMissed(Math.max(0, msgs.length - seenCount.current));
|
|
144
|
+
}, [msgs.length, atBottom]);
|
|
145
|
+
|
|
146
|
+
const jumpToLatest = useCallback(() => {
|
|
147
|
+
const el = scrollerRef.current;
|
|
148
|
+
if (el) el.scrollTo({ top: el.scrollHeight, behavior: "smooth" });
|
|
149
|
+
}, []);
|
|
150
|
+
const dockWatch = useRef<ResizeObserver | null>(null);
|
|
151
|
+
const dockRef = useCallback((el: HTMLDivElement | null) => {
|
|
152
|
+
dockWatch.current?.disconnect();
|
|
153
|
+
if (!el) {
|
|
154
|
+
dockWatch.current = null;
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const ro = new ResizeObserver(([entry]) => setDockH(entry.contentRect.height));
|
|
158
|
+
ro.observe(el);
|
|
159
|
+
dockWatch.current = ro;
|
|
160
|
+
}, []);
|
|
47
161
|
|
|
48
162
|
// Selection state — drives both the sidebar highlight and the right-pane
|
|
49
163
|
// header. Restored from the URL query on mount (so a chat is deep-linkable),
|
|
@@ -63,30 +177,55 @@ export function ChatTab({
|
|
|
63
177
|
if (agent) return { kind: "live", agentSlug: agent };
|
|
64
178
|
return { kind: "live", agentSlug: ROBY_SLUG };
|
|
65
179
|
});
|
|
180
|
+
// Nobody chose for us — no deep link in the URL, no host-supplied selection —
|
|
181
|
+
// so let the sidebar open this project's most recent chat once its lists land.
|
|
182
|
+
// Read once at mount: selectChat() writes those same params, and re-reading
|
|
183
|
+
// them later would turn every switch back to a live session into an auto-pick.
|
|
184
|
+
const [autoSelectLatest] = useState(
|
|
185
|
+
() =>
|
|
186
|
+
!initialSelection &&
|
|
187
|
+
!params.get("conv") &&
|
|
188
|
+
!params.get("thread") &&
|
|
189
|
+
!params.get("agent"),
|
|
190
|
+
);
|
|
66
191
|
// Display metadata for the current selection (channel/created date/title),
|
|
67
192
|
// carried from the sidebar so the header can show it without a second fetch.
|
|
68
193
|
const [selectedMeta, setSelectedMeta] = useState<ChatSelectionMeta | undefined>(undefined);
|
|
69
194
|
const [confirmDelete, setConfirmDelete] = useState(false);
|
|
195
|
+
const [confirmNew, setConfirmNew] = useState(false);
|
|
196
|
+
const [renaming, setRenaming] = useState<string | null>(null);
|
|
70
197
|
const [deleting, setDeleting] = useState(false);
|
|
198
|
+
const [addOpen, setAddOpen] = useState(false);
|
|
199
|
+
// A pending group rewind awaiting confirmation (there are messages after the
|
|
200
|
+
// target that the regenerate/edit would overwrite).
|
|
201
|
+
const [groupRewind, setGroupRewind] = useState<
|
|
202
|
+
{ kind: "edit" | "regen"; keepVisible: number; drop: number; text?: string; media?: UploadedMedia[]; from?: string; reason?: string | null } | null
|
|
203
|
+
>(null);
|
|
71
204
|
|
|
72
205
|
// Select a chat and mirror its id into the URL query so the current chat is
|
|
73
206
|
// shareable/deep-linkable. `replace` keeps navigation history clean.
|
|
74
207
|
const selectChat = (key: ChatKey, meta?: ChatSelectionMeta) => {
|
|
75
208
|
setSelected(key);
|
|
76
209
|
setSelectedMeta(meta);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
next.set("channel", key.channel);
|
|
83
|
-
next.set("thread", key.threadId);
|
|
84
|
-
} else {
|
|
85
|
-
next.set("agent", key.agentSlug);
|
|
210
|
+
// A host that owns the URL writes it in its own shape; writing both would
|
|
211
|
+
// leave two spellings of the same thing to disagree on the next reload.
|
|
212
|
+
if (onSelectionChange) {
|
|
213
|
+
onSelectionChange(key);
|
|
214
|
+
return;
|
|
86
215
|
}
|
|
87
|
-
setSearchParams(
|
|
216
|
+
setSearchParams(queryForChat(key), { replace: true });
|
|
88
217
|
};
|
|
89
218
|
|
|
219
|
+
// An embedding host (the inbox) picks the thread via initialSelection and
|
|
220
|
+
// never calls selectChat. Without this write, `/inbox` has no query, and
|
|
221
|
+
// agent notifications cannot tell you are already reading the row that just
|
|
222
|
+
// moved — they fire for a message already on screen.
|
|
223
|
+
const initialAddr = initialSelection && !onSelectionChange ? queryForChat(initialSelection).toString() : "";
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (!initialAddr) return;
|
|
226
|
+
setSearchParams(new URLSearchParams(initialAddr), { replace: true });
|
|
227
|
+
}, [initialAddr, setSearchParams]);
|
|
228
|
+
|
|
90
229
|
const agentList = agents.data || [];
|
|
91
230
|
const isRoby = (slug: string | null | undefined) => slug === ROBY_SLUG;
|
|
92
231
|
|
|
@@ -101,6 +240,38 @@ export function ChatTab({
|
|
|
101
240
|
);
|
|
102
241
|
const activeIsRoby = selected.kind === "thread" || isRoby(selected.agentSlug);
|
|
103
242
|
|
|
243
|
+
const visibleActivityKey = useMemo(() => {
|
|
244
|
+
if (selected.kind === "conv") return conversationActivityKey(pid, selected.convId);
|
|
245
|
+
if (selected.kind === "thread") {
|
|
246
|
+
return threadActivityKey(pid, selected.channel, selected.threadId);
|
|
247
|
+
}
|
|
248
|
+
if (conversationId) return conversationActivityKey(pid, conversationId);
|
|
249
|
+
return activeIsRoby
|
|
250
|
+
? threadActivityKey(pid, "web", new Date().toISOString().slice(0, 10))
|
|
251
|
+
: liveActivityKey(pid, selected.agentSlug);
|
|
252
|
+
}, [pid, selected, conversationId, activeIsRoby]);
|
|
253
|
+
useChatVisibility(visibleActivityKey);
|
|
254
|
+
|
|
255
|
+
const isA2A = selected.kind === "thread" && selected.channel === "a2a";
|
|
256
|
+
const isGroup = selected.kind === "thread" && selected.channel === "group";
|
|
257
|
+
// a2a and group are both multi-agent threads: many faces, no super-agent badge,
|
|
258
|
+
// no rewind. Their name comes from the thread, not an agent.
|
|
259
|
+
const isMultiThread = isA2A || isGroup;
|
|
260
|
+
|
|
261
|
+
// Transcript layout: tools visible (ActionGroup) vs pelado (text only).
|
|
262
|
+
// Persisted per chat so the header switch and the create-group checkbox stick,
|
|
263
|
+
// falling back to this device's last header flip for chats never set.
|
|
264
|
+
const showToolsKey = `${SHOW_TOOLS_PREF}.${pid}.${chatKeyToString(selected)}`;
|
|
265
|
+
const [showTools, setShowToolsState] = useState(() => readShowTools(showToolsKey));
|
|
266
|
+
useEffect(() => {
|
|
267
|
+
setShowToolsState(readShowTools(showToolsKey));
|
|
268
|
+
}, [showToolsKey]);
|
|
269
|
+
const setShowTools = (v: boolean) => {
|
|
270
|
+
setShowToolsState(v);
|
|
271
|
+
writeShowTools(showToolsKey, v);
|
|
272
|
+
writeShowToolsDefault(v);
|
|
273
|
+
};
|
|
274
|
+
|
|
104
275
|
// Whenever the user picks a stored conversation or a channel thread, reload
|
|
105
276
|
// the in-memory chat with its persisted history. Conversations bind the
|
|
106
277
|
// conversation_id (sends append to the file); threads stay unbound —
|
|
@@ -115,7 +286,11 @@ export function ChatTab({
|
|
|
115
286
|
// conversationId undefined, so an `if (conversationId)` guard would skip
|
|
116
287
|
// clearing and the previous chat's messages would linger under the new
|
|
117
288
|
// header — the "title changes but content stays" bug.)
|
|
118
|
-
clear(
|
|
289
|
+
clear(
|
|
290
|
+
isRoby(selected.agentSlug)
|
|
291
|
+
? threadActivityKey(pid, "web", new Date().toISOString().slice(0, 10))
|
|
292
|
+
: liveActivityKey(pid, selected.agentSlug),
|
|
293
|
+
);
|
|
119
294
|
}
|
|
120
295
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
121
296
|
}, [
|
|
@@ -127,13 +302,95 @@ export function ChatTab({
|
|
|
127
302
|
: selected.agentSlug,
|
|
128
303
|
]);
|
|
129
304
|
|
|
130
|
-
|
|
305
|
+
// Who is in this room, straight off the loaded thread. The daemon resolves the
|
|
306
|
+
// roster and the faces with the messages (api/thread-faces.js), so there is
|
|
307
|
+
// nothing left to fetch a second time and nothing to re-derive: the header
|
|
308
|
+
// shows every member and "add someone" knows who is already in.
|
|
309
|
+
const groupParticipants = conversationMeta?.participants ?? [];
|
|
310
|
+
|
|
311
|
+
// The conversation on screen moved somewhere ELSE — a message on Telegram,
|
|
312
|
+
// another device on the same thread, a routine writing into the file. The
|
|
313
|
+
// daemon says which thread moved and we re-read it; nothing arrives over the
|
|
314
|
+
// socket except the fact that something did (see lib/live.ts).
|
|
315
|
+
//
|
|
316
|
+
// Skipped while this tab is streaming its own answer: the turn being painted
|
|
317
|
+
// token by token is not in storage yet, and re-reading mid-stream would
|
|
318
|
+
// replace it with a version that stops at the last thing written.
|
|
319
|
+
useLiveMessages(
|
|
320
|
+
useCallback(
|
|
321
|
+
(events: LiveEvent[]) => {
|
|
322
|
+
if (streaming) return;
|
|
323
|
+
if (selected.kind === "thread") {
|
|
324
|
+
if (events.some((e) => concernsThread(e, selected.channel, selected.threadId))) {
|
|
325
|
+
void loadThread(selected.channel, selected.threadId, { silent: true });
|
|
326
|
+
}
|
|
327
|
+
} else if (selected.kind === "conv") {
|
|
328
|
+
if (events.some((e) => concernsConversation(e, selected.agentSlug, selected.convId))) {
|
|
329
|
+
void load(selected.agentSlug, selected.convId, { silent: true });
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// A live session has no stored thread to catch up with: everything it
|
|
333
|
+
// shows was produced in this tab.
|
|
334
|
+
},
|
|
335
|
+
[selected, streaming, load, loadThread],
|
|
336
|
+
),
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// A group room streams through useChat's own pending-bubble machinery — the
|
|
340
|
+
// owner's line fans out to the members and each speaker's tokens land live,
|
|
341
|
+
// exactly like a 1:1 turn. Refresh the sidebar/inbox once it settles.
|
|
342
|
+
const nameOfSlug = (slug: string) => agentList.find((a) => a.slug === slug)?.name || slug;
|
|
343
|
+
const groupSend = async (gid: string, text: string, media?: UploadedMedia[]) => {
|
|
344
|
+
await sendGroup(gid, text, nameOfSlug, media?.length ? { media } : undefined);
|
|
345
|
+
void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
346
|
+
void mutate((key) => typeof key === "string" && key.startsWith(`/api/inbox`));
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const send = async (text: string, media?: UploadedMedia[]) => {
|
|
350
|
+
// `/invite andy hola…` — pull them in, then send the message (with @ so
|
|
351
|
+
// the cascade addresses them). The composer only inserts the command; the
|
|
352
|
+
// invite itself happens here on send.
|
|
353
|
+
const inv = parseInviteCommand(text);
|
|
354
|
+
if (inv && canAddPeople) {
|
|
355
|
+
let gid: string | null = null;
|
|
356
|
+
if (isGroup && selected.kind === "thread") {
|
|
357
|
+
if (groupParticipants.includes(inv.slug)) {
|
|
358
|
+
gid = selected.threadId;
|
|
359
|
+
} else {
|
|
360
|
+
gid = await addToGroup(inv.slug);
|
|
361
|
+
if (!gid) return;
|
|
362
|
+
}
|
|
363
|
+
} else if (inv.slug !== activeAgent?.slug) {
|
|
364
|
+
gid = await escalateToGroup(inv.slug);
|
|
365
|
+
if (!gid) return;
|
|
366
|
+
}
|
|
367
|
+
if (gid) {
|
|
368
|
+
await groupSend(gid, messageForInvite(inv.slug, inv.message), media);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (selected.kind === "thread" && selected.channel === "group") {
|
|
373
|
+
await groupSend(selected.threadId, text, media);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
131
376
|
if (activeIsRoby) {
|
|
132
|
-
await sendChat(text, {
|
|
377
|
+
await sendChat(text, {
|
|
378
|
+
model: model || undefined,
|
|
379
|
+
...(media?.length ? { attachments: media } : {}),
|
|
380
|
+
});
|
|
381
|
+
// The turn just wrote itself into the channel ledger. Revalidate so the
|
|
382
|
+
// new chat shows up in the sidebar now, instead of only after a reload —
|
|
383
|
+
// which read as "the conversation was lost".
|
|
384
|
+
void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
133
385
|
return;
|
|
134
386
|
}
|
|
135
387
|
if (!activeAgent) return;
|
|
136
|
-
await sendChat(text, {
|
|
388
|
+
await sendChat(text, {
|
|
389
|
+
model: model || undefined,
|
|
390
|
+
agentSlug: activeAgent.slug,
|
|
391
|
+
...(media?.length ? { attachments: media } : {}),
|
|
392
|
+
});
|
|
393
|
+
void mutate(`/api/projects/${pid}/agents/${activeAgent.slug}/conversations`);
|
|
137
394
|
};
|
|
138
395
|
|
|
139
396
|
const copyToClipboard = async (text: string) => {
|
|
@@ -149,6 +406,77 @@ export function ChatTab({
|
|
|
149
406
|
clear();
|
|
150
407
|
};
|
|
151
408
|
|
|
409
|
+
// "New group" from the sidebar: create the room, then open it as a group
|
|
410
|
+
// thread. It shows in the Groups section (and the inbox) from here on.
|
|
411
|
+
// `showTools` seeds the header switch for that room (pelado by default).
|
|
412
|
+
const onNewGroup = async (agentSlugs: string[], opts?: { showTools?: boolean }) => {
|
|
413
|
+
try {
|
|
414
|
+
const g = await Groups.create(pid, { participants: agentSlugs });
|
|
415
|
+
const key = { kind: "thread" as const, channel: "group", threadId: g.id };
|
|
416
|
+
writeShowTools(`${SHOW_TOOLS_PREF}.${pid}.${chatKeyToString(key)}`, opts?.showTools === true);
|
|
417
|
+
void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
418
|
+
selectChat(key, { channel: "group", title: g.title });
|
|
419
|
+
} catch (e) {
|
|
420
|
+
toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
421
|
+
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
// Add a member to the CURRENT group room.
|
|
425
|
+
const addToGroup = async (slug: string): Promise<string | null> => {
|
|
426
|
+
if (selected.kind !== "thread") return null;
|
|
427
|
+
const gid = selected.threadId;
|
|
428
|
+
try {
|
|
429
|
+
await Groups.addParticipant(pid, gid, slug);
|
|
430
|
+
// The silent re-read brings the new roster back with the messages.
|
|
431
|
+
await loadThread("group", gid, { silent: true });
|
|
432
|
+
void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
433
|
+
return gid;
|
|
434
|
+
} catch (e) {
|
|
435
|
+
toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
// Remove a member from the current group (records a "… salió del chat" notice;
|
|
441
|
+
// agents stop citing them). Keep at least one agent in the room.
|
|
442
|
+
const removeFromGroup = async (slug: string) => {
|
|
443
|
+
if (selected.kind !== "thread") return;
|
|
444
|
+
const gid = selected.threadId;
|
|
445
|
+
try {
|
|
446
|
+
await Groups.removeParticipant(pid, gid, slug);
|
|
447
|
+
await loadThread("group", gid, { silent: true });
|
|
448
|
+
void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Escalate a 1:1 with a project agent into a group by pulling someone in.
|
|
455
|
+
const escalateToGroup = async (slug: string): Promise<string | null> => {
|
|
456
|
+
const base = activeAgent?.slug;
|
|
457
|
+
if (!base) return null;
|
|
458
|
+
try {
|
|
459
|
+
const g = await Groups.create(pid, { participants: [base, slug] });
|
|
460
|
+
void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
461
|
+
selectChat({ kind: "thread", channel: "group", threadId: g.id }, { channel: "group", title: g.title });
|
|
462
|
+
return g.id;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
465
|
+
return null;
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
// Who "add someone" offers: in a group, every agent not already in it; in a
|
|
470
|
+
// 1:1 with a project agent, every other project agent (adding one makes it a
|
|
471
|
+
// group). Not offered for the super-agent or a2a threads.
|
|
472
|
+
// activeAgent is only set for a 1:1 with a project agent (undefined for threads
|
|
473
|
+
// and the super-agent), so it alone marks the escalate-able case.
|
|
474
|
+
const canAddPeople = isGroup || (!activeIsRoby && !!activeAgent);
|
|
475
|
+
const addCandidates = isGroup
|
|
476
|
+
? agentList.filter((a) => !groupParticipants.includes(a.slug))
|
|
477
|
+
: agentList.filter((a) => a.slug !== activeAgent?.slug);
|
|
478
|
+
const onPickAdd = (slug: string) => (isGroup ? addToGroup(slug) : escalateToGroup(slug));
|
|
479
|
+
|
|
152
480
|
// "New session" header button: reset the pane but stay with the current
|
|
153
481
|
// agent (Roby for channel threads / the super-agent, else the project agent).
|
|
154
482
|
const newSession = () => {
|
|
@@ -172,7 +500,7 @@ export function ChatTab({
|
|
|
172
500
|
}
|
|
173
501
|
toast.success(t("project.chat.deleted"));
|
|
174
502
|
setConfirmDelete(false);
|
|
175
|
-
|
|
503
|
+
await openNeighbour();
|
|
176
504
|
} catch (e) {
|
|
177
505
|
toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
178
506
|
} finally {
|
|
@@ -180,105 +508,723 @@ export function ChatTab({
|
|
|
180
508
|
}
|
|
181
509
|
};
|
|
182
510
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
const
|
|
511
|
+
/** Both lists that could be showing this session. Cheaper to revalidate the
|
|
512
|
+
* pair than to work out which frame we are in. */
|
|
513
|
+
const refreshSessions = () => {
|
|
514
|
+
if (selected.kind === "thread") void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
515
|
+
else if (selected.kind === "conv") void mutate(`/api/projects/${pid}/agents/${selected.agentSlug}/conversations`);
|
|
516
|
+
void mutate(`sessions:${pid}:${activeAgent?.slug || ""}:${activeIsRoby}`);
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
/** Name it something you will recognise. An empty name is not an error — it
|
|
520
|
+
* drops the override, and the derived one (the first thing said) comes back. */
|
|
521
|
+
const doRename = async (title: string) => {
|
|
522
|
+
try {
|
|
523
|
+
if (selected.kind === "conv") {
|
|
524
|
+
await Conversations.update(pid, selected.agentSlug, selected.convId, { title });
|
|
525
|
+
} else if (selected.kind === "thread") {
|
|
526
|
+
await Conversations.updateThread(pid, selected.channel, selected.threadId, { title });
|
|
527
|
+
}
|
|
528
|
+
setSelectedMeta((m) => ({ ...(m || {}), title: title.trim() || undefined }) as ChatSelectionMeta);
|
|
529
|
+
refreshSessions();
|
|
530
|
+
setRenaming(null);
|
|
531
|
+
toast.success(t("project.chat.renamed"));
|
|
532
|
+
} catch (e) {
|
|
533
|
+
toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
|
|
537
|
+
/** Put away, or take back out. The smaller decision than deleting: the record
|
|
538
|
+
* stays exactly where it is and only leaves the lists that offer chats to
|
|
539
|
+
* resume, so archiving is never the end of anything. */
|
|
540
|
+
const doArchive = async (archived: boolean) => {
|
|
541
|
+
try {
|
|
542
|
+
if (selected.kind === "conv") {
|
|
543
|
+
await Conversations.update(pid, selected.agentSlug, selected.convId, { archived });
|
|
544
|
+
} else if (selected.kind === "thread") {
|
|
545
|
+
await Conversations.updateThread(pid, selected.channel, selected.threadId, { archived });
|
|
546
|
+
}
|
|
547
|
+
refreshSessions();
|
|
548
|
+
toast.success(t(archived ? "project.chat.archived_ok" : "project.chat.unarchived_ok"));
|
|
549
|
+
// Archiving takes this thread out of the list you are reading it from, so
|
|
550
|
+
// stay somewhere that still exists. Un-archiving leaves you put.
|
|
551
|
+
if (archived) await openNeighbour();
|
|
552
|
+
} catch (e) {
|
|
553
|
+
toast.error((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
554
|
+
}
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/** Where to land once a conversation is gone: the most recent one still
|
|
558
|
+
* there, for the same agent or the same channel wall. Dropping the reader on
|
|
559
|
+
* an empty new session instead is the app answering "deleted" with "and now
|
|
560
|
+
* you are nowhere" — the thread next to it is what they were reading around.
|
|
561
|
+
* An empty list, or a list we cannot fetch, still falls back to a fresh one. */
|
|
562
|
+
const openNeighbour = async () => {
|
|
563
|
+
try {
|
|
564
|
+
if (selected.kind === "conv") {
|
|
565
|
+
const gone = selected.convId;
|
|
566
|
+
const slug = selected.agentSlug;
|
|
567
|
+
const rest = (await Conversations.list(pid, slug)).filter((c) => c.id !== gone);
|
|
568
|
+
const next = rest.sort(byRecency)[0];
|
|
569
|
+
if (next) return selectChat({ kind: "conv", agentSlug: slug, convId: next.id });
|
|
570
|
+
} else if (selected.kind === "thread") {
|
|
571
|
+
const { channel, threadId } = selected;
|
|
572
|
+
const rest = (await Conversations.threads(pid)).filter(
|
|
573
|
+
(th) => !(th.channel === channel && th.id === threadId),
|
|
574
|
+
);
|
|
575
|
+
const next = [...rest].sort((a, b) => (b.last_ts || b.id).localeCompare(a.last_ts || a.id))[0];
|
|
576
|
+
if (next) return selectChat({ kind: "thread", channel: next.channel, threadId: next.id });
|
|
577
|
+
}
|
|
578
|
+
} catch {
|
|
579
|
+
/* the list is unreachable: a fresh session is still a place to be */
|
|
580
|
+
}
|
|
581
|
+
newSession();
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
// The header answers WHO first and WHICH CONVERSATION second: the agent's
|
|
585
|
+
// face and name on the title line, the thread's own identity (title, date,
|
|
586
|
+
// channel) demoted to the meta line under it. It used to lead with the
|
|
587
|
+
// conversation id — a bare date string — while the agent was a chip off to
|
|
588
|
+
// the right, so a thread looked like it belonged to nobody.
|
|
589
|
+
// An a2a thread is a conversation BETWEEN two agents — not the super-agent's,
|
|
590
|
+
// even though (like every stored thread) selected.kind is "thread". It gets
|
|
591
|
+
// both faces and their "A · B" title, and none of the super-agent chrome.
|
|
592
|
+
// Faces for the multi-agent header — read, not derived. They arrive resolved
|
|
593
|
+
// on the thread, which is what makes this identical in every frame around the
|
|
594
|
+
// chat: opened from the inbox, from the phone, or from the project sidebar,
|
|
595
|
+
// an a2a thread wears the same two avatars and the same "Andy · Claude". The
|
|
596
|
+
// panel used to resolve them here against `agentList`, which knows neither the
|
|
597
|
+
// super-agent nor a coding CLI, so the surface that had no inbox row to copy
|
|
598
|
+
// from showed a faceless header titled with the raw pair id.
|
|
599
|
+
const a2aFaces = useMemo<AgentFace[]>(
|
|
600
|
+
() => (isMultiThread ? conversationMeta?.faces ?? [] : []),
|
|
601
|
+
[isMultiThread, conversationMeta],
|
|
602
|
+
);
|
|
603
|
+
// What a multi-agent thread is CALLED — "Andy · Claude", not `andy~claude-code`.
|
|
604
|
+
// The loaded thread first (it is the only source on a deep link), then the row
|
|
605
|
+
// that opened it, so clicking one in the sidebar does not flash the pair id for
|
|
606
|
+
// the length of a fetch. Both already carry the daemon's resolved name.
|
|
607
|
+
const multiThreadLabel = isMultiThread
|
|
608
|
+
? conversationMeta?.title ||
|
|
609
|
+
selectedMeta?.title ||
|
|
610
|
+
(selected.kind === "thread" ? selected.threadId : "")
|
|
611
|
+
: "";
|
|
612
|
+
const agentLabel = isMultiThread
|
|
613
|
+
? multiThreadLabel
|
|
614
|
+
: activeIsRoby ? persona : activeAgent?.name || activeAgent?.slug || selected.agentSlug;
|
|
186
615
|
const channelLabel =
|
|
187
616
|
selected.kind === "thread" ? selected.channel : selectedMeta?.channel || "web";
|
|
188
617
|
const createdIso =
|
|
189
618
|
selected.kind === "thread" ? selected.threadId : selectedMeta?.createdAt;
|
|
190
619
|
|
|
191
|
-
|
|
620
|
+
// What this session is CALLED. The loaded file (or thread) knows its own
|
|
621
|
+
// name, including the one the reader gave it; the list row is only what
|
|
622
|
+
// happened to be carried in from wherever you clicked, and on a deep link
|
|
623
|
+
// there is no row at all — which is how the header ended up showing a bare
|
|
624
|
+
// date where the name should be.
|
|
625
|
+
const convLabel =
|
|
192
626
|
selected.kind === "live"
|
|
193
|
-
?
|
|
194
|
-
:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
627
|
+
? ""
|
|
628
|
+
: isMultiThread
|
|
629
|
+
? multiThreadLabel
|
|
630
|
+
: conversationMeta?.title ||
|
|
631
|
+
selectedMeta?.title ||
|
|
632
|
+
(selected.kind === "thread" ? selected.threadId : selected.convId);
|
|
633
|
+
// Kept as the delete-dialog subject: there the conversation, not the agent,
|
|
634
|
+
// is the thing being destroyed.
|
|
635
|
+
const headerTitle = convLabel || t("project.chat.live_title", { agent: agentLabel });
|
|
636
|
+
// The loaded file knows its own channel and engine; the selection metadata is
|
|
637
|
+
// only what the list row happened to carry. Prefer the file — a routine
|
|
638
|
+
// conversation was reading as "new chat · web" with no model named anywhere.
|
|
639
|
+
const shownChannel = conversationMeta?.channel || channelLabel;
|
|
640
|
+
// One face per speaker, resolved from the same data the inbox uses. Turns a
|
|
641
|
+
// delegated agent produced inside a super-agent thread keep THEIR face, so a
|
|
642
|
+
// multi-agent thread is readable without expanding anything.
|
|
643
|
+
const headerFace: AgentFace = activeIsRoby
|
|
644
|
+
? { icon: superAgentIcon, name: persona }
|
|
645
|
+
: { icon: activeAgent?.icon, emoji: activeAgent?.emoji, name: agentLabel };
|
|
646
|
+
|
|
647
|
+
const faceFor = (msg: ChatMsg): AgentFace => {
|
|
648
|
+
const id = msg.agentId || msg.agent;
|
|
649
|
+
if (!id) return headerFace;
|
|
650
|
+
if (id === "super_agent") return { icon: superAgentIcon, name: msg.agent || persona };
|
|
651
|
+
const hit = agentList.find((a) => a.slug === id || a.name === id);
|
|
652
|
+
if (hit) return { icon: hit.icon, emoji: hit.emoji, name: hit.name || hit.slug };
|
|
653
|
+
const participant = a2aFaces.find((face) => face.slug === id || face.name === id);
|
|
654
|
+
if (participant) return participant;
|
|
655
|
+
if (["roby", "apx", "default", "superagent", "super-agent", "__super_agent__"].includes(String(id).toLowerCase())) {
|
|
656
|
+
return { icon: superAgentIcon, name: msg.agent || persona };
|
|
657
|
+
}
|
|
658
|
+
return { ...headerFace, name: msg.agent || headerFace.name };
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
// A stored session is the only kind you can name, put away or destroy: a live
|
|
662
|
+
// one is not anywhere yet.
|
|
663
|
+
const storedSession = selected.kind === "conv" || selected.kind === "thread";
|
|
664
|
+
const isArchived = !!selectedMeta?.archived;
|
|
665
|
+
const newSessionAction = {
|
|
666
|
+
key: "new",
|
|
667
|
+
icon: RotateCcw,
|
|
668
|
+
label: t("project.chat.new_session"),
|
|
669
|
+
onClick: () => setConfirmNew(true),
|
|
670
|
+
disabled: streaming || msgs.length === 0,
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
// Go to the agent's card / project. Inbox passes `onOpenInProject` (leave the
|
|
674
|
+
// inbox axis). Inside a project chat we default to the agent's own ficha —
|
|
675
|
+
// tapping the face used to do nothing there because the prop was absent.
|
|
676
|
+
// `!activeIsRoby` already means this is not a thread (a2a/group/super-agent),
|
|
677
|
+
// so `selected` is a live/conv here and carries an agentSlug.
|
|
678
|
+
const agentViewSlug = !activeIsRoby && !isMultiThread
|
|
679
|
+
? (activeAgent?.slug || (selected as { agentSlug?: string }).agentSlug)
|
|
680
|
+
: undefined;
|
|
681
|
+
const openAgentView = agentViewSlug
|
|
682
|
+
? () => navigate(`/p/${pid}/agents/${encodeURIComponent(agentViewSlug)}`)
|
|
683
|
+
: undefined;
|
|
684
|
+
const openAgentAction = onOpenInProject || openAgentView;
|
|
685
|
+
const openAgentActionMeta = openAgentAction
|
|
686
|
+
? {
|
|
687
|
+
key: "open-project",
|
|
688
|
+
icon: onOpenInProject ? ArrowUpRight : Eye,
|
|
689
|
+
label: t(onOpenInProject ? "inbox.open_in_project" : "project.chat.view_agent"),
|
|
690
|
+
onClick: openAgentAction,
|
|
691
|
+
disabled: false,
|
|
692
|
+
}
|
|
693
|
+
: null;
|
|
694
|
+
|
|
695
|
+
// Resolve a group/a2a face to a project-agent slug we can open. Coding CLIs
|
|
696
|
+
// and the super-agent have no ficha here — tip still names them, click no-ops.
|
|
697
|
+
const resolveFaceSlug = (face: AgentFace): string | undefined => {
|
|
698
|
+
if (face.slug && agentList.some((a) => a.slug === face.slug)) return face.slug;
|
|
699
|
+
return agentList.find((a) => a.name === face.name || a.slug === face.name)?.slug;
|
|
700
|
+
};
|
|
701
|
+
const openFace = (face: AgentFace) => {
|
|
702
|
+
const slug = resolveFaceSlug(face);
|
|
703
|
+
if (!slug) return;
|
|
704
|
+
navigate(`/p/${pid}/agents/${encodeURIComponent(slug)}`);
|
|
705
|
+
};
|
|
706
|
+
const faceOpenable = (face: AgentFace) => !!resolveFaceSlug(face);
|
|
707
|
+
|
|
708
|
+
// What the ⋯ holds: everything that edits THIS session. Described once, so
|
|
709
|
+
// the phone and the desktop cannot drift into offering different things.
|
|
710
|
+
const menuActions = [
|
|
711
|
+
// Face + ⋯ both offer the same escape hatch (inbox → project, or chat →
|
|
712
|
+
// agent ficha). Always in the menu so it is not only a discoverable click
|
|
713
|
+
// on the blob.
|
|
714
|
+
...(openAgentActionMeta ? [openAgentActionMeta] : []),
|
|
715
|
+
...(compact ? [newSessionAction] : []),
|
|
716
|
+
...(storedSession
|
|
717
|
+
? [
|
|
718
|
+
{
|
|
719
|
+
key: "rename",
|
|
720
|
+
icon: Pencil,
|
|
721
|
+
label: t("project.chat.rename"),
|
|
722
|
+
onClick: () => setRenaming(convLabel || ""),
|
|
723
|
+
disabled: false,
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
key: "archive",
|
|
727
|
+
icon: isArchived ? ArchiveRestore : Archive,
|
|
728
|
+
label: t(isArchived ? "project.chat.unarchive" : "project.chat.archive"),
|
|
729
|
+
onClick: () => void doArchive(!isArchived),
|
|
730
|
+
disabled: streaming,
|
|
731
|
+
},
|
|
732
|
+
]
|
|
733
|
+
: []),
|
|
734
|
+
];
|
|
735
|
+
// Below the line, on its own: the one that cannot be taken back.
|
|
736
|
+
const deleteAction = storedSession
|
|
737
|
+
? { key: "delete", icon: Trash2, label: t("project.chat.delete"), onClick: () => setConfirmDelete(true), disabled: streaming }
|
|
738
|
+
: null;
|
|
739
|
+
|
|
740
|
+
// Regenerate / edit-and-resend are wired only for a project agent's own chat —
|
|
741
|
+
// the case whose history the daemon rebuilds from a file we can rewind. The
|
|
742
|
+
// super-agent's channel threads and a2a share a day-ledger, not a per-chat
|
|
743
|
+
// file, so they're left out for now.
|
|
744
|
+
const canRewind = !activeIsRoby && !!activeAgent && !isMultiThread && !streaming;
|
|
745
|
+
const afterRewind = () =>
|
|
746
|
+
void mutate(`/api/projects/${pid}/agents/${activeAgent?.slug}/conversations`);
|
|
747
|
+
const onRegenerate = canRewind
|
|
748
|
+
? (index: number) => { void regenerate(index, { model: model || undefined, agentSlug: activeAgent!.slug }); afterRewind(); }
|
|
749
|
+
: undefined;
|
|
750
|
+
const onEditResend = canRewind
|
|
751
|
+
? (index: number, text: string) => { void editAndResend(index, text, { model: model || undefined, agentSlug: activeAgent!.slug }); afterRewind(); }
|
|
752
|
+
: undefined;
|
|
753
|
+
|
|
754
|
+
// ── Group rewind (regenerate / edit & resend) ─────────────────────────────
|
|
755
|
+
// A group is a ledger thread, so rewinding truncates the ledger (not a file)
|
|
756
|
+
// and then resumes the cascade from the target speaker. Regenerating the last
|
|
757
|
+
// bubble keeps earlier replies this turn; regenerating an earlier one drops
|
|
758
|
+
// everything after it (those speakers may be pulled back in by a new @mention).
|
|
759
|
+
//
|
|
760
|
+
// keepVisible = pane bubbles before the target. truncateGroupThread collapses
|
|
761
|
+
// the ledger the same way (tools ride with their agent; consecutive same-speaker
|
|
762
|
+
// agent rows = one bubble). A raw row count used to cut too early and delete
|
|
763
|
+
// the owner's message when Candela had spoken twice in a row.
|
|
764
|
+
const gidOf = () => (selected.kind === "thread" ? selected.threadId : "");
|
|
765
|
+
const groupKeepVisible = (index: number) =>
|
|
766
|
+
msgs.slice(0, index).filter((m) => m.role === "user" || (m.role === "assistant" && !m.event)).length;
|
|
767
|
+
const runGroupEdit = async (keepVisible: number, text: string, media?: UploadedMedia[]) => {
|
|
768
|
+
const gid = gidOf();
|
|
769
|
+
try { await Groups.truncate(pid, gid, keepVisible); } catch (e) { toast.error((e as Error)?.message); return; }
|
|
770
|
+
await loadThread("group", gid, { silent: true });
|
|
771
|
+
await groupSend(gid, text, media);
|
|
772
|
+
};
|
|
773
|
+
const runGroupRegen = async (keepVisible: number, from?: string, reason?: string | null) => {
|
|
774
|
+
const gid = gidOf();
|
|
775
|
+
if (streaming) return;
|
|
776
|
+
try { await Groups.truncate(pid, gid, keepVisible); } catch (e) { toast.error((e as Error)?.message); return; }
|
|
777
|
+
await loadThread("group", gid, { silent: true });
|
|
778
|
+
await sendGroup(gid, "", nameOfSlug, { rerun: true, from, reason });
|
|
779
|
+
void mutate(`/api/projects/${pid}/super-agent/threads`);
|
|
780
|
+
};
|
|
781
|
+
const groupRegenerate = (index: number) => {
|
|
782
|
+
const target = msgs[index];
|
|
783
|
+
if (target?.role !== "assistant" || target.event || !target.agentId) return;
|
|
784
|
+
// Keep owner/agent turns BEFORE this bubble; drop it and anything after.
|
|
785
|
+
const keepVisible = groupKeepVisible(index);
|
|
786
|
+
const from = target.agentId;
|
|
787
|
+
const reason = target.reason || null;
|
|
788
|
+
if (index < msgs.length - 1) setGroupRewind({ kind: "regen", keepVisible, drop: msgs.length - 1 - index, from, reason });
|
|
789
|
+
else void runGroupRegen(keepVisible, from, reason);
|
|
790
|
+
};
|
|
791
|
+
const groupEdit = (index: number, text: string) => {
|
|
792
|
+
const target = msgs[index];
|
|
793
|
+
if (!target || target.role !== "user") return;
|
|
794
|
+
const keepVisible = groupKeepVisible(index); // drop the edited owner line + everything after
|
|
795
|
+
// Keep the photo/file on the edited turn — only the caption changes.
|
|
796
|
+
const media = attachmentsOf(target);
|
|
797
|
+
if (index < msgs.length - 1) setGroupRewind({ kind: "edit", keepVisible, text, media, drop: msgs.length - 1 - index });
|
|
798
|
+
else void runGroupEdit(keepVisible, text, media);
|
|
799
|
+
};
|
|
800
|
+
// In a group, the same affordances rewind the ledger instead of a file.
|
|
801
|
+
const regenerateHandler = isGroup ? (streaming ? undefined : groupRegenerate) : onRegenerate;
|
|
802
|
+
const editHandler = isGroup ? (streaming ? undefined : groupEdit) : onEditResend;
|
|
199
803
|
|
|
200
804
|
if (agents.isLoading) return <Loading />;
|
|
201
805
|
|
|
202
806
|
return (
|
|
203
|
-
<div className="flex h-full overflow-hidden rounded-xl border border-border bg-card/40">
|
|
807
|
+
<div className={cn("flex h-full min-h-0 overflow-hidden", !bare && "rounded-xl border border-border bg-card/40")}>
|
|
204
808
|
{hideSidebar ? null : <ChatList
|
|
205
809
|
pid={pid}
|
|
206
810
|
agents={agentList}
|
|
207
811
|
superAgentSlug={ROBY_SLUG}
|
|
208
812
|
superAgentLabel={t("agents_ui.super_agent_label", { persona })}
|
|
813
|
+
superAgentIcon={superAgentIcon}
|
|
209
814
|
selected={selected}
|
|
210
815
|
onSelect={selectChat}
|
|
211
816
|
onNewChat={onNewChat}
|
|
817
|
+
onNewGroup={onNewGroup}
|
|
818
|
+
autoSelectLatest={autoSelectLatest}
|
|
212
819
|
/>}
|
|
213
820
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
821
|
+
{/* min-h-0 is what makes the message list the only scroller: without it a
|
|
822
|
+
flex child refuses to shrink below its content, the column grows past
|
|
823
|
+
the viewport, and the composer sits under the fold with no way to
|
|
824
|
+
reach it. Invisible on a tall desktop pane, fatal on a phone. */}
|
|
825
|
+
<section className="flex min-h-0 min-w-0 flex-1 flex-col">
|
|
826
|
+
{hideHeader ? null : <header
|
|
827
|
+
className={cn(
|
|
828
|
+
"flex shrink-0 items-center justify-between gap-3 border-b border-border",
|
|
829
|
+
// The phone pays for the notch here rather than in a second header
|
|
830
|
+
// of its own: one implementation, two frames around it.
|
|
831
|
+
//
|
|
832
|
+
// Except inside the Android app, which already lays the WebView out
|
|
833
|
+
// below the system bars — there `env(safe-area-inset-top)` is paid
|
|
834
|
+
// a SECOND time and the chat opens with a band of empty space the
|
|
835
|
+
// list it came from does not have. Same condition the phone list
|
|
836
|
+
// uses for its own header.
|
|
837
|
+
compact
|
|
838
|
+
? cn("gap-2 px-2 pb-2", isNativeShell() ? "pt-1.5" : "pt-[max(0.5rem,env(safe-area-inset-top))]")
|
|
839
|
+
: "px-3 py-2",
|
|
840
|
+
)}
|
|
841
|
+
>
|
|
842
|
+
{onBack && (
|
|
843
|
+
<button
|
|
844
|
+
type="button"
|
|
845
|
+
onClick={onBack}
|
|
846
|
+
aria-label={t("mobile.back")}
|
|
847
|
+
className="flex size-10 shrink-0 items-center justify-center rounded-full text-muted-fg active:bg-accent/60"
|
|
848
|
+
>
|
|
849
|
+
<ChevronLeft size={22} />
|
|
850
|
+
</button>
|
|
851
|
+
)}
|
|
852
|
+
<div className="flex min-w-0 flex-1 items-center gap-2.5">
|
|
853
|
+
{(() => {
|
|
854
|
+
// Group / a2a: each face is its own control (tip + open that agent).
|
|
855
|
+
// Wrapping the whole cluster in one button made every blob do the
|
|
856
|
+
// same thing — or nothing, when openAgentAction was absent.
|
|
857
|
+
if (isMultiThread && a2aFaces.length) {
|
|
858
|
+
return (
|
|
859
|
+
<AgentAvatarGroup
|
|
860
|
+
faces={a2aFaces}
|
|
861
|
+
size={compact ? 30 : 26}
|
|
862
|
+
max={3}
|
|
863
|
+
onFaceClick={openFace}
|
|
864
|
+
faceOpenable={faceOpenable}
|
|
865
|
+
/>
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
const avatar = <AgentAvatar {...headerFace} size={compact ? 36 : 30} />;
|
|
869
|
+
// Tapping the face is the quick way to the agent's card/project —
|
|
870
|
+
// more discoverable than the button in the row of actions.
|
|
871
|
+
return openAgentAction ? (
|
|
872
|
+
<button
|
|
873
|
+
type="button"
|
|
874
|
+
onClick={openAgentAction}
|
|
875
|
+
aria-label={t(onOpenInProject ? "inbox.open_in_project" : "project.chat.view_agent")}
|
|
876
|
+
className="shrink-0 rounded-full transition-opacity hover:opacity-80 active:opacity-70"
|
|
877
|
+
>
|
|
878
|
+
{avatar}
|
|
879
|
+
</button>
|
|
880
|
+
) : avatar;
|
|
881
|
+
})()}
|
|
882
|
+
{/* The session on top, WHO on the line under it. The header used to
|
|
883
|
+
lead with the agent and demote the thread to a date in the meta
|
|
884
|
+
line — but the agent's face is already sitting right there, and
|
|
885
|
+
the date was standing in the one place the name should be. */}
|
|
886
|
+
<div className="min-w-0 flex-1">
|
|
887
|
+
{/* A group is one room, not an agent with many sessions — so it
|
|
888
|
+
gets a plain title, not the session switcher. */}
|
|
889
|
+
{isGroup ? (
|
|
890
|
+
<div className={cn(
|
|
891
|
+
"max-w-full truncate font-semibold text-foreground",
|
|
892
|
+
compact ? "text-[15px] leading-tight" : "text-sm",
|
|
893
|
+
)}>
|
|
894
|
+
{convLabel || t("project.groups.title")}
|
|
895
|
+
</div>
|
|
896
|
+
) : (
|
|
897
|
+
<SessionPicker
|
|
898
|
+
pid={pid}
|
|
899
|
+
agentSlug={activeAgent?.slug || (selected.kind === "thread" ? "" : selected.agentSlug)}
|
|
900
|
+
isSuper={activeIsRoby}
|
|
901
|
+
selected={selected}
|
|
902
|
+
label={convLabel || t("mobile.live_session")}
|
|
903
|
+
onPick={selectChat}
|
|
904
|
+
channelScope={channelScope}
|
|
905
|
+
className={cn(
|
|
906
|
+
"max-w-full font-semibold text-foreground",
|
|
907
|
+
compact ? "text-[15px] leading-tight" : "text-sm",
|
|
908
|
+
)}
|
|
909
|
+
/>
|
|
910
|
+
)}
|
|
911
|
+
{/* Who answered, where, and when — three facts, each short. Kept
|
|
912
|
+
on one line and truncated as a whole, so a long agent name
|
|
913
|
+
cannot push the channel and the date onto a second row. */}
|
|
914
|
+
<p className="flex min-w-0 items-center gap-1.5 truncate text-[11px] text-muted-fg">
|
|
915
|
+
{/* For a2a the picker above already names both agents, so the
|
|
916
|
+
meta line skips the "who" and leads with the channel. */}
|
|
917
|
+
{!isMultiThread && <span className="truncate">{agentLabel}</span>}
|
|
918
|
+
{/* Not on a phone: at 375px the badge and the two facts after it
|
|
919
|
+
squeezed "Roby" down to "Ro…", and the face two inches to
|
|
920
|
+
the left already says which agent this is. */}
|
|
921
|
+
{activeIsRoby && !isMultiThread && !compact && (
|
|
922
|
+
<span className={cn("shrink-0 rounded px-1 py-px text-[9px] font-semibold uppercase tracking-wide", toneChip.emerald)}>
|
|
923
|
+
{t("agents_ui.super_agent_badge")}
|
|
924
|
+
</span>
|
|
925
|
+
)}
|
|
926
|
+
<span className="shrink-0">· {shownChannel}</span>
|
|
927
|
+
{createdIso && <span className="shrink-0">· {formatDate(createdIso)}</span>}
|
|
928
|
+
{conversationMeta?.engine && !compact && (
|
|
929
|
+
<span className="truncate">· {conversationMeta.engine}</span>
|
|
930
|
+
)}
|
|
931
|
+
</p>
|
|
932
|
+
</div>
|
|
219
933
|
</div>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
934
|
+
|
|
935
|
+
{/* Same actions on both surfaces, presented for the room available:
|
|
936
|
+
spelled out where there is width, folded behind one ⋯ where there
|
|
937
|
+
is not. On the phone they used to be absent altogether. */}
|
|
938
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
939
|
+
{/* Same switch as "create group": on = tools/ActionGroup, off = pelado
|
|
940
|
+
(narration as bubbles, tools hidden). Lives next to add-person. */}
|
|
941
|
+
<Tip content={showTools ? t("chat_ui.show_tools_on") : t("chat_ui.show_tools_off")}>
|
|
942
|
+
<div
|
|
943
|
+
className={cn(
|
|
944
|
+
"flex items-center gap-1.5 rounded-full text-muted-fg",
|
|
945
|
+
compact ? "px-1.5" : "px-1",
|
|
946
|
+
)}
|
|
947
|
+
aria-label={t("chat_ui.show_tools")}
|
|
948
|
+
>
|
|
949
|
+
{!compact && <Wrench size={13} className="shrink-0 opacity-70" aria-hidden />}
|
|
950
|
+
<Switch checked={showTools} onChange={setShowTools} />
|
|
951
|
+
</div>
|
|
952
|
+
</Tip>
|
|
953
|
+
{!agentList.length && !activeIsRoby && !compact && (
|
|
227
954
|
<Button variant="primary" size="sm" onClick={() => setCreating(true)}>
|
|
228
955
|
<Plus size={14} /> {t("project.chat.create_agent")}
|
|
229
956
|
</Button>
|
|
230
957
|
)}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
958
|
+
{/* Add someone: turns a 1:1 into a group, or grows an existing one.
|
|
959
|
+
Same control, two meanings — the menu names which. */}
|
|
960
|
+
{canAddPeople && (isGroup || addCandidates.length > 0) && (
|
|
961
|
+
<div className="relative">
|
|
962
|
+
<Tip content={isGroup ? t("project.groups.members_label") : t("project.groups.make_group")}>
|
|
963
|
+
<button
|
|
964
|
+
type="button"
|
|
965
|
+
aria-label={isGroup ? t("project.groups.members_label") : t("project.groups.make_group")}
|
|
966
|
+
onClick={() => setAddOpen((o) => !o)}
|
|
967
|
+
className={cn(
|
|
968
|
+
"flex items-center justify-center rounded-full text-muted-fg",
|
|
969
|
+
compact ? "size-10 active:bg-accent/60" : "size-8 hover:bg-accent/60",
|
|
970
|
+
addOpen && "bg-accent/60",
|
|
971
|
+
)}
|
|
972
|
+
>
|
|
973
|
+
<UserPlus size={compact ? 20 : 16} />
|
|
974
|
+
</button>
|
|
975
|
+
</Tip>
|
|
976
|
+
{addOpen && (
|
|
977
|
+
<>
|
|
978
|
+
<button type="button" aria-hidden tabIndex={-1}
|
|
979
|
+
className="fixed inset-0 z-10 cursor-default" onClick={() => setAddOpen(false)} />
|
|
980
|
+
<div className="absolute right-0 top-full z-20 mt-1 w-60 rounded-md border border-border bg-card p-1 shadow-lg">
|
|
981
|
+
{/* In a group: the current roster (each removable) then who
|
|
982
|
+
to add. In a 1:1: only who to pull in to make a group. */}
|
|
983
|
+
{isGroup && groupParticipants.length > 0 && (
|
|
984
|
+
<>
|
|
985
|
+
<p className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-fg">
|
|
986
|
+
{t("project.groups.members_label")}
|
|
987
|
+
</p>
|
|
988
|
+
<div className="max-h-40 overflow-y-auto">
|
|
989
|
+
{groupParticipants.map((slug) => {
|
|
990
|
+
const a = agentList.find((x) => x.slug === slug);
|
|
991
|
+
return (
|
|
992
|
+
<div key={slug} className="group/mem flex items-center gap-2 rounded px-2 py-1.5 text-xs hover:bg-accent/50">
|
|
993
|
+
<AgentAvatar icon={a?.icon} emoji={a?.emoji} name={a?.name || slug} size={18} />
|
|
994
|
+
<span className="min-w-0 flex-1 truncate">{a?.name || slug}</span>
|
|
995
|
+
{groupParticipants.length > 1 && (
|
|
996
|
+
<button
|
|
997
|
+
type="button"
|
|
998
|
+
aria-label={t("project.groups.remove_member")}
|
|
999
|
+
title={t("project.groups.remove_member")}
|
|
1000
|
+
onClick={() => { setAddOpen(false); void removeFromGroup(slug); }}
|
|
1001
|
+
className="shrink-0 rounded p-0.5 text-muted-fg opacity-0 hover:text-destructive group-hover/mem:opacity-100"
|
|
1002
|
+
>
|
|
1003
|
+
<X size={13} />
|
|
1004
|
+
</button>
|
|
1005
|
+
)}
|
|
1006
|
+
</div>
|
|
1007
|
+
);
|
|
1008
|
+
})}
|
|
1009
|
+
</div>
|
|
1010
|
+
<div className="my-1 border-t border-border" />
|
|
1011
|
+
</>
|
|
1012
|
+
)}
|
|
1013
|
+
<p className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-muted-fg">
|
|
1014
|
+
{isGroup ? t("project.groups.add_member") : t("project.groups.make_group")}
|
|
1015
|
+
</p>
|
|
1016
|
+
<div className="max-h-40 overflow-y-auto">
|
|
1017
|
+
{addCandidates.map((a) => (
|
|
1018
|
+
<button
|
|
1019
|
+
key={a.slug}
|
|
1020
|
+
type="button"
|
|
1021
|
+
onClick={() => { setAddOpen(false); void onPickAdd(a.slug); }}
|
|
1022
|
+
className="flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-xs hover:bg-accent/50"
|
|
1023
|
+
>
|
|
1024
|
+
<AgentAvatar icon={a.icon} emoji={a.emoji} name={a.name || a.slug} size={18} />
|
|
1025
|
+
<span className="truncate">{a.name || a.slug}</span>
|
|
1026
|
+
</button>
|
|
1027
|
+
))}
|
|
1028
|
+
{isGroup && addCandidates.length === 0 && (
|
|
1029
|
+
<p className="px-2 py-1.5 text-[11px] text-muted-fg">{t("project.groups.all_in")}</p>
|
|
1030
|
+
)}
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
</>
|
|
1034
|
+
)}
|
|
1035
|
+
</div>
|
|
1036
|
+
)}
|
|
1037
|
+
{/* Inbox only: leave the inbox axis in words. Inside a project the
|
|
1038
|
+
face + ⋯ already open the agent ficha — a second labeled button
|
|
1039
|
+
just repeats "you are already here". */}
|
|
1040
|
+
{onOpenInProject && !compact && (
|
|
1041
|
+
<Button variant="ghost" size="sm" onClick={onOpenInProject}>
|
|
1042
|
+
{t("inbox.open_in_project")} <ArrowUpRight size={13} />
|
|
247
1043
|
</Button>
|
|
248
1044
|
)}
|
|
1045
|
+
{!compact && (
|
|
1046
|
+
<Tip content={newSessionAction.label}>
|
|
1047
|
+
<Button
|
|
1048
|
+
variant="ghost"
|
|
1049
|
+
size="sm"
|
|
1050
|
+
aria-label={newSessionAction.label}
|
|
1051
|
+
disabled={newSessionAction.disabled}
|
|
1052
|
+
onClick={newSessionAction.onClick}
|
|
1053
|
+
>
|
|
1054
|
+
<RotateCcw size={13} />
|
|
1055
|
+
</Button>
|
|
1056
|
+
</Tip>
|
|
1057
|
+
)}
|
|
1058
|
+
{(menuActions.length > 0 || deleteAction) && (
|
|
1059
|
+
<DropdownMenu>
|
|
1060
|
+
<DropdownMenuTrigger
|
|
1061
|
+
aria-label={t("common.more")}
|
|
1062
|
+
className={cn(
|
|
1063
|
+
"flex items-center justify-center rounded-full text-muted-fg data-[popup-open]:bg-accent/60",
|
|
1064
|
+
compact ? "size-10 active:bg-accent/60" : "size-8 hover:bg-accent/60",
|
|
1065
|
+
)}
|
|
1066
|
+
>
|
|
1067
|
+
<MoreVertical size={compact ? 20 : 16} />
|
|
1068
|
+
</DropdownMenuTrigger>
|
|
1069
|
+
<DropdownMenuContent align="end" sideOffset={6} className="w-60">
|
|
1070
|
+
{/* Which session these act on, named at the top — the same
|
|
1071
|
+
thing the project's right-click menu does. Without it the
|
|
1072
|
+
menu is four verbs with no subject. */}
|
|
1073
|
+
<DropdownMenuGroup>
|
|
1074
|
+
<DropdownMenuLabel className="truncate text-[11px] font-normal text-muted-fg">
|
|
1075
|
+
{convLabel || t("mobile.live_session")}
|
|
1076
|
+
</DropdownMenuLabel>
|
|
1077
|
+
</DropdownMenuGroup>
|
|
1078
|
+
{menuActions.map((a) => (
|
|
1079
|
+
<DropdownMenuItem key={a.key} onClick={a.onClick} disabled={a.disabled}>
|
|
1080
|
+
<a.icon className="size-4" /> {a.label}
|
|
1081
|
+
</DropdownMenuItem>
|
|
1082
|
+
))}
|
|
1083
|
+
{deleteAction && (
|
|
1084
|
+
<>
|
|
1085
|
+
<DropdownMenuSeparator />
|
|
1086
|
+
<DropdownMenuItem
|
|
1087
|
+
onClick={deleteAction.onClick}
|
|
1088
|
+
disabled={deleteAction.disabled}
|
|
1089
|
+
className="text-destructive"
|
|
1090
|
+
>
|
|
1091
|
+
<Trash2 className="size-4 text-destructive" /> {deleteAction.label}
|
|
1092
|
+
</DropdownMenuItem>
|
|
1093
|
+
</>
|
|
1094
|
+
)}
|
|
1095
|
+
</DropdownMenuContent>
|
|
1096
|
+
</DropdownMenu>
|
|
1097
|
+
)}
|
|
249
1098
|
</div>
|
|
250
|
-
</header>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
1099
|
+
</header>}
|
|
1100
|
+
|
|
1101
|
+
{/* The thread and the dock share the same box: the composer HOVERS over
|
|
1102
|
+
the conversation instead of taking a slice of the column away from
|
|
1103
|
+
it. The list is given the dock's exact height as trailing space, so
|
|
1104
|
+
the last line can always be scrolled clear of the field — floating
|
|
1105
|
+
over the text is only an improvement while you can still read the
|
|
1106
|
+
line you are answering. */}
|
|
1107
|
+
<div className="relative min-h-0 flex-1">
|
|
1108
|
+
<div ref={scrollerRef} className="h-full overflow-y-auto overflow-x-hidden">
|
|
1109
|
+
{msgs.length || queued.length ? (
|
|
1110
|
+
<MessageList
|
|
1111
|
+
msgs={msgs}
|
|
1112
|
+
queued={queued}
|
|
1113
|
+
onUnqueue={unqueue}
|
|
1114
|
+
onCopy={copyToClipboard}
|
|
1115
|
+
onRegenerate={regenerateHandler}
|
|
1116
|
+
onEdit={editHandler}
|
|
1117
|
+
faceFor={faceFor}
|
|
1118
|
+
showSpeaker={isGroup}
|
|
1119
|
+
nameOf={(slug) => agentList.find((a) => a.slug === slug)?.name || slug}
|
|
1120
|
+
showTools={showTools}
|
|
1121
|
+
compact={compact}
|
|
1122
|
+
bottomInset={bottomInset}
|
|
1123
|
+
onAtBottomChange={setAtBottom}
|
|
1124
|
+
/>
|
|
1125
|
+
) : (
|
|
1126
|
+
<Empty fill icon={MessageSquareDashed}>{t("project.chat.empty")}</Empty>
|
|
1127
|
+
)}
|
|
1128
|
+
</div>
|
|
1129
|
+
|
|
1130
|
+
{/* The way back to the end, once leaving it is something you can do
|
|
1131
|
+
without being dragged straight back. Positioned against the same
|
|
1132
|
+
box as the dock and sized by it, so it clears the field without
|
|
1133
|
+
being part of it — inside the dock it would grow the space the
|
|
1134
|
+
thread reserves, and the conversation would jump every time this
|
|
1135
|
+
appeared. */}
|
|
1136
|
+
{!atBottom && msgs.length > 0 && (
|
|
1137
|
+
<button
|
|
1138
|
+
type="button"
|
|
1139
|
+
onClick={jumpToLatest}
|
|
1140
|
+
aria-label={missed ? t("chat_ui.jump_latest_new") : t("chat_ui.jump_latest")}
|
|
1141
|
+
// Against the dock's REAL height, not the frozen inset: it has to
|
|
1142
|
+
// clear whatever is actually on screen, panel included.
|
|
1143
|
+
style={{ bottom: dockH + 8 }}
|
|
1144
|
+
className={cn(
|
|
1145
|
+
"absolute left-1/2 z-10 flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-border",
|
|
1146
|
+
"bg-card px-3 py-1.5 text-xs shadow-lg transition-colors hover:bg-accent",
|
|
1147
|
+
missed > 0 && "border-primary/50 text-primary",
|
|
1148
|
+
)}
|
|
1149
|
+
>
|
|
1150
|
+
<ArrowDown size={14} />
|
|
1151
|
+
{missed > 0 && <span className="font-medium tabular-nums">{missed}</span>}
|
|
1152
|
+
</button>
|
|
259
1153
|
)}
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
1154
|
+
|
|
1155
|
+
<div ref={dockRef} className="absolute inset-x-0 bottom-0">
|
|
1156
|
+
<Composer
|
|
1157
|
+
onSend={send}
|
|
1158
|
+
onStop={stop}
|
|
1159
|
+
streaming={streaming}
|
|
1160
|
+
model={model}
|
|
1161
|
+
onModelChange={setModel}
|
|
1162
|
+
// Every chat carries files now — Roby and project agents alike.
|
|
1163
|
+
// A project agent's turn goes through /agents/:slug/chat, which
|
|
1164
|
+
// resolves the same media dir and folds a marker into the prompt,
|
|
1165
|
+
// so an engine with vision renders the image and one without is
|
|
1166
|
+
// still told a file arrived and where it lives.
|
|
1167
|
+
allowFiles
|
|
1168
|
+
floating
|
|
1169
|
+
placeholder={
|
|
1170
|
+
isGroup
|
|
1171
|
+
? t("project.groups.composer_ph")
|
|
1172
|
+
: canAddPeople
|
|
1173
|
+
? t("project.groups.invite_ph")
|
|
1174
|
+
: undefined
|
|
1175
|
+
}
|
|
1176
|
+
mentionAgents={
|
|
1177
|
+
isGroup
|
|
1178
|
+
? groupParticipants.map((slug) => {
|
|
1179
|
+
const a = agentList.find((x) => x.slug === slug);
|
|
1180
|
+
return {
|
|
1181
|
+
slug,
|
|
1182
|
+
name: a?.name || slug,
|
|
1183
|
+
icon: a?.icon,
|
|
1184
|
+
emoji: a?.emoji,
|
|
1185
|
+
};
|
|
1186
|
+
})
|
|
1187
|
+
: undefined
|
|
1188
|
+
}
|
|
1189
|
+
inviteAgents={
|
|
1190
|
+
canAddPeople
|
|
1191
|
+
? addCandidates.map((a) => ({
|
|
1192
|
+
slug: a.slug,
|
|
1193
|
+
name: a.name || a.slug,
|
|
1194
|
+
icon: a.icon,
|
|
1195
|
+
emoji: a.emoji,
|
|
1196
|
+
}))
|
|
1197
|
+
: undefined
|
|
1198
|
+
}
|
|
1199
|
+
inviteKind={isGroup ? "add" : "escalate"}
|
|
1200
|
+
onNewSession={() => setConfirmNew(true)}
|
|
1201
|
+
onToggleTools={() => setShowTools(!showTools)}
|
|
1202
|
+
// The strip and the questions are both part of the field: what the
|
|
1203
|
+
// turn cost, and what it is waiting on. The panel used to hover as
|
|
1204
|
+
// a separate card above the composer, which put the thing you have
|
|
1205
|
+
// to answer somewhere other than the thing you answer with.
|
|
1206
|
+
context={
|
|
1207
|
+
<>
|
|
1208
|
+
<ContextBar msgs={msgs} docked onOpenChange={setCtxOpen} />
|
|
1209
|
+
{(() => {
|
|
1210
|
+
const pending = !streaming ? pendingAskQuestions(msgs) : null;
|
|
1211
|
+
if (!pending || pending.turnKey === dismissedAskKey) return null;
|
|
1212
|
+
return (
|
|
1213
|
+
<InlineAskPanel
|
|
1214
|
+
docked
|
|
1215
|
+
turnKey={pending.turnKey}
|
|
1216
|
+
questions={pending.questions}
|
|
1217
|
+
onSubmit={(compiled) => void send(compiled)}
|
|
1218
|
+
onDismiss={() => setDismissedAskKey(pending.turnKey)}
|
|
1219
|
+
disabled={streaming}
|
|
1220
|
+
/>
|
|
1221
|
+
);
|
|
1222
|
+
})()}
|
|
1223
|
+
</>
|
|
1224
|
+
}
|
|
272
1225
|
/>
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<Composer
|
|
276
|
-
onSend={send}
|
|
277
|
-
onStop={stop}
|
|
278
|
-
streaming={streaming}
|
|
279
|
-
model={model}
|
|
280
|
-
onModelChange={setModel}
|
|
281
|
-
/>
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
282
1228
|
</section>
|
|
283
1229
|
|
|
284
1230
|
<CreateAgentDialog
|
|
@@ -288,6 +1234,54 @@ export function ChatTab({
|
|
|
288
1234
|
onCreated={() => { setCreating(false); agents.mutate(); }}
|
|
289
1235
|
/>
|
|
290
1236
|
|
|
1237
|
+
<Dialog
|
|
1238
|
+
open={renaming !== null}
|
|
1239
|
+
onClose={() => setRenaming(null)}
|
|
1240
|
+
title={t("project.chat.rename_title")}
|
|
1241
|
+
size="sm"
|
|
1242
|
+
footer={
|
|
1243
|
+
<>
|
|
1244
|
+
<Button variant="ghost" onClick={() => setRenaming(null)}>
|
|
1245
|
+
{t("common.cancel")}
|
|
1246
|
+
</Button>
|
|
1247
|
+
<Button variant="primary" onClick={() => void doRename(renaming || "")}>
|
|
1248
|
+
{t("common.save")}
|
|
1249
|
+
</Button>
|
|
1250
|
+
</>
|
|
1251
|
+
}
|
|
1252
|
+
>
|
|
1253
|
+
<Field label={t("project.chat.rename_label")} hint={t("project.chat.rename_hint")}>
|
|
1254
|
+
<Input
|
|
1255
|
+
autoFocus
|
|
1256
|
+
value={renaming ?? ""}
|
|
1257
|
+
onChange={(e) => setRenaming(e.target.value)}
|
|
1258
|
+
onKeyDown={(e) => {
|
|
1259
|
+
if (e.key === "Enter") void doRename(renaming || "");
|
|
1260
|
+
}}
|
|
1261
|
+
/>
|
|
1262
|
+
</Field>
|
|
1263
|
+
</Dialog>
|
|
1264
|
+
|
|
1265
|
+
<Dialog
|
|
1266
|
+
open={confirmNew}
|
|
1267
|
+
onClose={() => setConfirmNew(false)}
|
|
1268
|
+
title={t("project.chat.new_session_confirm_title")}
|
|
1269
|
+
description={t("project.chat.new_session_confirm_desc")}
|
|
1270
|
+
size="sm"
|
|
1271
|
+
footer={
|
|
1272
|
+
<>
|
|
1273
|
+
<Button variant="ghost" onClick={() => setConfirmNew(false)}>
|
|
1274
|
+
{t("common.cancel")}
|
|
1275
|
+
</Button>
|
|
1276
|
+
<Button variant="primary" onClick={() => { setConfirmNew(false); newSession(); }}>
|
|
1277
|
+
<RotateCcw size={14} /> {t("project.chat.new_session")}
|
|
1278
|
+
</Button>
|
|
1279
|
+
</>
|
|
1280
|
+
}
|
|
1281
|
+
>
|
|
1282
|
+
<p className="text-sm text-muted-fg">{headerTitle}</p>
|
|
1283
|
+
</Dialog>
|
|
1284
|
+
|
|
291
1285
|
<Dialog
|
|
292
1286
|
open={confirmDelete}
|
|
293
1287
|
onClose={() => setConfirmDelete(false)}
|
|
@@ -307,10 +1301,46 @@ export function ChatTab({
|
|
|
307
1301
|
>
|
|
308
1302
|
<p className="text-sm text-muted-fg">{headerTitle}</p>
|
|
309
1303
|
</Dialog>
|
|
1304
|
+
|
|
1305
|
+
{/* Group regenerate / edit overwrites the messages after the target. */}
|
|
1306
|
+
<Dialog
|
|
1307
|
+
open={!!groupRewind}
|
|
1308
|
+
onClose={() => setGroupRewind(null)}
|
|
1309
|
+
title={groupRewind?.kind === "edit" ? t("project.chat.group_edit_title") : t("project.chat.group_regen_title")}
|
|
1310
|
+
description={t("project.chat.group_rewind_desc", { n: groupRewind?.drop ?? 0 })}
|
|
1311
|
+
size="sm"
|
|
1312
|
+
footer={
|
|
1313
|
+
<>
|
|
1314
|
+
<Button variant="ghost" onClick={() => setGroupRewind(null)}>{t("common.cancel")}</Button>
|
|
1315
|
+
<Button
|
|
1316
|
+
variant="primary"
|
|
1317
|
+
onClick={() => {
|
|
1318
|
+
const r = groupRewind;
|
|
1319
|
+
setGroupRewind(null);
|
|
1320
|
+
if (!r) return;
|
|
1321
|
+
if (r.kind === "edit") void runGroupEdit(r.keepVisible, r.text || "", r.media);
|
|
1322
|
+
else void runGroupRegen(r.keepVisible, r.from, r.reason);
|
|
1323
|
+
}}
|
|
1324
|
+
>
|
|
1325
|
+
{groupRewind?.kind === "edit" ? t("project.chat.group_edit_confirm") : t("project.chat.group_regen_confirm")}
|
|
1326
|
+
</Button>
|
|
1327
|
+
</>
|
|
1328
|
+
}
|
|
1329
|
+
>
|
|
1330
|
+
<p className="text-sm text-muted-fg">{convLabel}</p>
|
|
1331
|
+
</Dialog>
|
|
310
1332
|
</div>
|
|
311
1333
|
);
|
|
312
1334
|
}
|
|
313
1335
|
|
|
1336
|
+
/** Most recent first. `ended_at` is when it was last written to; a conversation
|
|
1337
|
+
* that has only ever been opened has just `started_at`, and the id is the last
|
|
1338
|
+
* resort so the order is never arbitrary. */
|
|
1339
|
+
function byRecency(a: ConversationListEntry, b: ConversationListEntry): number {
|
|
1340
|
+
const when = (c: ConversationListEntry) => c.ended_at || c.started_at || c.id;
|
|
1341
|
+
return when(b).localeCompare(when(a));
|
|
1342
|
+
}
|
|
1343
|
+
|
|
314
1344
|
// Localised short date for the header "Created {date}" line. Falls back to the
|
|
315
1345
|
// raw string for anything Date can't parse.
|
|
316
1346
|
function formatDate(iso?: string): string {
|
|
@@ -320,6 +1350,42 @@ function formatDate(iso?: string): string {
|
|
|
320
1350
|
return d.toLocaleDateString();
|
|
321
1351
|
}
|
|
322
1352
|
|
|
1353
|
+
// Per-chat preference: show tool ActionGroups vs pelado transcript.
|
|
1354
|
+
// A chat you have already set keeps its own value; one you have never opened
|
|
1355
|
+
// starts from the last flip of the header switch, so you do not re-flip it in
|
|
1356
|
+
// every new conversation. Pelado the very first time. Both keys live in
|
|
1357
|
+
// localStorage, so each device remembers what you read on that device.
|
|
1358
|
+
const SHOW_TOOLS_PREF = "apx.chat.showTools";
|
|
1359
|
+
const SHOW_TOOLS_LAST = "apx.chat.showTools.last";
|
|
1360
|
+
|
|
1361
|
+
function readFlag(key: string): boolean | null {
|
|
1362
|
+
try {
|
|
1363
|
+
const raw = localStorage.getItem(key);
|
|
1364
|
+
if (raw === null) return null;
|
|
1365
|
+
return raw === "1" || raw === "true";
|
|
1366
|
+
} catch {
|
|
1367
|
+
return null;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
function readShowTools(key: string): boolean {
|
|
1372
|
+
return readFlag(key) ?? readFlag(SHOW_TOOLS_LAST) ?? false;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
function writeShowTools(key: string, v: boolean): void {
|
|
1376
|
+
try {
|
|
1377
|
+
localStorage.setItem(key, v ? "1" : "0");
|
|
1378
|
+
} catch { /* quota / private mode */ }
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
/** The header switch also moves the device default. The create-group checkbox
|
|
1382
|
+
* does not: that one is a choice about that one room. */
|
|
1383
|
+
function writeShowToolsDefault(v: boolean): void {
|
|
1384
|
+
try {
|
|
1385
|
+
localStorage.setItem(SHOW_TOOLS_LAST, v ? "1" : "0");
|
|
1386
|
+
} catch { /* quota / private mode */ }
|
|
1387
|
+
}
|
|
1388
|
+
|
|
323
1389
|
function CreateAgentDialog({
|
|
324
1390
|
open,
|
|
325
1391
|
onClose,
|
|
@@ -368,7 +1434,7 @@ function CreateAgentDialog({
|
|
|
368
1434
|
}
|
|
369
1435
|
>
|
|
370
1436
|
<div className="space-y-3">
|
|
371
|
-
<Field label="
|
|
1437
|
+
<Field label={t("project.chat.slug_label")}>
|
|
372
1438
|
<Input autoFocus value={slug} onChange={(e) => setSlug(e.target.value)} placeholder="master" />
|
|
373
1439
|
</Field>
|
|
374
1440
|
<Field label={t("project.chat.role_label")}>
|