@agentprojectcontext/apx 1.80.2 → 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 +134 -0
- 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,7 +1,17 @@
|
|
|
1
|
-
import { useCallback, useRef, useState } from "react";
|
|
2
|
-
import { SuperAgent, Agents, Conversations } from "../lib/api";
|
|
3
|
-
import type { ChatStreamEvent, ChatUsage, ConversationMessage, ToolSummary } from "../types/daemon";
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { SuperAgent, Agents, Conversations, Groups, Turns } from "../lib/api";
|
|
3
|
+
import type { ActiveTurn, AgentFace, ChatStreamEvent, ChatUsage, ConversationMessage, MessageMedia, ToolSummary, TurnFrame } from "../types/daemon";
|
|
4
|
+
import type { UploadedMedia } from "../lib/api/media";
|
|
5
|
+
import { subscribeTurns } from "../lib/live";
|
|
4
6
|
import { t } from "../i18n";
|
|
7
|
+
import { queueOnSend, onChatPrefsChange } from "../lib/chat-prefs";
|
|
8
|
+
import {
|
|
9
|
+
conversationActivityKey,
|
|
10
|
+
isChatTurnClosed,
|
|
11
|
+
liveActivityKey,
|
|
12
|
+
setChatQueued,
|
|
13
|
+
threadActivityKey,
|
|
14
|
+
} from "../lib/chat-activity";
|
|
5
15
|
|
|
6
16
|
export type ToolStatus = "running" | "done" | "error" | "deduped";
|
|
7
17
|
|
|
@@ -17,9 +27,20 @@ export interface ToolPart {
|
|
|
17
27
|
export interface TextPart {
|
|
18
28
|
kind: "text";
|
|
19
29
|
text: string;
|
|
30
|
+
/** Still being painted by `assistant_delta` tokens. Cleared when the segment
|
|
31
|
+
* closes (`assistant_text` / `final`) and the cleaned text replaces it. */
|
|
32
|
+
streaming?: boolean;
|
|
20
33
|
}
|
|
21
34
|
|
|
22
|
-
|
|
35
|
+
/** The model thinking out loud. Never part of the answer — `textOf` skips it,
|
|
36
|
+
* so it is not copied, not counted as the reply, and not spoken. */
|
|
37
|
+
export interface ReasoningPart {
|
|
38
|
+
kind: "reasoning";
|
|
39
|
+
text: string;
|
|
40
|
+
streaming?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type ChatPart = TextPart | ToolPart | ReasoningPart;
|
|
23
44
|
|
|
24
45
|
export interface ChatMsg {
|
|
25
46
|
role: "user" | "assistant";
|
|
@@ -33,19 +54,40 @@ export interface ChatMsg {
|
|
|
33
54
|
agent?: string;
|
|
34
55
|
/** Stable id of that actor (super_agent | agent slug). Turns are split on it. */
|
|
35
56
|
agentId?: string;
|
|
57
|
+
/** Group only: which agent's @mention pulled this speaker in ("traído por X"). */
|
|
58
|
+
reason?: string;
|
|
59
|
+
/** A group system notice ("joined"/"left") — rendered as a centred line, not a
|
|
60
|
+
* bubble. `who` is the agent slug it is about. */
|
|
61
|
+
event?: "joined" | "left";
|
|
62
|
+
who?: string;
|
|
36
63
|
/** Token accounting from the `final` event. */
|
|
37
64
|
usage?: ChatUsage;
|
|
65
|
+
/** You stopped this turn. What is here is what it had done — real work, kept
|
|
66
|
+
* on purpose so the message that interrupted it reads this as its history. */
|
|
67
|
+
stopped?: boolean;
|
|
38
68
|
/** Operational notes (engine fallbacks, retries, suppressions). */
|
|
39
69
|
notes?: string[];
|
|
40
70
|
/** What a HISTORICAL turn did. Live turns render real ToolCall parts from
|
|
41
71
|
* the stream; replayed ones only have this, recorded at write time. */
|
|
42
72
|
toolSummary?: ToolSummary;
|
|
73
|
+
/** The attachment this user turn carried (voice note, photo, document).
|
|
74
|
+
* Rendered as the file itself — the stored text is only the marker the
|
|
75
|
+
* agent was handed. */
|
|
76
|
+
media?: MessageMedia[];
|
|
77
|
+
/** Composed HERE, in this tab, rather than read back from storage.
|
|
78
|
+
* A reply typed into a Telegram thread goes out on the `web` channel, so the
|
|
79
|
+
* Telegram thread file will never contain it — and a background refresh that
|
|
80
|
+
* took the file as the whole truth would erase what you just sent. See
|
|
81
|
+
* `mergeLocalTurns`. */
|
|
82
|
+
local?: boolean;
|
|
43
83
|
/** Skill Inspector decision for this turn (when the feature is on): which
|
|
44
84
|
* skills the per-turn RAG loaded inline vs merely hinted. */
|
|
45
85
|
inspector?: {
|
|
46
86
|
embedder?: string;
|
|
47
87
|
loaded?: string[];
|
|
48
88
|
hinted?: string[];
|
|
89
|
+
/** Top matches with their cosine similarity, for the badge popover. */
|
|
90
|
+
scored?: { slug: string; sim: number }[];
|
|
49
91
|
};
|
|
50
92
|
}
|
|
51
93
|
|
|
@@ -54,25 +96,110 @@ export interface SendOptions {
|
|
|
54
96
|
model?: string;
|
|
55
97
|
/** When set, talk to a project agent (non-streaming) instead of Roby. */
|
|
56
98
|
agentSlug?: string;
|
|
99
|
+
/** Files already stored under ~/.apx/media that this turn carries. The daemon
|
|
100
|
+
* resolves each path, hands the images to the model and writes the marker
|
|
101
|
+
* that names them. Super-agent turns only. */
|
|
102
|
+
attachments?: UploadedMedia[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** A turn written while the previous one was still running.
|
|
106
|
+
*
|
|
107
|
+
* It is IN the thread from the moment you send it — dimmed, with a way out —
|
|
108
|
+
* and goes out by itself the moment the running turn lands. Sending during a
|
|
109
|
+
* run used to be refused outright, which left "stop" as the only thing the
|
|
110
|
+
* button under a working agent could do: the way to add a sentence to the
|
|
111
|
+
* conversation was to kill the answer being written. */
|
|
112
|
+
export interface QueuedTurn {
|
|
113
|
+
id: string;
|
|
114
|
+
/** The bubble, built exactly like the one a sent turn gets — the marker text
|
|
115
|
+
* included, so it renders its photo and not the marker's words. */
|
|
116
|
+
msg: ChatMsg;
|
|
117
|
+
/** What actually goes out when its turn comes. */
|
|
118
|
+
text: string;
|
|
119
|
+
opts: SendOptions;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Queue ownership follows the CHAT, not the mounted pane. Navigating away
|
|
123
|
+
// destroys ChatTab but must not destroy a message already accepted from the
|
|
124
|
+
// composer. The worker that started the active turn drains this map even after
|
|
125
|
+
// its component unmounts; a newly mounted pane subscribes and paints it again.
|
|
126
|
+
const backgroundQueues = new Map<string, QueuedTurn[]>();
|
|
127
|
+
const backgroundQueueListeners = new Set<(key: string) => void>();
|
|
128
|
+
let backgroundQueueSeq = 0;
|
|
129
|
+
|
|
130
|
+
function readBackgroundQueue(key: string | null): QueuedTurn[] {
|
|
131
|
+
return key ? backgroundQueues.get(key) || [] : [];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function writeBackgroundQueue(key: string, queue: QueuedTurn[]) {
|
|
135
|
+
if (queue.length) backgroundQueues.set(key, queue);
|
|
136
|
+
else backgroundQueues.delete(key);
|
|
137
|
+
setChatQueued(key, queue.length);
|
|
138
|
+
for (const listener of backgroundQueueListeners) listener(key);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function takeBackgroundQueue(key: string): QueuedTurn | null {
|
|
142
|
+
const [next, ...rest] = readBackgroundQueue(key);
|
|
143
|
+
if (!next) return null;
|
|
144
|
+
writeBackgroundQueue(key, rest);
|
|
145
|
+
return next;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function moveBackgroundQueue(from: string | null, to: string) {
|
|
149
|
+
if (!from || from === to) return;
|
|
150
|
+
const pending = readBackgroundQueue(from);
|
|
151
|
+
if (!pending.length) return;
|
|
152
|
+
writeBackgroundQueue(from, []);
|
|
153
|
+
writeBackgroundQueue(to, [...readBackgroundQueue(to), ...pending]);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface ReloadOptions {
|
|
157
|
+
/** A BACKGROUND re-read: the same conversation moved somewhere else and we
|
|
158
|
+
* are catching up. It must not blank the pane first, must not drop what was
|
|
159
|
+
* typed here, and must leave the view alone if the fetch fails — none of
|
|
160
|
+
* which apply when the user actively picked a different chat. */
|
|
161
|
+
silent?: boolean;
|
|
57
162
|
}
|
|
58
163
|
|
|
59
164
|
export interface UseChatResult {
|
|
60
165
|
msgs: ChatMsg[];
|
|
61
166
|
send: (text: string, opts?: SendOptions) => Promise<void>;
|
|
167
|
+
/** Run one GROUP turn: the owner's line fans out to the room as a cascade of
|
|
168
|
+
* speakers, each streamed into its own pending bubble (name + "traído por X").
|
|
169
|
+
* `nameOf` resolves a slug to a display name; `rerun` re-runs from a speaker
|
|
170
|
+
* against the last owner message (`from` = slug, `reason` = who pulled them). */
|
|
171
|
+
sendGroup: (gid: string, text: string, nameOf: (slug: string) => string, opts?: { rerun?: boolean; from?: string; reason?: string | null; media?: UploadedMedia[] }) => Promise<void>;
|
|
62
172
|
stop: () => void;
|
|
63
|
-
clear: () => void;
|
|
173
|
+
clear: (queueKey?: string) => void;
|
|
64
174
|
/** Load a persisted conversation as history and bind subsequent sends to it.
|
|
65
175
|
* Only supported for project agents (super-agent conversations aren't
|
|
66
176
|
* persisted per-file). Pass `null` to drop the binding without clearing. */
|
|
67
|
-
load: (agentSlug: string, conversationId: string) => Promise<void>;
|
|
177
|
+
load: (agentSlug: string, conversationId: string, opts?: ReloadOptions) => Promise<void>;
|
|
68
178
|
/** Load a super-agent channel thread (telegram/desktop/…) as history. Not
|
|
69
179
|
* bound to a conversation file — continuing sends go out as fresh web
|
|
70
180
|
* turns with the thread as previousMessages context. */
|
|
71
|
-
loadThread: (channel: string, threadId: string) => Promise<void>;
|
|
181
|
+
loadThread: (channel: string, threadId: string, opts?: ReloadOptions) => Promise<void>;
|
|
182
|
+
/** Re-run the user turn that produced the assistant message at `index`,
|
|
183
|
+
* dropping that answer and everything after it (in the pane AND, for a bound
|
|
184
|
+
* project-agent conversation, on disk). Project-agent / live chats only. */
|
|
185
|
+
regenerate: (index: number, opts?: SendOptions) => Promise<void>;
|
|
186
|
+
/** Replace the user message at `index` with `text` and re-send, dropping that
|
|
187
|
+
* message and everything after it. Project-agent / live chats only. */
|
|
188
|
+
editAndResend: (index: number, text: string, opts?: SendOptions) => Promise<void>;
|
|
72
189
|
streaming: boolean;
|
|
190
|
+
/** Turns typed while this one was running, in the order they were written.
|
|
191
|
+
* They belong under the thread, not in `msgs`: everything that paints a live
|
|
192
|
+
* answer works on the trailing message, and a queued bubble parked there
|
|
193
|
+
* would silently swallow the rest of the stream. */
|
|
194
|
+
queued: QueuedTurn[];
|
|
195
|
+
/** Take one back before it goes out. */
|
|
196
|
+
unqueue: (id: string) => void;
|
|
73
197
|
/** Conversation id we're bound to, if any. Lets callers reflect "live vs
|
|
74
198
|
* loaded" state in the UI. */
|
|
75
199
|
conversationId: string | undefined;
|
|
200
|
+
/** What the loaded conversation records about itself — engine, channel — from
|
|
201
|
+
* its own frontmatter, which is authoritative where a list row's guess is not. */
|
|
202
|
+
conversationMeta: ConversationMeta | undefined;
|
|
76
203
|
}
|
|
77
204
|
|
|
78
205
|
/** Concatenate the text parts of a message (for clipboard). */
|
|
@@ -129,8 +256,55 @@ export function historyTextOf(msg: ChatMsg): string {
|
|
|
129
256
|
return chunks.join("\n\n").trim();
|
|
130
257
|
}
|
|
131
258
|
|
|
259
|
+
/** Re-send shape for files already on a stored turn (edit / regenerate). */
|
|
260
|
+
export function attachmentsOf(msg: ChatMsg): UploadedMedia[] {
|
|
261
|
+
return (msg.media || [])
|
|
262
|
+
.filter((m): m is MessageMedia & { path: string } => typeof m.path === "string" && m.path.length > 0)
|
|
263
|
+
.map((m) => ({
|
|
264
|
+
path: m.path,
|
|
265
|
+
name: m.name || "file",
|
|
266
|
+
mime: m.mime || "application/octet-stream",
|
|
267
|
+
kind: m.kind,
|
|
268
|
+
size: m.size ?? 0,
|
|
269
|
+
}));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Drop leading `[…]` markers so re-attaching files does not double them. */
|
|
273
|
+
function stripLeadingMarkers(text: string, count: number): string {
|
|
274
|
+
let out = text;
|
|
275
|
+
for (let i = 0; i < Math.max(1, count); i++) {
|
|
276
|
+
const next = out.replace(/^\[[^\]]*\]\s*/, "");
|
|
277
|
+
if (next === out) break;
|
|
278
|
+
out = next;
|
|
279
|
+
}
|
|
280
|
+
return out.trim();
|
|
281
|
+
}
|
|
282
|
+
|
|
132
283
|
const userPart = (text: string): ChatPart[] => [{ kind: "text", text }];
|
|
133
284
|
|
|
285
|
+
/** The stored-turn shape of a file the composer just uploaded. */
|
|
286
|
+
const mediaOf = (file: UploadedMedia): MessageMedia => ({
|
|
287
|
+
kind: file.kind,
|
|
288
|
+
path: file.path,
|
|
289
|
+
name: file.name,
|
|
290
|
+
mime: file.mime,
|
|
291
|
+
size: file.size,
|
|
292
|
+
duration: null,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
/** The markers the daemon will write for these files (see readTurnAttachments),
|
|
296
|
+
* mirrored locally so the sent turn matches the ledger after a silent reload.
|
|
297
|
+
* A short `[image attached]` used to diverge from `[image attached — saved to
|
|
298
|
+
* …]` and leave the optimistic bubble dangling as a duplicate photo. */
|
|
299
|
+
const markersFor = (files: UploadedMedia[]): string =>
|
|
300
|
+
files
|
|
301
|
+
.map((f) =>
|
|
302
|
+
f.kind === "photo"
|
|
303
|
+
? `[image attached — saved to ${f.path}]`
|
|
304
|
+
: `[file attached: ${f.name}, ${f.mime} — saved to ${f.path}. You can open it with your file tools.]`,
|
|
305
|
+
)
|
|
306
|
+
.join(" ");
|
|
307
|
+
|
|
134
308
|
function isErrorResult(result: unknown): boolean {
|
|
135
309
|
if (!result || typeof result !== "object") return false;
|
|
136
310
|
const r = result as Record<string, unknown>;
|
|
@@ -159,15 +333,29 @@ function threadToChatMsgs(messages: ConversationMessage[]): ChatMsg[] {
|
|
|
159
333
|
if (m.role === "user") {
|
|
160
334
|
turn = null;
|
|
161
335
|
turnActor = undefined;
|
|
162
|
-
|
|
336
|
+
// A stored turn records ONE file (the ledger row holds a single media
|
|
337
|
+
// block) — the live turn below can carry several, so the field is a list
|
|
338
|
+
// either way and history just has a list of one.
|
|
339
|
+
out.push({ role: "user", parts: userPart(m.content), ts, ...(m.media ? { media: [m.media] } : {}) });
|
|
163
340
|
} else if (m.role === "assistant" || m.role === "tool") {
|
|
164
341
|
// Tool rows inherit the current actor (they're logged by whoever is
|
|
165
342
|
// running); only assistant rows can start a new one.
|
|
343
|
+
//
|
|
344
|
+
// A turn OPENED by tool rows has no actor yet, and the assistant row that
|
|
345
|
+
// follows is the one that names it — not a second turn. Comparing against
|
|
346
|
+
// an undefined actor split every "tools first, then the answer" turn in
|
|
347
|
+
// two: the calls in a bubble credited to nobody, the answer in another.
|
|
348
|
+
// That is what kept the question panel from ever appearing — the
|
|
349
|
+
// ask_questions call ended up one bubble BEHIND the last message, so the
|
|
350
|
+
// questions read as already answered and there was nothing to pick from.
|
|
166
351
|
const actor = m.role === "assistant" ? m.agent : turnActor;
|
|
167
|
-
|
|
352
|
+
const named = turnActor !== undefined;
|
|
353
|
+
if (!turn || (m.role === "assistant" && named && actor !== turnActor)) {
|
|
168
354
|
turn = { role: "assistant", parts: [], ts };
|
|
169
355
|
turnActor = actor;
|
|
170
356
|
out.push(turn);
|
|
357
|
+
} else if (m.role === "assistant" && !named) {
|
|
358
|
+
turnActor = actor;
|
|
171
359
|
}
|
|
172
360
|
if (m.role === "tool") {
|
|
173
361
|
turn.parts.push({
|
|
@@ -181,22 +369,109 @@ function threadToChatMsgs(messages: ConversationMessage[]): ChatMsg[] {
|
|
|
181
369
|
} else {
|
|
182
370
|
if (m.agent) turn.agentId = m.agent;
|
|
183
371
|
if (m.agent_name) turn.agent = m.agent_name;
|
|
372
|
+
if (m.reason) turn.reason = m.reason; // group: "traído por X"
|
|
184
373
|
if (m.model) turn.model = m.model;
|
|
185
374
|
if (m.tool_summary) turn.toolSummary = m.tool_summary;
|
|
375
|
+
if (m.skill_inspector) turn.inspector = m.skill_inspector;
|
|
376
|
+
// What the AGENT sent with this turn — a skill's image it attached, a
|
|
377
|
+
// photo it pushed to Telegram, a file a routine delivered. The stored
|
|
378
|
+
// row always carried it; this branch simply never read it, so an agent
|
|
379
|
+
// could hand you a file and the thread would show the caption alone.
|
|
380
|
+
// `media_list` is the several-file spelling a delivery writes; the flat
|
|
381
|
+
// one mirrors its first file, so the list wins where both are present.
|
|
382
|
+
const sent = m.media_list?.length ? m.media_list : m.media ? [m.media] : [];
|
|
383
|
+
if (sent.length) turn.media = [...(turn.media || []), ...sent];
|
|
186
384
|
if (m.usage) {
|
|
187
385
|
turn.usage = {
|
|
188
386
|
input_tokens: (turn.usage?.input_tokens || 0) + (m.usage.input_tokens || 0),
|
|
189
387
|
output_tokens: (turn.usage?.output_tokens || 0) + (m.usage.output_tokens || 0),
|
|
190
388
|
};
|
|
191
389
|
}
|
|
390
|
+
// The thinking first, the answer after — the order it happened in and
|
|
391
|
+
// the order the live stream paints it.
|
|
392
|
+
for (const think of m.reasoning || []) {
|
|
393
|
+
if (think) turn.parts.push({ kind: "reasoning", text: think });
|
|
394
|
+
}
|
|
192
395
|
if (m.content) turn.parts.push({ kind: "text", text: m.content });
|
|
193
396
|
}
|
|
397
|
+
} else if (m.role === "system" && m.event) {
|
|
398
|
+
// A group join/leave notice — a standalone centred line, not part of any
|
|
399
|
+
// agent's turn.
|
|
400
|
+
turn = null;
|
|
401
|
+
turnActor = undefined;
|
|
402
|
+
out.push({ role: "assistant", parts: [], ts, event: m.event, who: m.who });
|
|
194
403
|
}
|
|
195
|
-
// system/compact rows are context-only; not rendered in the
|
|
404
|
+
// other system/compact rows are context-only; not rendered in the viewer.
|
|
196
405
|
}
|
|
197
406
|
return out;
|
|
198
407
|
}
|
|
199
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Reconcile a background re-read with what this tab composed itself.
|
|
411
|
+
*
|
|
412
|
+
* Storage wins for everything it knows about — it is the record, and it now
|
|
413
|
+
* holds whatever the other device just said. But a turn typed HERE may not be
|
|
414
|
+
* in the file we just read: a reply sent while reading a Telegram thread goes
|
|
415
|
+
* out on the `web` channel, so the Telegram day file will never contain it, and
|
|
416
|
+
* taking the file as the whole truth would make what you just sent vanish.
|
|
417
|
+
*
|
|
418
|
+
* Local turns that DID land in storage are matched by role + text OR by the
|
|
419
|
+
* same attachment path (media markers used to diverge between optimistic and
|
|
420
|
+
* ledger text, which left a duplicate photo at the foot of the thread until
|
|
421
|
+
* a hard reload) and dropped, so the same turn is never shown twice.
|
|
422
|
+
*/
|
|
423
|
+
function mediaPathsKey(m: ChatMsg): string {
|
|
424
|
+
return (m.media || []).map((x) => x.path).filter(Boolean).join("\0");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export function mergeLocalTurns(remote: ChatMsg[], current: ChatMsg[]): ChatMsg[] {
|
|
428
|
+
const extras = current.filter((m) => m.local);
|
|
429
|
+
if (!extras.length) return remote;
|
|
430
|
+
const seenText = new Set(remote.map((m) => `${m.role}|${textOf(m)}`));
|
|
431
|
+
const seenMedia = new Set(
|
|
432
|
+
remote.filter((m) => mediaPathsKey(m)).map((m) => `${m.role}|${mediaPathsKey(m)}`),
|
|
433
|
+
);
|
|
434
|
+
return [
|
|
435
|
+
...remote,
|
|
436
|
+
...extras.filter((m) => {
|
|
437
|
+
if (seenText.has(`${m.role}|${textOf(m)}`)) return false;
|
|
438
|
+
const paths = mediaPathsKey(m);
|
|
439
|
+
if (paths && seenMedia.has(`${m.role}|${paths}`)) return false;
|
|
440
|
+
return true;
|
|
441
|
+
}),
|
|
442
|
+
];
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/** History plus a trailing streaming bubble for a turn still being written, so
|
|
446
|
+
* opening a chat mid-answer shows the partial (then the live frames fill it).
|
|
447
|
+
* Marked local so a background refetch keeps it until the real turn persists. */
|
|
448
|
+
function withActiveTurn(loaded: ChatMsg[], active?: ActiveTurn | null): ChatMsg[] {
|
|
449
|
+
if (!active) return loaded;
|
|
450
|
+
const parts: ChatPart[] = active.parts?.length
|
|
451
|
+
? active.parts.map((part) => part.kind === "text"
|
|
452
|
+
? { kind: "text", text: part.text, ...(part.streaming ? { streaming: true } : {}) }
|
|
453
|
+
: {
|
|
454
|
+
kind: "tool",
|
|
455
|
+
id: part.id,
|
|
456
|
+
tool: part.tool,
|
|
457
|
+
args: part.args || undefined,
|
|
458
|
+
result: part.result,
|
|
459
|
+
status: part.status,
|
|
460
|
+
})
|
|
461
|
+
: active.text ? [{ kind: "text", text: active.text, streaming: true }] : [];
|
|
462
|
+
return [
|
|
463
|
+
...loaded,
|
|
464
|
+
{
|
|
465
|
+
role: "assistant",
|
|
466
|
+
parts,
|
|
467
|
+
ts: active.started_at || new Date().toISOString(),
|
|
468
|
+
local: true,
|
|
469
|
+
pending: true,
|
|
470
|
+
...(active.agent_slug ? { agent: active.agent_slug, agentId: active.agent_slug } : {}),
|
|
471
|
+
},
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
200
475
|
/**
|
|
201
476
|
* Pure reducer: apply one NDJSON stream event to an assistant turn and return
|
|
202
477
|
* the next turn. Every surface that consumes the super-agent stream (ChatTab,
|
|
@@ -226,18 +501,67 @@ export function applyStreamEvent(turn: ChatMsg, ev: ChatStreamEvent): ChatMsg {
|
|
|
226
501
|
return withNote(`tools suppressed: ${(ev.tools || []).join(", ")}`);
|
|
227
502
|
case "skill_inspector": {
|
|
228
503
|
const insp = ev.inspector;
|
|
229
|
-
|
|
504
|
+
// Keep the row when anything was injected OR merely scored: the "considered"
|
|
505
|
+
// near-misses are shown each round too, so the per-turn RAG is visible even
|
|
506
|
+
// when nothing crossed the load/hint bar (e.g. on the offline tf embedder).
|
|
507
|
+
if (!insp || (!insp.loaded?.length && !insp.hinted?.length && !insp.scored?.length))
|
|
508
|
+
return turn;
|
|
230
509
|
return {
|
|
231
510
|
...turn,
|
|
232
511
|
inspector: {
|
|
233
512
|
embedder: insp.embedder,
|
|
234
513
|
loaded: insp.loaded || [],
|
|
235
514
|
hinted: insp.hinted || [],
|
|
515
|
+
scored: insp.scored || [],
|
|
236
516
|
},
|
|
237
517
|
};
|
|
238
518
|
}
|
|
239
|
-
case "
|
|
240
|
-
|
|
519
|
+
case "assistant_reasoning_delta": {
|
|
520
|
+
// The thinking, live. Fills the wait before the first word of the answer.
|
|
521
|
+
const piece = ev.reasoning || "";
|
|
522
|
+
if (!piece) return turn;
|
|
523
|
+
const parts = [...turn.parts];
|
|
524
|
+
const last = parts[parts.length - 1];
|
|
525
|
+
if (last && last.kind === "reasoning" && last.streaming) {
|
|
526
|
+
parts[parts.length - 1] = { ...last, text: last.text + piece };
|
|
527
|
+
} else {
|
|
528
|
+
parts.push({ kind: "reasoning", text: piece, streaming: true });
|
|
529
|
+
}
|
|
530
|
+
return { ...turn, parts };
|
|
531
|
+
}
|
|
532
|
+
case "assistant_reasoning": {
|
|
533
|
+
// The whole block at once — the loop emits it after the call returns, so
|
|
534
|
+
// by now the answer may already be painted below the streamed thinking.
|
|
535
|
+
const full = ev.reasoning || "";
|
|
536
|
+
if (!full) return turn;
|
|
537
|
+
const parts = [...turn.parts];
|
|
538
|
+
for (let i = parts.length - 1; i >= 0; i--) {
|
|
539
|
+
const p = parts[i];
|
|
540
|
+
if (p.kind === "reasoning" && p.streaming) {
|
|
541
|
+
parts[i] = { kind: "reasoning", text: full };
|
|
542
|
+
return { ...turn, parts };
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
// An engine that never streamed it: the thinking happened BEFORE the
|
|
546
|
+
// answer, so it goes in front of the segment being written, not after.
|
|
547
|
+
const last = parts[parts.length - 1];
|
|
548
|
+
const at = last && last.kind === "text" && last.streaming ? parts.length - 1 : parts.length;
|
|
549
|
+
parts.splice(at, 0, { kind: "reasoning", text: full });
|
|
550
|
+
return { ...turn, parts };
|
|
551
|
+
}
|
|
552
|
+
case "assistant_text": {
|
|
553
|
+
// The cleaned close of the segment the deltas were painting. It REPLACES
|
|
554
|
+
// the streamed part — appending it would show the same answer twice, once
|
|
555
|
+
// token by token and once whole.
|
|
556
|
+
if (!ev.text) return turn;
|
|
557
|
+
const parts = [...turn.parts];
|
|
558
|
+
const last = parts[parts.length - 1];
|
|
559
|
+
if (last && last.kind === "text" && last.streaming) {
|
|
560
|
+
parts[parts.length - 1] = { kind: "text", text: ev.text };
|
|
561
|
+
return { ...turn, parts };
|
|
562
|
+
}
|
|
563
|
+
return { ...turn, parts: [...parts, { kind: "text", text: ev.text }] };
|
|
564
|
+
}
|
|
241
565
|
case "tool_start":
|
|
242
566
|
return ev.trace
|
|
243
567
|
? {
|
|
@@ -280,7 +604,24 @@ export function applyStreamEvent(turn: ChatMsg, ev: ChatStreamEvent): ChatMsg {
|
|
|
280
604
|
),
|
|
281
605
|
};
|
|
282
606
|
}
|
|
283
|
-
case "final":
|
|
607
|
+
case "final": {
|
|
608
|
+
// The closing message. It only counts as "already shown" when it IS one
|
|
609
|
+
// of the streamed segments — the guard used to be "the turn has any text
|
|
610
|
+
// at all", which silently dropped every answer that came after a tool
|
|
611
|
+
// call. Since the model writes a line before each tool, that was every
|
|
612
|
+
// multi-step turn: the reader saw the steps and never the conclusion.
|
|
613
|
+
// (api/code.js fixed the same bug on the persistence side.)
|
|
614
|
+
const finalText = ev.result?.text || "";
|
|
615
|
+
const parts = [...turn.parts];
|
|
616
|
+
const last = parts[parts.length - 1];
|
|
617
|
+
// A part still marked streaming holds raw tokens; `final` carries the
|
|
618
|
+
// cleaned version of that same text, so it lands in place.
|
|
619
|
+
if (finalText && last && last.kind === "text" && last.streaming) {
|
|
620
|
+
parts[parts.length - 1] = { kind: "text", text: finalText };
|
|
621
|
+
}
|
|
622
|
+
const alreadyShown =
|
|
623
|
+
!!finalText &&
|
|
624
|
+
parts.some((p) => p.kind === "text" && p.text.trim() === finalText.trim());
|
|
284
625
|
return {
|
|
285
626
|
...turn,
|
|
286
627
|
pending: false,
|
|
@@ -291,23 +632,73 @@ export function applyStreamEvent(turn: ChatMsg, ev: ChatStreamEvent): ChatMsg {
|
|
|
291
632
|
model: turn.model ?? ev.result?.model,
|
|
292
633
|
agent: turn.agent ?? ev.result?.name,
|
|
293
634
|
parts:
|
|
294
|
-
|
|
295
|
-
? [...
|
|
296
|
-
:
|
|
635
|
+
finalText && !alreadyShown
|
|
636
|
+
? [...parts, { kind: "text", text: finalText }]
|
|
637
|
+
: parts,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
case "start":
|
|
641
|
+
// Identity only (which conversation / channel this turn is on). The pane
|
|
642
|
+
// binds to it outside the reducer — nothing about the bubble changes.
|
|
643
|
+
return turn;
|
|
644
|
+
case "aborted": {
|
|
645
|
+
// You stopped it. Everything streamed so far stands: it is work that
|
|
646
|
+
// really happened, the daemon persisted it, and the message that
|
|
647
|
+
// interrupted this turn reads it as history. So the only thing to do is
|
|
648
|
+
// close the open segment — no text is thrown away, and this is NOT an
|
|
649
|
+
// error, which is why it does not travel as one.
|
|
650
|
+
return {
|
|
651
|
+
...turn,
|
|
652
|
+
pending: false,
|
|
653
|
+
stopped: true,
|
|
654
|
+
parts: turn.parts.map((p) =>
|
|
655
|
+
p.kind === "text" && p.streaming ? { kind: "text", text: p.text } : p,
|
|
656
|
+
),
|
|
297
657
|
};
|
|
658
|
+
}
|
|
298
659
|
default: {
|
|
299
|
-
//
|
|
660
|
+
// `assistant_delta` — tokens as the model writes them. They only ever
|
|
661
|
+
// extend a part that is still streaming; a segment already closed by
|
|
662
|
+
// assistant_text is finished and must not absorb the next one's tokens.
|
|
300
663
|
const piece = ev.delta || ev.content || "";
|
|
301
664
|
if (!piece) return turn;
|
|
302
665
|
const parts = [...turn.parts];
|
|
303
666
|
const last = parts[parts.length - 1];
|
|
304
|
-
if (last && last.kind === "text"
|
|
305
|
-
|
|
667
|
+
if (last && last.kind === "text" && last.streaming) {
|
|
668
|
+
parts[parts.length - 1] = { ...last, text: last.text + piece };
|
|
669
|
+
} else {
|
|
670
|
+
parts.push({ kind: "text", text: piece, streaming: true });
|
|
671
|
+
}
|
|
306
672
|
return { ...turn, parts };
|
|
307
673
|
}
|
|
308
674
|
}
|
|
309
675
|
}
|
|
310
676
|
|
|
677
|
+
/** What a stored conversation records about itself (its frontmatter). */
|
|
678
|
+
export interface ConversationMeta {
|
|
679
|
+
engine?: string;
|
|
680
|
+
channel?: string;
|
|
681
|
+
title?: string;
|
|
682
|
+
started?: string;
|
|
683
|
+
/** Multi-agent threads (a2a, group) only: who is in the room, and the face
|
|
684
|
+
* each one wears. Both come resolved from the daemon with the messages, so
|
|
685
|
+
* the header can draw the thread without being handed a list row — which is
|
|
686
|
+
* all the project Chats tab ever had, and why it drew none. */
|
|
687
|
+
participants?: string[];
|
|
688
|
+
faces?: AgentFace[];
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function metaFromDetail(detail: { channel?: string; meta?: Record<string, unknown> }): ConversationMeta {
|
|
692
|
+
const fm = (detail.meta || {}) as Record<string, unknown>;
|
|
693
|
+
const str = (v: unknown) => (typeof v === "string" && v.trim() ? v.trim() : undefined);
|
|
694
|
+
return {
|
|
695
|
+
engine: str(fm.engine),
|
|
696
|
+
channel: str(fm.channel) || detail.channel,
|
|
697
|
+
title: str(fm.title)?.replace(/^"|"$/g, ""),
|
|
698
|
+
started: str(fm.started),
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
|
|
311
702
|
/**
|
|
312
703
|
* Single source of truth for the project chat. For Roby (super-agent) it
|
|
313
704
|
* consumes the NDJSON event stream and builds a rich, opencode-style turn:
|
|
@@ -317,25 +708,111 @@ export function applyStreamEvent(turn: ChatMsg, ev: ChatStreamEvent): ChatMsg {
|
|
|
317
708
|
*/
|
|
318
709
|
export function useChat(pid: string, onError?: (msg: string) => void): UseChatResult {
|
|
319
710
|
const [msgs, setMsgs] = useState<ChatMsg[]>([]);
|
|
711
|
+
const msgsRef = useRef<ChatMsg[]>([]);
|
|
712
|
+
// React drops state updates after unmount. The request does not stop, so keep
|
|
713
|
+
// the worker's own copy synchronously: a background queue needs the answer
|
|
714
|
+
// that just finished when it builds the next turn's history.
|
|
715
|
+
const updateMsgs = useCallback((next: ChatMsg[] | ((curr: ChatMsg[]) => ChatMsg[])) => {
|
|
716
|
+
const value = typeof next === "function" ? next(msgsRef.current) : next;
|
|
717
|
+
msgsRef.current = value;
|
|
718
|
+
setMsgs(value);
|
|
719
|
+
}, []);
|
|
320
720
|
const [streaming, setStreaming] = useState(false);
|
|
721
|
+
const streamingRef = useRef(false);
|
|
722
|
+
const updateStreaming = useCallback((value: boolean) => {
|
|
723
|
+
streamingRef.current = value;
|
|
724
|
+
setStreaming(value);
|
|
725
|
+
}, []);
|
|
726
|
+
// A turn opened from another pane is busy too, but it is followed through the
|
|
727
|
+
// shared feed instead of this hook's NDJSON response.
|
|
728
|
+
const [following, setFollowing] = useState(false);
|
|
729
|
+
const followingRef = useRef(false);
|
|
730
|
+
const updateFollowing = useCallback((value: boolean) => {
|
|
731
|
+
followingRef.current = value;
|
|
732
|
+
setFollowing(value);
|
|
733
|
+
}, []);
|
|
321
734
|
const [conversationId, setConversationId] = useState<string | undefined>(undefined);
|
|
735
|
+
// What the LOADED conversation says about itself — its engine, its channel —
|
|
736
|
+
// read from the file's own frontmatter rather than from whatever the list row
|
|
737
|
+
// guessed. A routine conversation opened from the sidebar was labelled "new
|
|
738
|
+
// chat · web" and showed no model at all, because the header only ever saw
|
|
739
|
+
// the selection metadata.
|
|
740
|
+
const [conversationMeta, setConversationMeta] = useState<ConversationMeta | undefined>(undefined);
|
|
741
|
+
// Written during a run, waiting their turn. Deliberately their own state and
|
|
742
|
+
// not a flag on a `msgs` entry: `patchLast` and the two error paths below all
|
|
743
|
+
// address the LAST message, so a queued bubble sitting there would take the
|
|
744
|
+
// rest of the stream, or be the thing a failed turn deleted.
|
|
745
|
+
const [queued, setQueued] = useState<QueuedTurn[]>([]);
|
|
746
|
+
const queueKeyRef = useRef<string | null>(null);
|
|
747
|
+
const bindQueue = useCallback((key: string) => {
|
|
748
|
+
queueKeyRef.current = key;
|
|
749
|
+
const snapshot = readBackgroundQueue(key);
|
|
750
|
+
setQueued(snapshot);
|
|
751
|
+
setChatQueued(key, snapshot.length);
|
|
752
|
+
}, []);
|
|
753
|
+
useEffect(() => {
|
|
754
|
+
const listener = (key: string) => {
|
|
755
|
+
if (key === queueKeyRef.current) setQueued(readBackgroundQueue(key));
|
|
756
|
+
};
|
|
757
|
+
backgroundQueueListeners.add(listener);
|
|
758
|
+
return () => { backgroundQueueListeners.delete(listener); };
|
|
759
|
+
}, []);
|
|
322
760
|
const abortRef = useRef<AbortController | null>(null);
|
|
761
|
+
// How the DAEMON addresses the turn this tab started, so it can be stopped for
|
|
762
|
+
// real. abortRef only closes our socket, which the daemon deliberately ignores
|
|
763
|
+
// (that is what lets another tab catch up on a running turn) — the run itself
|
|
764
|
+
// stops only when asked, through POST /turns/abort.
|
|
765
|
+
const turnTargetRef = useRef<{ conversation_id?: string; channel?: string; thread_id?: string } | null>(null);
|
|
323
766
|
const convoRef = useRef<string | undefined>(undefined);
|
|
767
|
+
const threadRef = useRef<{ channel: string; id: string } | null>(null);
|
|
324
768
|
// Monotonic token guarding async history loads. Every load()/loadThread()/
|
|
325
769
|
// clear() bumps it; a load only applies its result if it's still the latest.
|
|
326
770
|
// Without this, clicking chat A then B could land A's (slower) response last
|
|
327
771
|
// and paint A's messages under B's header.
|
|
328
772
|
const loadSeqRef = useRef(0);
|
|
773
|
+
// A running request belongs to the chat it started in, not whichever chat
|
|
774
|
+
// the owner opens next. Loading another chat advances this epoch: the old
|
|
775
|
+
// request continues in the daemon, but its late NDJSON frames cannot append
|
|
776
|
+
// themselves to the newly selected transcript.
|
|
777
|
+
const viewEpochRef = useRef(0);
|
|
778
|
+
// True while THIS tab is the one streaming a turn over its own NDJSON socket.
|
|
779
|
+
// The sender renders from that; it must ignore the live-turn frames the daemon
|
|
780
|
+
// also pushes for everyone else, or it would paint each token twice.
|
|
781
|
+
// Updated synchronously by updateStreaming; no render/effect round trip, so a
|
|
782
|
+
// send in the same tick cannot start a second run.
|
|
783
|
+
// Interrupt or wait, when you write during a running turn. Held in a ref so
|
|
784
|
+
// flipping the switch mid-turn takes effect on the very next send instead of
|
|
785
|
+
// rebuilding `send` and its captured history.
|
|
786
|
+
const queueOnSendRef = useRef(queueOnSend());
|
|
787
|
+
useEffect(() => {
|
|
788
|
+
const read = () => { queueOnSendRef.current = queueOnSend(); };
|
|
789
|
+
read();
|
|
790
|
+
return onChatPrefsChange(read);
|
|
791
|
+
}, []);
|
|
792
|
+
// The turn this tab is FOLLOWING from the live feed (a turn it did not start):
|
|
793
|
+
// its id and the text accumulated so far, so a frame that arrives after a
|
|
794
|
+
// background refetch wiped the bubble can repaint it whole rather than lose it.
|
|
795
|
+
const liveTurnRef = useRef<{ id: string; text: string } | null>(null);
|
|
796
|
+
const drainQueueRef = useRef<() => void>(() => {});
|
|
797
|
+
const beginViewChange = useCallback(() => {
|
|
798
|
+
viewEpochRef.current++;
|
|
799
|
+
// Detach this pane from a turn that is still running elsewhere. The daemon
|
|
800
|
+
// and shared feed keep it alive; this chat must not inherit its typing pill.
|
|
801
|
+
updateStreaming(false);
|
|
802
|
+
updateFollowing(false);
|
|
803
|
+
liveTurnRef.current = null;
|
|
804
|
+
turnTargetRef.current = null;
|
|
805
|
+
}, [updateFollowing, updateStreaming]);
|
|
329
806
|
|
|
330
807
|
// Mutate the trailing assistant turn in place.
|
|
331
808
|
const patchLast = useCallback((fn: (m: ChatMsg) => ChatMsg) => {
|
|
332
|
-
|
|
809
|
+
updateMsgs((curr) => {
|
|
333
810
|
const copy = [...curr];
|
|
334
811
|
const last = copy[copy.length - 1];
|
|
335
812
|
if (last && last.role === "assistant") copy[copy.length - 1] = fn(last);
|
|
336
813
|
return copy;
|
|
337
814
|
});
|
|
338
|
-
}, []);
|
|
815
|
+
}, [updateMsgs]);
|
|
339
816
|
|
|
340
817
|
const applyEvent = useCallback(
|
|
341
818
|
(ev: ChatStreamEvent) => {
|
|
@@ -343,53 +820,310 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
|
|
|
343
820
|
onError?.(ev.error || t("shared_ui.err_stream"));
|
|
344
821
|
return;
|
|
345
822
|
}
|
|
823
|
+
// The turn names itself before it does any work, so stopping it (or
|
|
824
|
+
// interrupting it by sending) works from the first token — not only once
|
|
825
|
+
// it is over, which is when `final` used to be the first mention of the
|
|
826
|
+
// conversation it had been writing to all along.
|
|
827
|
+
if (ev.type === "start") {
|
|
828
|
+
if (ev.conversation_id) {
|
|
829
|
+
const nextKey = conversationActivityKey(pid, ev.conversation_id);
|
|
830
|
+
moveBackgroundQueue(queueKeyRef.current, nextKey);
|
|
831
|
+
bindQueue(nextKey);
|
|
832
|
+
convoRef.current = ev.conversation_id;
|
|
833
|
+
threadRef.current = null;
|
|
834
|
+
setConversationId(ev.conversation_id);
|
|
835
|
+
turnTargetRef.current = { conversation_id: ev.conversation_id };
|
|
836
|
+
} else if (ev.channel) {
|
|
837
|
+
const threadId = ev.thread_id || new Date().toISOString().slice(0, 10);
|
|
838
|
+
bindQueue(threadActivityKey(pid, ev.channel, threadId));
|
|
839
|
+
threadRef.current = { channel: ev.channel, id: threadId };
|
|
840
|
+
turnTargetRef.current = { channel: ev.channel };
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
// Stopped by you. Mark it the same way the local-abort path does, so the
|
|
844
|
+
// two routes to the same outcome read identically in the thread.
|
|
845
|
+
if (ev.type === "aborted") {
|
|
846
|
+
patchLast((m) => {
|
|
847
|
+
const closed = applyStreamEvent(m, ev);
|
|
848
|
+
return { ...closed, parts: [...closed.parts, { kind: "text", text: t("code_module.stopped") }] };
|
|
849
|
+
});
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
346
852
|
patchLast((m) => applyStreamEvent(m, ev));
|
|
347
853
|
},
|
|
348
|
-
[patchLast, onError],
|
|
854
|
+
[pid, patchLast, onError, bindQueue],
|
|
349
855
|
);
|
|
350
856
|
|
|
857
|
+
// ── Following a turn this tab did NOT start (live push) ──────────────────
|
|
858
|
+
// paintLive upserts a trailing STREAMING assistant bubble (local, so a
|
|
859
|
+
// background refetch keeps it) from the full accumulated text; finalizeLive
|
|
860
|
+
// turns it into a settled reply (no longer local, so the next silent reload
|
|
861
|
+
// replaces it with the identical persisted turn instead of doubling it).
|
|
862
|
+
const paintLive = useCallback((text: string, agentSlug?: string) => {
|
|
863
|
+
updateMsgs((curr) => {
|
|
864
|
+
const copy = [...curr];
|
|
865
|
+
const last = copy[copy.length - 1];
|
|
866
|
+
const base: ChatMsg = {
|
|
867
|
+
role: "assistant",
|
|
868
|
+
parts: text ? [{ kind: "text", text, streaming: true }] : [],
|
|
869
|
+
ts: new Date().toISOString(),
|
|
870
|
+
local: true,
|
|
871
|
+
pending: true,
|
|
872
|
+
...(agentSlug ? { agent: agentSlug, agentId: agentSlug } : {}),
|
|
873
|
+
};
|
|
874
|
+
if (last && last.role === "assistant" && (last.pending || last.local)) {
|
|
875
|
+
copy[copy.length - 1] = { ...base, ts: last.ts };
|
|
876
|
+
} else {
|
|
877
|
+
copy.push(base);
|
|
878
|
+
}
|
|
879
|
+
return copy;
|
|
880
|
+
});
|
|
881
|
+
}, [updateMsgs]);
|
|
882
|
+
|
|
883
|
+
const finalizeLive = useCallback(
|
|
884
|
+
(text: string, opts: { model?: string; name?: string; usage?: ChatUsage; error?: string }) => {
|
|
885
|
+
updateMsgs((curr) => {
|
|
886
|
+
const copy = [...curr];
|
|
887
|
+
const last = copy[copy.length - 1];
|
|
888
|
+
if (!last || last.role !== "assistant") return curr;
|
|
889
|
+
copy[copy.length - 1] = {
|
|
890
|
+
...last,
|
|
891
|
+
local: false,
|
|
892
|
+
pending: false,
|
|
893
|
+
parts: text
|
|
894
|
+
? [{ kind: "text", text }]
|
|
895
|
+
: opts.error
|
|
896
|
+
? [{ kind: "text", text: t("shared_ui.err_stream") }]
|
|
897
|
+
: last.parts,
|
|
898
|
+
...(opts.model ? { model: opts.model } : {}),
|
|
899
|
+
...(opts.name ? { agent: opts.name, agentId: opts.name } : {}),
|
|
900
|
+
...(opts.usage ? { usage: opts.usage } : {}),
|
|
901
|
+
};
|
|
902
|
+
return copy;
|
|
903
|
+
});
|
|
904
|
+
},
|
|
905
|
+
[updateMsgs],
|
|
906
|
+
);
|
|
907
|
+
|
|
908
|
+
// The daemon pushes every in-progress turn's tokens over the shared feed. A
|
|
909
|
+
// tab that is NOT the sender — a second window, or this one after a refresh /
|
|
910
|
+
// switching chats and back — follows them here, so streaming survives losing
|
|
911
|
+
// the connection that started it.
|
|
912
|
+
const onTurnFrame = useCallback((f: TurnFrame) => {
|
|
913
|
+
if (streamingRef.current) return; // the sender renders via NDJSON
|
|
914
|
+
if (String(f.project_id) !== String(pid)) return;
|
|
915
|
+
const sameConversation = !!convoRef.current && f.conversation_id === convoRef.current;
|
|
916
|
+
const sameThread = !!threadRef.current &&
|
|
917
|
+
f.channel === threadRef.current.channel &&
|
|
918
|
+
f.thread_id === threadRef.current.id;
|
|
919
|
+
if (!sameConversation && !sameThread) return;
|
|
920
|
+
if (f.phase === "start") {
|
|
921
|
+
updateFollowing(true);
|
|
922
|
+
turnTargetRef.current = sameConversation
|
|
923
|
+
? { conversation_id: f.conversation_id || undefined }
|
|
924
|
+
: { channel: f.channel || undefined };
|
|
925
|
+
liveTurnRef.current = { id: f.turn_id, text: "" };
|
|
926
|
+
paintLive("", f.agent_slug || undefined);
|
|
927
|
+
} else if (f.phase === "delta") {
|
|
928
|
+
const cur =
|
|
929
|
+
liveTurnRef.current && liveTurnRef.current.id === f.turn_id
|
|
930
|
+
? liveTurnRef.current
|
|
931
|
+
: (liveTurnRef.current = { id: f.turn_id, text: "" });
|
|
932
|
+
cur.text += f.delta || "";
|
|
933
|
+
paintLive(cur.text, f.agent_slug || undefined);
|
|
934
|
+
} else if (f.phase === "final") {
|
|
935
|
+
liveTurnRef.current = null;
|
|
936
|
+
updateFollowing(false);
|
|
937
|
+
finalizeLive(f.result?.text ?? "", {
|
|
938
|
+
model: f.result?.model,
|
|
939
|
+
name: f.result?.name || f.agent_slug || undefined,
|
|
940
|
+
usage: f.result?.usage,
|
|
941
|
+
});
|
|
942
|
+
turnTargetRef.current = null;
|
|
943
|
+
queueMicrotask(() => drainQueueRef.current());
|
|
944
|
+
} else if (f.phase === "aborted") {
|
|
945
|
+
liveTurnRef.current = null;
|
|
946
|
+
updateFollowing(false);
|
|
947
|
+
finalizeLive(f.result?.text ?? "", {
|
|
948
|
+
model: f.result?.model,
|
|
949
|
+
name: f.result?.name || f.agent_slug || undefined,
|
|
950
|
+
usage: f.result?.usage,
|
|
951
|
+
});
|
|
952
|
+
turnTargetRef.current = null;
|
|
953
|
+
queueMicrotask(() => drainQueueRef.current());
|
|
954
|
+
} else if (f.phase === "error") {
|
|
955
|
+
liveTurnRef.current = null;
|
|
956
|
+
updateFollowing(false);
|
|
957
|
+
finalizeLive("", { error: f.error });
|
|
958
|
+
turnTargetRef.current = null;
|
|
959
|
+
queueMicrotask(() => drainQueueRef.current());
|
|
960
|
+
}
|
|
961
|
+
}, [pid, paintLive, finalizeLive, updateFollowing]);
|
|
962
|
+
|
|
963
|
+
useEffect(() => subscribeTurns(onTurnFrame), [onTurnFrame]);
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Stop the turn the DAEMON is running, not just the socket we read it through.
|
|
967
|
+
*
|
|
968
|
+
* Closing the stream has never stopped a run and must not start to: another
|
|
969
|
+
* tab, or this one after a refresh, catches up on a turn in progress from the
|
|
970
|
+
* daemon's own copy (see active-turns.js). So the ask is explicit, and the
|
|
971
|
+
* daemon answers by closing the stream with `aborted` — which carries the
|
|
972
|
+
* partial and is not an error.
|
|
973
|
+
*
|
|
974
|
+
* The local abort is the fallback for the two cases the daemon cannot answer:
|
|
975
|
+
* there is no live turn under that key (it finished a moment before the click
|
|
976
|
+
* landed), or the request itself failed.
|
|
977
|
+
*/
|
|
978
|
+
const stopTurn = useCallback(async () => {
|
|
979
|
+
const target = turnTargetRef.current;
|
|
980
|
+
if (target) {
|
|
981
|
+
try {
|
|
982
|
+
const { aborted } = await Turns.abort(pid, target);
|
|
983
|
+
if (aborted) return;
|
|
984
|
+
} catch {
|
|
985
|
+
/* the daemon could not be asked — fall through and at least stop reading */
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
abortRef.current?.abort();
|
|
989
|
+
}, [pid]);
|
|
990
|
+
|
|
351
991
|
const send = useCallback(
|
|
352
992
|
async (text: string, opts: SendOptions = {}) => {
|
|
353
993
|
const trimmed = text.trim();
|
|
354
|
-
|
|
994
|
+
const files = opts.attachments || [];
|
|
995
|
+
// A photo with no caption is a turn; text alone still is one.
|
|
996
|
+
if (!trimmed && !files.length) return;
|
|
355
997
|
const nowIso = () => new Date().toISOString();
|
|
356
|
-
|
|
998
|
+
// The bubble a turn gets, whether it goes out now or in a minute. The
|
|
999
|
+
// marker rides on the turn's text the way it does on every other channel:
|
|
1000
|
+
// the bubble strips it (the file is shown instead) and the NEXT turn's
|
|
1001
|
+
// history still records that something was attached.
|
|
1002
|
+
const bubble = (): ChatMsg => ({
|
|
1003
|
+
role: "user",
|
|
1004
|
+
parts: userPart([markersFor(files), trimmed].filter(Boolean).join(" ")),
|
|
1005
|
+
ts: nowIso(),
|
|
1006
|
+
// Composed here: a background refresh keeps it even when the thread
|
|
1007
|
+
// being read is not the channel this turn goes out on.
|
|
1008
|
+
local: true,
|
|
1009
|
+
...(files.length ? { media: files.map(mediaOf) } : {}),
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
// Written while the previous turn was still going. It joins the thread
|
|
1013
|
+
// now and leaves on its own when the run lands — refusing it is what made
|
|
1014
|
+
// "stop" the only button a working agent would show you.
|
|
1015
|
+
if (streamingRef.current || followingRef.current) {
|
|
1016
|
+
// Queued either way — the drain effect below is what actually sends it,
|
|
1017
|
+
// once the pane is free and its history includes whatever the running
|
|
1018
|
+
// turn wrote. The preference only decides whether we WAIT for that turn
|
|
1019
|
+
// or cut it short.
|
|
1020
|
+
const key = queueKeyRef.current || (
|
|
1021
|
+
opts.agentSlug
|
|
1022
|
+
? liveActivityKey(pid, opts.agentSlug)
|
|
1023
|
+
: threadActivityKey(pid, "web", new Date().toISOString().slice(0, 10))
|
|
1024
|
+
);
|
|
1025
|
+
bindQueue(key);
|
|
1026
|
+
writeBackgroundQueue(key, [
|
|
1027
|
+
...readBackgroundQueue(key),
|
|
1028
|
+
{ id: `q${++backgroundQueueSeq}`, text: trimmed, opts, msg: bubble() },
|
|
1029
|
+
]);
|
|
1030
|
+
// Interrupt, by default: writing while an agent works almost always
|
|
1031
|
+
// means "no, stop, do this instead", which is what a new message has
|
|
1032
|
+
// always done on Telegram. Whatever the turn had written stays in the
|
|
1033
|
+
// thread, so this lands as a redirection of work in progress rather
|
|
1034
|
+
// than a fresh start.
|
|
1035
|
+
if (!queueOnSendRef.current) void stopTurn();
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (!queueKeyRef.current) {
|
|
1040
|
+
bindQueue(
|
|
1041
|
+
opts.agentSlug
|
|
1042
|
+
? liveActivityKey(pid, opts.agentSlug)
|
|
1043
|
+
: threadActivityKey(pid, "web", new Date().toISOString().slice(0, 10)),
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
const history: ConversationMessage[] = msgsRef.current.map((m) => ({
|
|
357
1048
|
role: m.role,
|
|
358
1049
|
content: historyTextOf(m),
|
|
359
1050
|
}));
|
|
360
1051
|
|
|
361
|
-
|
|
1052
|
+
updateMsgs((curr) => [
|
|
362
1053
|
...curr,
|
|
363
|
-
|
|
364
|
-
{ role: "assistant", parts: [], ts: nowIso(), pending: true },
|
|
1054
|
+
bubble(),
|
|
1055
|
+
{ role: "assistant", parts: [], ts: nowIso(), pending: true, local: true },
|
|
365
1056
|
]);
|
|
366
|
-
|
|
1057
|
+
updateStreaming(true);
|
|
1058
|
+
// This request can outlive the selected pane. Keep its paint work tied
|
|
1059
|
+
// to the view it started from; navigation must never paste old tokens or
|
|
1060
|
+
// a typing state into another agent's history.
|
|
1061
|
+
const streamViewEpoch = viewEpochRef.current;
|
|
1062
|
+
const ownsView = () => viewEpochRef.current === streamViewEpoch;
|
|
367
1063
|
|
|
368
|
-
// ── Project agent:
|
|
1064
|
+
// ── Project agent: streamed NDJSON, token-by-token like the super-agent.
|
|
1065
|
+
// It used to be a single blocking call, which meant no "typing", no
|
|
1066
|
+
// word-by-word — the pane sat blank until the whole reply arrived at once,
|
|
1067
|
+
// and a slow model read as "nothing happened", so turns got re-sent. ─────
|
|
369
1068
|
if (opts.agentSlug) {
|
|
1069
|
+
const slug = opts.agentSlug;
|
|
1070
|
+
const ctrl = new AbortController();
|
|
1071
|
+
abortRef.current = ctrl;
|
|
1072
|
+
// Best guess until the turn's own `start` event names the conversation
|
|
1073
|
+
// — which it does before any work, so this only covers the microseconds
|
|
1074
|
+
// in between, and the first turn of a brand-new chat has nothing to name.
|
|
1075
|
+
turnTargetRef.current = convoRef.current ? { conversation_id: convoRef.current } : null;
|
|
370
1076
|
try {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
1077
|
+
await Agents.chatStream(
|
|
1078
|
+
pid,
|
|
1079
|
+
slug,
|
|
1080
|
+
{
|
|
1081
|
+
prompt: trimmed,
|
|
1082
|
+
conversation_id: convoRef.current,
|
|
1083
|
+
model: opts.model || undefined,
|
|
1084
|
+
channel: "web",
|
|
1085
|
+
// Paths only, same as the super-agent turn below: the bytes are on
|
|
1086
|
+
// disk and the daemon re-resolves each one inside ~/.apx/media. An
|
|
1087
|
+
// engine with vision renders images; one without still gets a marker
|
|
1088
|
+
// in the prompt naming the file and its path.
|
|
1089
|
+
...(opts.attachments?.length
|
|
1090
|
+
? { attachments: opts.attachments.map((a) => ({ path: a.path, name: a.name })) }
|
|
1091
|
+
: {}),
|
|
1092
|
+
},
|
|
1093
|
+
(ev) => {
|
|
1094
|
+
if (!ownsView()) return;
|
|
1095
|
+
// Bind this pane (and any later regenerate/edit rewind) to the file
|
|
1096
|
+
// the daemon appended to — it only names it on the closing event.
|
|
1097
|
+
if (ev.type === "final" && ev.result?.conversation_id) {
|
|
1098
|
+
convoRef.current = ev.result.conversation_id;
|
|
1099
|
+
setConversationId(ev.result.conversation_id);
|
|
1100
|
+
}
|
|
1101
|
+
// The face: a project agent's turns are all its own, so stamp the
|
|
1102
|
+
// slug the reducer's final event doesn't know to set as the id.
|
|
1103
|
+
patchLast((m) => (m.agentId ? m : { ...m, agentId: slug }));
|
|
1104
|
+
applyEvent(ev);
|
|
1105
|
+
},
|
|
1106
|
+
ctrl.signal,
|
|
1107
|
+
);
|
|
1108
|
+
if (ownsView()) patchLast((m) => ({ ...m, pending: false }));
|
|
388
1109
|
} catch (e) {
|
|
389
|
-
|
|
390
|
-
|
|
1110
|
+
if (ctrl.signal.aborted && ownsView()) {
|
|
1111
|
+
patchLast((m) => ({
|
|
1112
|
+
...m,
|
|
1113
|
+
pending: false,
|
|
1114
|
+
parts: [...m.parts, { kind: "text", text: t("code_module.stopped") }],
|
|
1115
|
+
}));
|
|
1116
|
+
} else if (ownsView()) {
|
|
1117
|
+
onError?.((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
1118
|
+
updateMsgs((curr) => curr.filter((_, i) => i !== curr.length - 1));
|
|
1119
|
+
}
|
|
391
1120
|
} finally {
|
|
392
|
-
|
|
1121
|
+
if (ownsView()) {
|
|
1122
|
+
updateStreaming(false);
|
|
1123
|
+
abortRef.current = null;
|
|
1124
|
+
turnTargetRef.current = null;
|
|
1125
|
+
queueMicrotask(() => drainQueueRef.current());
|
|
1126
|
+
}
|
|
393
1127
|
}
|
|
394
1128
|
return;
|
|
395
1129
|
}
|
|
@@ -397,78 +1131,221 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
|
|
|
397
1131
|
// ── Roby (super-agent): NDJSON event stream with tools. ────────────────
|
|
398
1132
|
const ctrl = new AbortController();
|
|
399
1133
|
abortRef.current = ctrl;
|
|
1134
|
+
// Roby's thread IS the channel — it has no conversation id — so that is
|
|
1135
|
+
// how the daemon addresses its live turn. See superAgentTurnKey.
|
|
1136
|
+
turnTargetRef.current = { channel: "web" };
|
|
400
1137
|
try {
|
|
401
1138
|
await SuperAgent.stream(
|
|
402
1139
|
pid,
|
|
403
|
-
{
|
|
404
|
-
|
|
1140
|
+
{
|
|
1141
|
+
prompt: trimmed,
|
|
1142
|
+
previousMessages: history,
|
|
1143
|
+
model: opts.model || undefined,
|
|
1144
|
+
channel: "web",
|
|
1145
|
+
// Paths only: the bytes are already on disk, and the daemon re-resolves
|
|
1146
|
+
// each one inside ~/.apx/media before it reads a single byte.
|
|
1147
|
+
...(opts.attachments?.length
|
|
1148
|
+
? { attachments: opts.attachments.map((a) => ({ path: a.path, name: a.name })) }
|
|
1149
|
+
: {}),
|
|
1150
|
+
},
|
|
1151
|
+
(ev) => { if (ownsView()) applyEvent(ev); },
|
|
405
1152
|
ctrl.signal,
|
|
406
1153
|
);
|
|
407
|
-
patchLast((m) => ({ ...m, pending: false }));
|
|
1154
|
+
if (ownsView()) patchLast((m) => ({ ...m, pending: false }));
|
|
408
1155
|
} catch (e) {
|
|
409
|
-
if (ctrl.signal.aborted) {
|
|
1156
|
+
if (ctrl.signal.aborted && ownsView()) {
|
|
410
1157
|
patchLast((m) => ({
|
|
411
1158
|
...m,
|
|
412
1159
|
pending: false,
|
|
413
1160
|
parts: [...m.parts, { kind: "text", text: t("code_module.stopped") }],
|
|
414
1161
|
}));
|
|
415
|
-
} else {
|
|
1162
|
+
} else if (ownsView()) {
|
|
416
1163
|
onError?.((e as Error)?.message || t("shared_ui.err_stream_failed"));
|
|
417
|
-
|
|
1164
|
+
updateMsgs((curr) => curr.filter((_, i) => i !== curr.length - 1));
|
|
418
1165
|
}
|
|
419
1166
|
} finally {
|
|
420
|
-
|
|
421
|
-
|
|
1167
|
+
if (ownsView()) {
|
|
1168
|
+
updateStreaming(false);
|
|
1169
|
+
abortRef.current = null;
|
|
1170
|
+
turnTargetRef.current = null;
|
|
1171
|
+
queueMicrotask(() => drainQueueRef.current());
|
|
1172
|
+
}
|
|
422
1173
|
}
|
|
423
1174
|
},
|
|
424
|
-
[pid,
|
|
1175
|
+
[pid, applyEvent, patchLast, onError, stopTurn, bindQueue, updateMsgs, updateStreaming],
|
|
425
1176
|
);
|
|
426
1177
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
1178
|
+
// Rewind the pane (and the bound file) to `keepVisible` turns, then send. The
|
|
1179
|
+
// file rewind matters for a project agent: the daemon rebuilds its history
|
|
1180
|
+
// from the conversation file, so without it the dropped turns would still be
|
|
1181
|
+
// there and the "regenerated" answer would see them. send() appends its bubble
|
|
1182
|
+
// to whatever msgs is after this slice, and — for a project agent — reads
|
|
1183
|
+
// history from the file, not its (stale) msgs closure, so the order is right.
|
|
1184
|
+
const rewindAndSend = useCallback(
|
|
1185
|
+
async (keepVisible: number, text: string, opts: SendOptions) => {
|
|
1186
|
+
if (streaming) return;
|
|
1187
|
+
if (convoRef.current && opts.agentSlug) {
|
|
1188
|
+
try {
|
|
1189
|
+
await Conversations.truncate(pid, opts.agentSlug, convoRef.current, keepVisible);
|
|
1190
|
+
} catch (e) {
|
|
1191
|
+
onError?.((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
updateMsgs((curr) => curr.slice(0, keepVisible));
|
|
1196
|
+
await send(text, opts);
|
|
1197
|
+
},
|
|
1198
|
+
[pid, streaming, send, onError, updateMsgs],
|
|
1199
|
+
);
|
|
1200
|
+
|
|
1201
|
+
const regenerate = useCallback(
|
|
1202
|
+
async (index: number, opts: SendOptions = {}) => {
|
|
1203
|
+
const target = msgs[index];
|
|
1204
|
+
if (!target || target.role !== "assistant") return;
|
|
1205
|
+
// The user turn that produced it: the nearest user message before it.
|
|
1206
|
+
let u = index - 1;
|
|
1207
|
+
while (u >= 0 && msgs[u].role !== "user") u--;
|
|
1208
|
+
if (u < 0) return;
|
|
1209
|
+
// Keep the turns before that user turn; re-send its text (+ any files) so
|
|
1210
|
+
// it (and a fresh answer) are appended again. Strip markers when files
|
|
1211
|
+
// ride along — send() / the daemon add them back.
|
|
1212
|
+
const userMsg = msgs[u];
|
|
1213
|
+
const files = attachmentsOf(userMsg);
|
|
1214
|
+
const raw = historyTextOf(userMsg);
|
|
1215
|
+
const text = files.length ? stripLeadingMarkers(raw, files.length) : raw;
|
|
1216
|
+
await rewindAndSend(u, text, {
|
|
1217
|
+
...opts,
|
|
1218
|
+
...(files.length ? { attachments: files } : {}),
|
|
1219
|
+
});
|
|
1220
|
+
},
|
|
1221
|
+
[msgs, rewindAndSend],
|
|
1222
|
+
);
|
|
1223
|
+
|
|
1224
|
+
const editAndResend = useCallback(
|
|
1225
|
+
async (index: number, text: string, opts: SendOptions = {}) => {
|
|
1226
|
+
const target = msgs[index];
|
|
1227
|
+
if (!target || target.role !== "user") return;
|
|
1228
|
+
// Keep the photo/file that was on the edited turn — only the caption
|
|
1229
|
+
// changes. Dropping media here is what made "edit text with photo" wipe
|
|
1230
|
+
// the image until a hard reload couldn't bring it back either.
|
|
1231
|
+
const files = attachmentsOf(target);
|
|
1232
|
+
await rewindAndSend(index, text, {
|
|
1233
|
+
...opts,
|
|
1234
|
+
...(files.length ? { attachments: files } : {}),
|
|
1235
|
+
});
|
|
1236
|
+
},
|
|
1237
|
+
[msgs, rewindAndSend],
|
|
1238
|
+
);
|
|
1239
|
+
|
|
1240
|
+
// Stop ends the turn being written; it does not cancel what you queued. That
|
|
1241
|
+
// pairing is the point of the two buttons — "this answer is going the wrong
|
|
1242
|
+
// way, here is what I actually meant": send the correction, stop the run, and
|
|
1243
|
+
// the correction leaves immediately instead of after the wrong answer.
|
|
1244
|
+
const stop = useCallback(() => { void stopTurn(); }, [stopTurn]);
|
|
1245
|
+
const unqueue = useCallback((id: string) => {
|
|
1246
|
+
const key = queueKeyRef.current;
|
|
1247
|
+
if (!key) return;
|
|
1248
|
+
writeBackgroundQueue(key, readBackgroundQueue(key).filter((q) => q.id !== id));
|
|
1249
|
+
}, []);
|
|
1250
|
+
|
|
1251
|
+
// Drain: one at a time, as soon as the pane is free.
|
|
1252
|
+
// msgsRef is synchronous, so the worker can drain after its pane unmounts and
|
|
1253
|
+
// still include the answer that just finished in the next turn's history.
|
|
1254
|
+
const sendRef = useRef(send);
|
|
1255
|
+
sendRef.current = send;
|
|
1256
|
+
const drainQueue = useCallback(() => {
|
|
1257
|
+
if (streamingRef.current || followingRef.current) return;
|
|
1258
|
+
const key = queueKeyRef.current;
|
|
1259
|
+
if (!key) return;
|
|
1260
|
+
const next = takeBackgroundQueue(key);
|
|
1261
|
+
if (next) void sendRef.current(next.text, next.opts);
|
|
1262
|
+
}, []);
|
|
1263
|
+
drainQueueRef.current = drainQueue;
|
|
1264
|
+
useEffect(() => {
|
|
1265
|
+
if (!streaming && !following && queued.length) drainQueue();
|
|
1266
|
+
}, [streaming, following, queued.length, drainQueue]);
|
|
1267
|
+
|
|
1268
|
+
const clear = useCallback((queueKey?: string) => {
|
|
430
1269
|
loadSeqRef.current++; // cancel any in-flight history load
|
|
1270
|
+
beginViewChange();
|
|
431
1271
|
convoRef.current = undefined;
|
|
1272
|
+
threadRef.current = null;
|
|
432
1273
|
setConversationId(undefined);
|
|
433
|
-
|
|
434
|
-
|
|
1274
|
+
setConversationMeta(undefined);
|
|
1275
|
+
updateFollowing(false);
|
|
1276
|
+
turnTargetRef.current = null;
|
|
1277
|
+
if (queueKey) bindQueue(queueKey);
|
|
1278
|
+
else setQueued([]);
|
|
1279
|
+
updateMsgs([]);
|
|
1280
|
+
}, [beginViewChange, bindQueue, updateFollowing, updateMsgs]);
|
|
435
1281
|
|
|
436
1282
|
const load = useCallback(
|
|
437
|
-
async (agentSlug: string, conversationId: string) => {
|
|
438
|
-
if (streaming) return;
|
|
1283
|
+
async (agentSlug: string, conversationId: string, opts?: ReloadOptions) => {
|
|
439
1284
|
const seq = ++loadSeqRef.current;
|
|
1285
|
+
const activityKey = conversationActivityKey(pid, conversationId);
|
|
1286
|
+
if (!opts?.silent) {
|
|
1287
|
+
beginViewChange();
|
|
1288
|
+
bindQueue(activityKey);
|
|
1289
|
+
}
|
|
440
1290
|
// Blank the pane up front so it never shows the previous chat under the
|
|
441
|
-
// new header while the fetch is in flight.
|
|
442
|
-
|
|
1291
|
+
// new header while the fetch is in flight. A silent re-read is the SAME
|
|
1292
|
+
// chat catching up, so blanking would be a flash of empty for nothing —
|
|
1293
|
+
// and for the same reason it keeps the queue: what you queued belongs to
|
|
1294
|
+
// this thread, and only actually LEAVING it drops it.
|
|
1295
|
+
if (!opts?.silent) updateMsgs([]);
|
|
443
1296
|
try {
|
|
444
1297
|
const detail = await Conversations.get(pid, agentSlug, conversationId);
|
|
445
1298
|
if (seq !== loadSeqRef.current) return; // superseded by a newer pick
|
|
446
|
-
const loaded
|
|
447
|
-
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
448
|
-
.map((m) => ({
|
|
449
|
-
role: m.role as "user" | "assistant",
|
|
450
|
-
parts: [{ kind: "text", text: m.content }],
|
|
451
|
-
ts: m.ts || new Date().toISOString(),
|
|
452
|
-
}));
|
|
1299
|
+
const loaded = threadToChatMsgs(detail.messages ?? []);
|
|
453
1300
|
convoRef.current = conversationId;
|
|
1301
|
+
threadRef.current = null;
|
|
454
1302
|
setConversationId(conversationId);
|
|
455
|
-
|
|
1303
|
+
setConversationMeta(metaFromDetail(detail));
|
|
1304
|
+
// Opening a chat whose answer is still being written: show the partial
|
|
1305
|
+
// as a streaming bubble and let the live "turn" frames carry on filling
|
|
1306
|
+
// it — the whole point of the push feed. Only on a real open; a silent
|
|
1307
|
+
// refetch leaves the live bubble the frames are already maintaining.
|
|
1308
|
+
const active = !opts?.silent && !isChatTurnClosed(detail.active_turn?.turn_id)
|
|
1309
|
+
? detail.active_turn
|
|
1310
|
+
: null;
|
|
1311
|
+
if (active) {
|
|
1312
|
+
liveTurnRef.current = { id: active.turn_id, text: active.text || "" };
|
|
1313
|
+
updateFollowing(true);
|
|
1314
|
+
turnTargetRef.current = { conversation_id: conversationId };
|
|
1315
|
+
} else if (!opts?.silent) {
|
|
1316
|
+
liveTurnRef.current = null;
|
|
1317
|
+
updateFollowing(false);
|
|
1318
|
+
turnTargetRef.current = null;
|
|
1319
|
+
}
|
|
1320
|
+
updateMsgs((curr) =>
|
|
1321
|
+
opts?.silent ? mergeLocalTurns(loaded, curr) : withActiveTurn(loaded, active),
|
|
1322
|
+
);
|
|
456
1323
|
} catch (e) {
|
|
457
1324
|
if (seq !== loadSeqRef.current) return;
|
|
1325
|
+
// A background refresh that failed is a refresh that did not happen:
|
|
1326
|
+
// keep showing the conversation and stay quiet. Only a refresh the user
|
|
1327
|
+
// asked for by picking a chat reports, and clears.
|
|
1328
|
+
if (opts?.silent) return;
|
|
458
1329
|
convoRef.current = undefined;
|
|
1330
|
+
threadRef.current = null;
|
|
459
1331
|
setConversationId(undefined);
|
|
460
|
-
|
|
1332
|
+
setConversationMeta(undefined);
|
|
1333
|
+
updateFollowing(false);
|
|
1334
|
+
updateMsgs([]);
|
|
461
1335
|
onError?.((e as Error)?.message || t("shared_ui.err_load_conversation"));
|
|
462
1336
|
}
|
|
463
1337
|
},
|
|
464
|
-
[pid,
|
|
1338
|
+
[pid, onError, beginViewChange, bindQueue, updateFollowing, updateMsgs],
|
|
465
1339
|
);
|
|
466
1340
|
|
|
467
1341
|
const loadThread = useCallback(
|
|
468
|
-
async (channel: string, threadId: string) => {
|
|
469
|
-
if (streaming) return;
|
|
1342
|
+
async (channel: string, threadId: string, opts?: ReloadOptions) => {
|
|
470
1343
|
const seq = ++loadSeqRef.current;
|
|
471
|
-
|
|
1344
|
+
if (!opts?.silent) {
|
|
1345
|
+
beginViewChange();
|
|
1346
|
+
bindQueue(threadActivityKey(pid, channel, threadId));
|
|
1347
|
+
updateMsgs([]);
|
|
1348
|
+
}
|
|
472
1349
|
try {
|
|
473
1350
|
const detail = await Conversations.thread(pid, channel, threadId);
|
|
474
1351
|
if (seq !== loadSeqRef.current) return; // superseded by a newer pick
|
|
@@ -476,18 +1353,148 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
|
|
|
476
1353
|
// Ledger threads have no conversation file — sends continue as fresh
|
|
477
1354
|
// web turns with this history as previousMessages.
|
|
478
1355
|
convoRef.current = undefined;
|
|
1356
|
+
threadRef.current = { channel, id: threadId };
|
|
479
1357
|
setConversationId(undefined);
|
|
480
|
-
|
|
1358
|
+
// A thread knows its own name and channel, same as a conversation file
|
|
1359
|
+
// does — the header should not have to be handed them by whichever list
|
|
1360
|
+
// happened to open it.
|
|
1361
|
+
setConversationMeta({
|
|
1362
|
+
channel: detail.channel,
|
|
1363
|
+
title: detail.title,
|
|
1364
|
+
participants: detail.participants,
|
|
1365
|
+
faces: detail.participant_faces,
|
|
1366
|
+
});
|
|
1367
|
+
const active = !opts?.silent && !isChatTurnClosed(detail.active_turn?.turn_id)
|
|
1368
|
+
? detail.active_turn
|
|
1369
|
+
: null;
|
|
1370
|
+
if (active) {
|
|
1371
|
+
liveTurnRef.current = { id: active.turn_id, text: active.text || "" };
|
|
1372
|
+
updateFollowing(true);
|
|
1373
|
+
turnTargetRef.current = { channel };
|
|
1374
|
+
} else if (!opts?.silent) {
|
|
1375
|
+
liveTurnRef.current = null;
|
|
1376
|
+
updateFollowing(false);
|
|
1377
|
+
turnTargetRef.current = null;
|
|
1378
|
+
}
|
|
1379
|
+
updateMsgs((curr) => (
|
|
1380
|
+
opts?.silent
|
|
1381
|
+
? mergeLocalTurns(loaded, curr)
|
|
1382
|
+
: withActiveTurn(loaded, active)
|
|
1383
|
+
));
|
|
481
1384
|
} catch (e) {
|
|
482
1385
|
if (seq !== loadSeqRef.current) return;
|
|
1386
|
+
if (opts?.silent) return; // see load(): a failed catch-up changes nothing
|
|
483
1387
|
convoRef.current = undefined;
|
|
1388
|
+
threadRef.current = null;
|
|
484
1389
|
setConversationId(undefined);
|
|
485
|
-
|
|
1390
|
+
updateFollowing(false);
|
|
1391
|
+
updateMsgs([]);
|
|
486
1392
|
onError?.((e as Error)?.message || t("shared_ui.err_load_conversation"));
|
|
487
1393
|
}
|
|
488
1394
|
},
|
|
489
|
-
[pid,
|
|
1395
|
+
[pid, onError, beginViewChange, bindQueue, updateFollowing, updateMsgs],
|
|
1396
|
+
);
|
|
1397
|
+
|
|
1398
|
+
// A group turn streams like a 1:1 one — same pending-bubble machinery — but the
|
|
1399
|
+
// reply comes as a CASCADE of speakers on the group endpoint. Each speaker gets
|
|
1400
|
+
// its own streaming assistant bubble (name + "traído por X"), tokens land as
|
|
1401
|
+
// they are written, and the "… está escribiendo" pill rides on the pending
|
|
1402
|
+
// bubble exactly where a normal turn's does. `rerun` resumes from one speaker
|
|
1403
|
+
// against the last owner message (regenerate) instead of appending a new one.
|
|
1404
|
+
const sendGroup = useCallback(
|
|
1405
|
+
async (gid: string, text: string, nameOf: (slug: string) => string, opts: { rerun?: boolean; from?: string; reason?: string | null; media?: UploadedMedia[] } = {}) => {
|
|
1406
|
+
const trimmed = text.trim();
|
|
1407
|
+
const files = opts.media || [];
|
|
1408
|
+
if (streaming) return;
|
|
1409
|
+
if (!opts.rerun && !trimmed && !files.length) return;
|
|
1410
|
+
const nowIso = () => new Date().toISOString();
|
|
1411
|
+
if (!opts.rerun) {
|
|
1412
|
+
// Optimistic owner bubble — shows the photo/file immediately, same as a
|
|
1413
|
+
// 1:1 turn; the marker rides on the text the way the daemon folds it.
|
|
1414
|
+
updateMsgs((curr) => [...curr, {
|
|
1415
|
+
role: "user",
|
|
1416
|
+
parts: userPart([markersFor(files), trimmed].filter(Boolean).join(" ")),
|
|
1417
|
+
ts: nowIso(), local: true,
|
|
1418
|
+
...(files.length ? { media: files.map(mediaOf) } : {}),
|
|
1419
|
+
}]);
|
|
1420
|
+
}
|
|
1421
|
+
updateStreaming(true);
|
|
1422
|
+
const ctrl = new AbortController();
|
|
1423
|
+
abortRef.current = ctrl;
|
|
1424
|
+
// How the DAEMON addresses this cascade, set before the first byte: a room
|
|
1425
|
+
// is a thread on channel "group", and a project runs several at once, so
|
|
1426
|
+
// the id has to ride along. Without it Stop only closed this socket while
|
|
1427
|
+
// the room kept working — the one turn shape that can run ten tool loops
|
|
1428
|
+
// off a single line was the one that could not be interrupted.
|
|
1429
|
+
turnTargetRef.current = { channel: "group", thread_id: gid };
|
|
1430
|
+
const onEvent = (ev: import("../lib/api/groups").GroupStreamEvent) => {
|
|
1431
|
+
if (ev.type === "speaker_start") {
|
|
1432
|
+
updateMsgs((curr) => [...curr, {
|
|
1433
|
+
role: "assistant", parts: [], ts: nowIso(), pending: true, local: true,
|
|
1434
|
+
agentId: ev.slug, agent: nameOf(ev.slug),
|
|
1435
|
+
...(ev.reason ? { reason: ev.reason } : {}),
|
|
1436
|
+
}]);
|
|
1437
|
+
} else if (ev.type === "speaker_delta") {
|
|
1438
|
+
// Reuse the exact token-merge the 1:1 stream uses.
|
|
1439
|
+
applyEvent({ type: "assistant_delta", delta: ev.delta } as ChatStreamEvent);
|
|
1440
|
+
} else if (
|
|
1441
|
+
ev.type === "tool_start" ||
|
|
1442
|
+
ev.type === "tool_result" ||
|
|
1443
|
+
ev.type === "tool_deduped" ||
|
|
1444
|
+
ev.type === "assistant_text" ||
|
|
1445
|
+
ev.type === "model_start" ||
|
|
1446
|
+
ev.type === "model_routed" ||
|
|
1447
|
+
ev.type === "engine_failed" ||
|
|
1448
|
+
ev.type === "model_retry" ||
|
|
1449
|
+
ev.type === "tools_suppressed" ||
|
|
1450
|
+
ev.type === "skill_inspector" ||
|
|
1451
|
+
ev.type === "reasoning"
|
|
1452
|
+
) {
|
|
1453
|
+
applyEvent(ev as ChatStreamEvent);
|
|
1454
|
+
} else if (ev.type === "speaker_final") {
|
|
1455
|
+
patchLast((m) => ({ ...m, pending: false, ...(ev.model ? { model: ev.model } : {}), ...(ev.usage ? { usage: ev.usage } : {}) }));
|
|
1456
|
+
} else if (ev.type === "speaker_aborted") {
|
|
1457
|
+
// You stopped the room mid-reply. Whatever this speaker had written is
|
|
1458
|
+
// real work you watched happen and the daemon kept it in the thread —
|
|
1459
|
+
// mark it the way a stopped 1:1 turn is marked, not as a failure.
|
|
1460
|
+
patchLast((m) => ({ ...m, pending: false, parts: [...m.parts, { kind: "text", text: t("code_module.stopped") }] }));
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
try {
|
|
1464
|
+
if (opts.rerun) await Groups.rerunStream(pid, gid, onEvent, ctrl.signal,
|
|
1465
|
+
opts.from ? { from: opts.from, reason: opts.reason } : undefined);
|
|
1466
|
+
else await Groups.sendStream(pid, gid, trimmed, onEvent, ctrl.signal,
|
|
1467
|
+
files.map((a) => ({ path: a.path, name: a.name })));
|
|
1468
|
+
patchLast((m) => ({ ...m, pending: false }));
|
|
1469
|
+
} catch (e) {
|
|
1470
|
+
if (ctrl.signal.aborted) patchLast((m) => ({ ...m, pending: false }));
|
|
1471
|
+
else onError?.((e as Error)?.message || t("shared_ui.err_chat_failed"));
|
|
1472
|
+
} finally {
|
|
1473
|
+
updateStreaming(false);
|
|
1474
|
+
abortRef.current = null;
|
|
1475
|
+
turnTargetRef.current = null;
|
|
1476
|
+
// Reconcile with the canonical thread (model/usage/reason from the ledger,
|
|
1477
|
+
// join/leave notices, and drops the local bubbles for the persisted ones).
|
|
1478
|
+
void loadThread("group", gid, { silent: true });
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
[pid, streaming, applyEvent, patchLast, loadThread, onError, updateMsgs, updateStreaming],
|
|
490
1482
|
);
|
|
491
1483
|
|
|
492
|
-
return {
|
|
1484
|
+
return {
|
|
1485
|
+
msgs,
|
|
1486
|
+
send,
|
|
1487
|
+
sendGroup,
|
|
1488
|
+
regenerate,
|
|
1489
|
+
editAndResend,
|
|
1490
|
+
stop,
|
|
1491
|
+
clear,
|
|
1492
|
+
load,
|
|
1493
|
+
loadThread,
|
|
1494
|
+
streaming: streaming || following,
|
|
1495
|
+
queued,
|
|
1496
|
+
unqueue,
|
|
1497
|
+
conversationId,
|
|
1498
|
+
conversationMeta,
|
|
1499
|
+
};
|
|
493
1500
|
}
|