clawmini 0.0.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/.gemini/settings.json +46 -0
- package/.prettierrc +7 -0
- package/GEMINI.md +11 -0
- package/README.md +137 -0
- package/dist/adapter-discord/index.d.mts +5 -0
- package/dist/adapter-discord/index.d.mts.map +1 -0
- package/dist/adapter-discord/index.mjs +456 -0
- package/dist/adapter-discord/index.mjs.map +1 -0
- package/dist/chats-DKgTeU7i.mjs +91 -0
- package/dist/chats-DKgTeU7i.mjs.map +1 -0
- package/dist/chats-Zd_HXDHx.mjs +29 -0
- package/dist/chats-Zd_HXDHx.mjs.map +1 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +850 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/cli/lite.d.mts +1 -0
- package/dist/cli/lite.mjs +4434 -0
- package/dist/cli/lite.mjs.map +1 -0
- package/dist/daemon/index.d.mts +5 -0
- package/dist/daemon/index.d.mts.map +1 -0
- package/dist/daemon/index.mjs +1222 -0
- package/dist/daemon/index.mjs.map +1 -0
- package/dist/fetch-BjZVyU3Z.mjs +37 -0
- package/dist/fetch-BjZVyU3Z.mjs.map +1 -0
- package/dist/fs-B5wW0oaH.mjs +14 -0
- package/dist/fs-B5wW0oaH.mjs.map +1 -0
- package/dist/lite-Dl7WXyaH.mjs +80 -0
- package/dist/lite-Dl7WXyaH.mjs.map +1 -0
- package/dist/rolldown-runtime-95iHPtFO.mjs +18 -0
- package/dist/web/_app/env.js +1 -0
- package/dist/web/_app/immutable/assets/0.GI4C4dpV.css +1 -0
- package/dist/web/_app/immutable/chunks/B5abRDXp.js +1 -0
- package/dist/web/_app/immutable/chunks/B8yYFADm.js +1 -0
- package/dist/web/_app/immutable/chunks/BPy8HLo7.js +5 -0
- package/dist/web/_app/immutable/chunks/Bi0jeV7Q.js +1 -0
- package/dist/web/_app/immutable/chunks/BmUXQ3wy.js +2 -0
- package/dist/web/_app/immutable/chunks/C3k55nDF.js +1 -0
- package/dist/web/_app/immutable/chunks/COekwvP2.js +1 -0
- package/dist/web/_app/immutable/chunks/CSvS_NwK.js +1 -0
- package/dist/web/_app/immutable/chunks/CpaGRn9L.js +1 -0
- package/dist/web/_app/immutable/chunks/CyNaE55B.js +1 -0
- package/dist/web/_app/immutable/chunks/DG5RZBw-.js +2 -0
- package/dist/web/_app/immutable/chunks/Dc-UOHw9.js +1 -0
- package/dist/web/_app/immutable/chunks/DcrmIfTj.js +1 -0
- package/dist/web/_app/immutable/chunks/ZkLyk0mE.js +1 -0
- package/dist/web/_app/immutable/entry/app.B-vZe7PN.js +2 -0
- package/dist/web/_app/immutable/entry/start.oP1AgKhs.js +1 -0
- package/dist/web/_app/immutable/nodes/0.B5WFN0zw.js +1 -0
- package/dist/web/_app/immutable/nodes/1.D1wtJb2k.js +1 -0
- package/dist/web/_app/immutable/nodes/2.CK3CLC0f.js +1 -0
- package/dist/web/_app/immutable/nodes/3.BB5wCoBf.js +4 -0
- package/dist/web/_app/immutable/nodes/4.Dr2jvAXK.js +1 -0
- package/dist/web/_app/immutable/nodes/5.BJl7oM3b.js +1 -0
- package/dist/web/_app/version.json +1 -0
- package/dist/web/index.html +37 -0
- package/dist/web/robots.txt +3 -0
- package/dist/workspace-CSgfo_2J.mjs +383 -0
- package/dist/workspace-CSgfo_2J.mjs.map +1 -0
- package/docs/01_chats/development_log.md +36 -0
- package/docs/01_chats/notes.md +27 -0
- package/docs/01_chats/prd.md +47 -0
- package/docs/01_chats/questions.md +19 -0
- package/docs/01_chats/tickets.md +67 -0
- package/docs/02_sessions/development_log.md +79 -0
- package/docs/02_sessions/notes.md +40 -0
- package/docs/02_sessions/prd.md +75 -0
- package/docs/02_sessions/questions.md +7 -0
- package/docs/02_sessions/tickets.md +68 -0
- package/docs/03_web_interface/development_log.md +60 -0
- package/docs/03_web_interface/notes.md +29 -0
- package/docs/03_web_interface/prd.md +42 -0
- package/docs/03_web_interface/questions.md +8 -0
- package/docs/03_web_interface/tickets.md +59 -0
- package/docs/04_agents/development_log.md +54 -0
- package/docs/04_agents/notes.md +45 -0
- package/docs/04_agents/prd.md +47 -0
- package/docs/04_agents/questions.md +13 -0
- package/docs/04_agents/tickets.md +107 -0
- package/docs/05_routers/development_log.md +13 -0
- package/docs/05_routers/notes.md +40 -0
- package/docs/05_routers/prd.md +55 -0
- package/docs/05_routers/questions.md +21 -0
- package/docs/05_routers/tickets.md +109 -0
- package/docs/06_agent_templates/development_log.md +38 -0
- package/docs/06_agent_templates/notes.md +25 -0
- package/docs/06_agent_templates/prd.md +34 -0
- package/docs/06_agent_templates/questions.md +11 -0
- package/docs/06_agent_templates/tickets.md +49 -0
- package/docs/06_cron/development_log.md +51 -0
- package/docs/06_cron/notes.md +14 -0
- package/docs/06_cron/prd.md +92 -0
- package/docs/06_cron/questions.md +15 -0
- package/docs/06_cron/tickets.md +75 -0
- package/docs/07_web_chat_ux/development_log.md +30 -0
- package/docs/07_web_chat_ux/notes.md +25 -0
- package/docs/07_web_chat_ux/prd.md +46 -0
- package/docs/07_web_chat_ux/questions.md +7 -0
- package/docs/07_web_chat_ux/tickets.md +48 -0
- package/docs/08_agent_api/development_log.md +52 -0
- package/docs/08_agent_api/notes.md +31 -0
- package/docs/08_agent_api/prd.md +56 -0
- package/docs/08_agent_api/questions.md +14 -0
- package/docs/08_agent_api/tickets.md +104 -0
- package/docs/09_agent_fallbacks/development_log.md +52 -0
- package/docs/09_agent_fallbacks/notes.md +40 -0
- package/docs/09_agent_fallbacks/prd.md +55 -0
- package/docs/09_agent_fallbacks/questions.md +10 -0
- package/docs/09_agent_fallbacks/tickets.md +88 -0
- package/docs/09_discord_adapter/development_log.md +95 -0
- package/docs/09_discord_adapter/notes.md +18 -0
- package/docs/09_discord_adapter/prd.md +57 -0
- package/docs/09_discord_adapter/questions.md +16 -0
- package/docs/09_discord_adapter/tickets.md +116 -0
- package/docs/10_file_attachments/development_log.md +55 -0
- package/docs/10_file_attachments/notes.md +59 -0
- package/docs/10_file_attachments/prd.md +73 -0
- package/docs/10_file_attachments/questions.md +15 -0
- package/docs/10_file_attachments/tickets.md +88 -0
- package/docs/11_message_verbosity/development_log.md +43 -0
- package/docs/11_message_verbosity/notes.md +26 -0
- package/docs/11_message_verbosity/prd.md +44 -0
- package/docs/11_message_verbosity/questions.md +8 -0
- package/docs/11_message_verbosity/tickets.md +33 -0
- package/docs/12_environments/development_log.md +43 -0
- package/docs/12_environments/notes.md +45 -0
- package/docs/12_environments/prd.md +113 -0
- package/docs/12_environments/questions.md +17 -0
- package/docs/12_environments/tickets.md +87 -0
- package/docs/12_setup_flow_improvements/development_log.md +40 -0
- package/docs/12_setup_flow_improvements/notes.md +34 -0
- package/docs/12_setup_flow_improvements/prd.md +35 -0
- package/docs/12_setup_flow_improvements/questions.md +8 -0
- package/docs/12_setup_flow_improvements/tickets.md +122 -0
- package/docs/13_discord_typing_indicators/development_log.md +38 -0
- package/docs/13_discord_typing_indicators/notes.md +18 -0
- package/docs/13_discord_typing_indicators/prd.md +41 -0
- package/docs/13_discord_typing_indicators/questions.md +6 -0
- package/docs/13_discord_typing_indicators/tickets.md +60 -0
- package/docs/14_interruptions/development_log.md +50 -0
- package/docs/14_interruptions/notes.md +38 -0
- package/docs/14_interruptions/prd.md +46 -0
- package/docs/14_interruptions/questions.md +12 -0
- package/docs/14_interruptions/tickets.md +69 -0
- package/docs/15_sandbox_policies/development_log.md +95 -0
- package/docs/15_sandbox_policies/notes.md +33 -0
- package/docs/15_sandbox_policies/prd.md +163 -0
- package/docs/15_sandbox_policies/questions.md +10 -0
- package/docs/15_sandbox_policies/tickets.md +196 -0
- package/docs/CHECKS.md +9 -0
- package/docs/guides/discord_adapter_setup.md +69 -0
- package/docs/guides/sandbox_policies.md +76 -0
- package/eslint.config.js +47 -0
- package/napkin.md +21 -0
- package/package.json +50 -0
- package/scripts/create_worktree.sh +49 -0
- package/scripts/get_pr_comments.sh +36 -0
- package/src/adapter-discord/client.test.ts +65 -0
- package/src/adapter-discord/client.ts +41 -0
- package/src/adapter-discord/config.test.ts +156 -0
- package/src/adapter-discord/config.ts +61 -0
- package/src/adapter-discord/forwarder.test.ts +493 -0
- package/src/adapter-discord/forwarder.ts +246 -0
- package/src/adapter-discord/index.test.ts +399 -0
- package/src/adapter-discord/index.ts +147 -0
- package/src/adapter-discord/state.test.ts +65 -0
- package/src/adapter-discord/state.ts +44 -0
- package/src/cli/client.ts +46 -0
- package/src/cli/commands/agents.ts +138 -0
- package/src/cli/commands/chats.ts +79 -0
- package/src/cli/commands/down.ts +32 -0
- package/src/cli/commands/environments.ts +39 -0
- package/src/cli/commands/export-lite.ts +62 -0
- package/src/cli/commands/init.ts +79 -0
- package/src/cli/commands/jobs.ts +141 -0
- package/src/cli/commands/messages.ts +103 -0
- package/src/cli/commands/up.ts +26 -0
- package/src/cli/commands/web-api/agents.ts +138 -0
- package/src/cli/commands/web-api/chats.ts +213 -0
- package/src/cli/commands/web-api/utils.ts +27 -0
- package/src/cli/commands/web.ts +105 -0
- package/src/cli/e2e/adapter-discord.test.ts +76 -0
- package/src/cli/e2e/agents.test.ts +140 -0
- package/src/cli/e2e/basic.test.ts +43 -0
- package/src/cli/e2e/cron.test.ts +132 -0
- package/src/cli/e2e/daemon.test.ts +293 -0
- package/src/cli/e2e/environments.test.ts +66 -0
- package/src/cli/e2e/export-lite-func.test.ts +155 -0
- package/src/cli/e2e/export-lite.test.ts +51 -0
- package/src/cli/e2e/fallbacks.test.ts +169 -0
- package/src/cli/e2e/global-setup.ts +15 -0
- package/src/cli/e2e/init.test.ts +70 -0
- package/src/cli/e2e/messages.test.ts +294 -0
- package/src/cli/e2e/requests.test.ts +165 -0
- package/src/cli/e2e/utils.ts +66 -0
- package/src/cli/index.test.ts +7 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/lite.ts +247 -0
- package/src/cli/utils.ts +4 -0
- package/src/daemon/auth.test.ts +50 -0
- package/src/daemon/auth.ts +69 -0
- package/src/daemon/chats.ts +26 -0
- package/src/daemon/cron.test.ts +28 -0
- package/src/daemon/cron.ts +159 -0
- package/src/daemon/events.ts +15 -0
- package/src/daemon/index.ts +212 -0
- package/src/daemon/message-agent.test.ts +132 -0
- package/src/daemon/message-extraction.test.ts +166 -0
- package/src/daemon/message-fallbacks.test.ts +313 -0
- package/src/daemon/message-interruption.test.ts +125 -0
- package/src/daemon/message-queue.test.ts +143 -0
- package/src/daemon/message-router.test.ts +106 -0
- package/src/daemon/message-session.test.ts +127 -0
- package/src/daemon/message-test-utils.ts +41 -0
- package/src/daemon/message-typing.test.ts +93 -0
- package/src/daemon/message-verbosity.test.ts +127 -0
- package/src/daemon/message.ts +600 -0
- package/src/daemon/observation.test.ts +118 -0
- package/src/daemon/policy-request-service.test.ts +87 -0
- package/src/daemon/policy-request-service.ts +62 -0
- package/src/daemon/policy-utils.test.ts +138 -0
- package/src/daemon/policy-utils.ts +152 -0
- package/src/daemon/queue.test.ts +89 -0
- package/src/daemon/queue.ts +87 -0
- package/src/daemon/request-store.test.ts +103 -0
- package/src/daemon/request-store.ts +96 -0
- package/src/daemon/router-policy-request.test.ts +99 -0
- package/src/daemon/router.test.ts +380 -0
- package/src/daemon/router.ts +510 -0
- package/src/daemon/routers/slash-command.test.ts +145 -0
- package/src/daemon/routers/slash-command.ts +58 -0
- package/src/daemon/routers/slash-interrupt.test.ts +30 -0
- package/src/daemon/routers/slash-interrupt.ts +7 -0
- package/src/daemon/routers/slash-new.test.ts +59 -0
- package/src/daemon/routers/slash-new.ts +14 -0
- package/src/daemon/routers/slash-policies.test.ts +167 -0
- package/src/daemon/routers/slash-policies.ts +131 -0
- package/src/daemon/routers/slash-stop.test.ts +30 -0
- package/src/daemon/routers/slash-stop.ts +3 -0
- package/src/daemon/routers/types.ts +10 -0
- package/src/daemon/routers/utils.ts +22 -0
- package/src/daemon/routers.test.ts +141 -0
- package/src/daemon/routers.ts +115 -0
- package/src/daemon/utils/spawn.ts +61 -0
- package/src/shared/agent-utils.ts +30 -0
- package/src/shared/chats.test.ts +112 -0
- package/src/shared/chats.ts +164 -0
- package/src/shared/config.test.ts +90 -0
- package/src/shared/config.ts +100 -0
- package/src/shared/event-source.ts +121 -0
- package/src/shared/fetch.ts +45 -0
- package/src/shared/lite.ts +129 -0
- package/src/shared/policies.ts +24 -0
- package/src/shared/utils/env.ts +27 -0
- package/src/shared/utils/fs.ts +13 -0
- package/src/shared/workspace.test.ts +345 -0
- package/src/shared/workspace.ts +500 -0
- package/templates/environments/cladding/env.json +7 -0
- package/templates/environments/macos/env.json +8 -0
- package/templates/environments/macos/sandbox.sb +21 -0
- package/templates/environments/macos-proxy/allowlist.txt +1 -0
- package/templates/environments/macos-proxy/env.json +14 -0
- package/templates/environments/macos-proxy/proxy.mjs +86 -0
- package/templates/environments/macos-proxy/sandbox.sb +34 -0
- package/templates/gemini/settings.json +11 -0
- package/templates/gemini-claw/.gemini/hooks/clawmini-logging.sh +17 -0
- package/templates/gemini-claw/.gemini/settings.json +24 -0
- package/templates/gemini-claw/.gemini/skills/clawmini-jobs/SKILL.md +40 -0
- package/templates/gemini-claw/.gemini/system.md +98 -0
- package/templates/gemini-claw/BOOTSTRAP.md +54 -0
- package/templates/gemini-claw/GEMINI.md +107 -0
- package/templates/gemini-claw/HEARTBEAT.md +3 -0
- package/templates/gemini-claw/MEMORY.md +2 -0
- package/templates/gemini-claw/SOUL.md +42 -0
- package/templates/gemini-claw/TOOLS.md +38 -0
- package/templates/gemini-claw/USER.md +15 -0
- package/templates/gemini-claw/memory/.gitkeep +0 -0
- package/templates/gemini-claw/settings.json +24 -0
- package/templates/opencode/settings.json +11 -0
- package/tsconfig.json +42 -0
- package/tsdown.config.ts +19 -0
- package/vitest.config.ts +9 -0
- package/web/.svelte-kit/ambient.d.ts +382 -0
- package/web/.svelte-kit/generated/client/app.js +35 -0
- package/web/.svelte-kit/generated/client/matchers.js +1 -0
- package/web/.svelte-kit/generated/client/nodes/0.js +3 -0
- package/web/.svelte-kit/generated/client/nodes/1.js +1 -0
- package/web/.svelte-kit/generated/client/nodes/2.js +1 -0
- package/web/.svelte-kit/generated/client/nodes/3.js +1 -0
- package/web/.svelte-kit/generated/client/nodes/4.js +3 -0
- package/web/.svelte-kit/generated/client/nodes/5.js +3 -0
- package/web/.svelte-kit/generated/client-optimized/app.js +35 -0
- package/web/.svelte-kit/generated/client-optimized/matchers.js +1 -0
- package/web/.svelte-kit/generated/client-optimized/nodes/0.js +3 -0
- package/web/.svelte-kit/generated/client-optimized/nodes/1.js +1 -0
- package/web/.svelte-kit/generated/client-optimized/nodes/2.js +1 -0
- package/web/.svelte-kit/generated/client-optimized/nodes/3.js +1 -0
- package/web/.svelte-kit/generated/client-optimized/nodes/4.js +3 -0
- package/web/.svelte-kit/generated/client-optimized/nodes/5.js +3 -0
- package/web/.svelte-kit/generated/root.js +3 -0
- package/web/.svelte-kit/generated/root.svelte +68 -0
- package/web/.svelte-kit/generated/server/internal.js +53 -0
- package/web/.svelte-kit/non-ambient.d.ts +46 -0
- package/web/.svelte-kit/output/client/.vite/manifest.json +251 -0
- package/web/.svelte-kit/output/client/_app/immutable/assets/0.GI4C4dpV.css +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B5abRDXp.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/B8yYFADm.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BPy8HLo7.js +5 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Bi0jeV7Q.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/BmUXQ3wy.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/C3k55nDF.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/COekwvP2.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CSvS_NwK.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CpaGRn9L.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/CyNaE55B.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DG5RZBw-.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/Dc-UOHw9.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/DcrmIfTj.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/chunks/ZkLyk0mE.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/entry/app.B-vZe7PN.js +2 -0
- package/web/.svelte-kit/output/client/_app/immutable/entry/start.oP1AgKhs.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/0.B5WFN0zw.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/1.D1wtJb2k.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/2.CK3CLC0f.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/3.BB5wCoBf.js +4 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/4.Dr2jvAXK.js +1 -0
- package/web/.svelte-kit/output/client/_app/immutable/nodes/5.BJl7oM3b.js +1 -0
- package/web/.svelte-kit/output/client/_app/version.json +1 -0
- package/web/.svelte-kit/output/client/robots.txt +3 -0
- package/web/.svelte-kit/output/prerendered/dependencies/_app/env.js +1 -0
- package/web/.svelte-kit/output/server/.vite/manifest.json +215 -0
- package/web/.svelte-kit/output/server/_app/immutable/assets/_layout.GI4C4dpV.css +1 -0
- package/web/.svelte-kit/output/server/chunks/Icon.js +153 -0
- package/web/.svelte-kit/output/server/chunks/bot.js +2753 -0
- package/web/.svelte-kit/output/server/chunks/client.js +47 -0
- package/web/.svelte-kit/output/server/chunks/environment.js +34 -0
- package/web/.svelte-kit/output/server/chunks/exports.js +231 -0
- package/web/.svelte-kit/output/server/chunks/false.js +4 -0
- package/web/.svelte-kit/output/server/chunks/index-server.js +20 -0
- package/web/.svelte-kit/output/server/chunks/index.js +24 -0
- package/web/.svelte-kit/output/server/chunks/internal.js +133 -0
- package/web/.svelte-kit/output/server/chunks/plus.js +81 -0
- package/web/.svelte-kit/output/server/chunks/root.js +4076 -0
- package/web/.svelte-kit/output/server/chunks/shared.js +789 -0
- package/web/.svelte-kit/output/server/chunks/utils.js +43 -0
- package/web/.svelte-kit/output/server/entries/fallbacks/error.svelte.js +11 -0
- package/web/.svelte-kit/output/server/entries/pages/_layout.svelte.js +3944 -0
- package/web/.svelte-kit/output/server/entries/pages/_layout.ts.js +28 -0
- package/web/.svelte-kit/output/server/entries/pages/_page.svelte.js +7 -0
- package/web/.svelte-kit/output/server/entries/pages/agents/_page.svelte.js +379 -0
- package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.svelte.js +292 -0
- package/web/.svelte-kit/output/server/entries/pages/chats/_id_/_page.ts.js +17 -0
- package/web/.svelte-kit/output/server/entries/pages/chats/_id_/settings/_page.svelte.js +259 -0
- package/web/.svelte-kit/output/server/entries/pages/chats/_id_/settings/_page.ts.js +17 -0
- package/web/.svelte-kit/output/server/index.js +3748 -0
- package/web/.svelte-kit/output/server/internal.js +14 -0
- package/web/.svelte-kit/output/server/manifest-full.js +63 -0
- package/web/.svelte-kit/output/server/manifest.js +63 -0
- package/web/.svelte-kit/output/server/nodes/0.js +13 -0
- package/web/.svelte-kit/output/server/nodes/1.js +8 -0
- package/web/.svelte-kit/output/server/nodes/2.js +8 -0
- package/web/.svelte-kit/output/server/nodes/3.js +8 -0
- package/web/.svelte-kit/output/server/nodes/4.js +13 -0
- package/web/.svelte-kit/output/server/nodes/5.js +13 -0
- package/web/.svelte-kit/output/server/remote-entry.js +557 -0
- package/web/.svelte-kit/tsconfig.json +67 -0
- package/web/.svelte-kit/types/route_meta_data.json +17 -0
- package/web/.svelte-kit/types/src/routes/$types.d.ts +26 -0
- package/web/.svelte-kit/types/src/routes/agents/$types.d.ts +18 -0
- package/web/.svelte-kit/types/src/routes/chats/[id]/$types.d.ts +21 -0
- package/web/.svelte-kit/types/src/routes/chats/[id]/proxy+page.ts +20 -0
- package/web/.svelte-kit/types/src/routes/chats/[id]/settings/$types.d.ts +21 -0
- package/web/.svelte-kit/types/src/routes/chats/[id]/settings/proxy+page.ts +19 -0
- package/web/.svelte-kit/types/src/routes/proxy+layout.ts +35 -0
- package/web/README.md +42 -0
- package/web/components.json +16 -0
- package/web/package.json +41 -0
- package/web/src/app.css +121 -0
- package/web/src/app.d.ts +13 -0
- package/web/src/app.html +11 -0
- package/web/src/demo.spec.ts +7 -0
- package/web/src/lib/app-state.svelte.ts +3 -0
- package/web/src/lib/assets/favicon.svg +1 -0
- package/web/src/lib/components/app/app-sidebar-test-wrapper.svelte +10 -0
- package/web/src/lib/components/app/app-sidebar.svelte +171 -0
- package/web/src/lib/components/app/app-sidebar.svelte.spec.ts +13 -0
- package/web/src/lib/components/ui/button/button.svelte +82 -0
- package/web/src/lib/components/ui/button/index.ts +17 -0
- package/web/src/lib/components/ui/dialog/dialog-close.svelte +7 -0
- package/web/src/lib/components/ui/dialog/dialog-content.svelte +45 -0
- package/web/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/web/src/lib/components/ui/dialog/dialog-footer.svelte +20 -0
- package/web/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/web/src/lib/components/ui/dialog/dialog-overlay.svelte +20 -0
- package/web/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/web/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/web/src/lib/components/ui/dialog/dialog-trigger.svelte +7 -0
- package/web/src/lib/components/ui/dialog/dialog.svelte +7 -0
- package/web/src/lib/components/ui/dialog/index.ts +34 -0
- package/web/src/lib/components/ui/input/index.ts +7 -0
- package/web/src/lib/components/ui/input/input.svelte +52 -0
- package/web/src/lib/components/ui/separator/index.ts +7 -0
- package/web/src/lib/components/ui/separator/separator.svelte +21 -0
- package/web/src/lib/components/ui/sheet/index.ts +34 -0
- package/web/src/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/web/src/lib/components/ui/sheet/sheet-content.svelte +60 -0
- package/web/src/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/web/src/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/web/src/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/web/src/lib/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/web/src/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/web/src/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/web/src/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/web/src/lib/components/ui/sheet/sheet.svelte +7 -0
- package/web/src/lib/components/ui/sidebar/constants.ts +6 -0
- package/web/src/lib/components/ui/sidebar/context.svelte.ts +79 -0
- package/web/src/lib/components/ui/sidebar/index.ts +75 -0
- package/web/src/lib/components/ui/sidebar/sidebar-content.svelte +24 -0
- package/web/src/lib/components/ui/sidebar/sidebar-footer.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte +36 -0
- package/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte +34 -0
- package/web/src/lib/components/ui/sidebar/sidebar-group.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-header.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-input.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-inset.svelte +24 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte +43 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte +29 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte +103 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte +36 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte +43 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte +25 -0
- package/web/src/lib/components/ui/sidebar/sidebar-menu.svelte +21 -0
- package/web/src/lib/components/ui/sidebar/sidebar-provider.svelte +53 -0
- package/web/src/lib/components/ui/sidebar/sidebar-rail.svelte +36 -0
- package/web/src/lib/components/ui/sidebar/sidebar-separator.svelte +19 -0
- package/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte +35 -0
- package/web/src/lib/components/ui/sidebar/sidebar.svelte +104 -0
- package/web/src/lib/components/ui/skeleton/index.ts +7 -0
- package/web/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/web/src/lib/components/ui/switch/index.ts +7 -0
- package/web/src/lib/components/ui/switch/switch.svelte +29 -0
- package/web/src/lib/components/ui/textarea/index.ts +7 -0
- package/web/src/lib/components/ui/textarea/textarea.svelte +23 -0
- package/web/src/lib/components/ui/tooltip/index.ts +19 -0
- package/web/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/web/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/web/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/web/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/web/src/lib/hooks/is-mobile.svelte.ts +9 -0
- package/web/src/lib/index.ts +1 -0
- package/web/src/lib/types.ts +23 -0
- package/web/src/lib/utils.ts +13 -0
- package/web/src/routes/+layout.svelte +67 -0
- package/web/src/routes/+layout.ts +34 -0
- package/web/src/routes/+page.svelte +7 -0
- package/web/src/routes/agents/+page.svelte +206 -0
- package/web/src/routes/chats/[id]/+page.svelte +406 -0
- package/web/src/routes/chats/[id]/+page.ts +19 -0
- package/web/src/routes/chats/[id]/page.svelte.spec.ts +102 -0
- package/web/src/routes/chats/[id]/settings/+page.svelte +165 -0
- package/web/src/routes/chats/[id]/settings/+page.ts +18 -0
- package/web/src/routes/page.svelte.spec.ts +13 -0
- package/web/static/robots.txt +3 -0
- package/web/svelte.config.js +21 -0
- package/web/tsconfig.json +20 -0
- package/web/vite.config.ts +41 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { invalidate } from '$app/navigation';
|
|
3
|
+
import { Button } from '$lib/components/ui/button/index.js';
|
|
4
|
+
import { Input } from '$lib/components/ui/input/index.js';
|
|
5
|
+
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
|
6
|
+
import { Plus, Trash, Edit, Bot } from 'lucide-svelte';
|
|
7
|
+
|
|
8
|
+
let { data }: { data: { agents: { id: string; directory?: string; env?: Record<string, string> }[] } } = $props();
|
|
9
|
+
|
|
10
|
+
let agents = $derived(data.agents || []);
|
|
11
|
+
|
|
12
|
+
let editAgentOpen = $state(false);
|
|
13
|
+
let isEditing = $state(false);
|
|
14
|
+
|
|
15
|
+
let agentId = $state('');
|
|
16
|
+
let agentDirectory = $state('');
|
|
17
|
+
let agentEnv = $state(''); // will parse as JSON or multiline KEY=VALUE
|
|
18
|
+
|
|
19
|
+
let isSaving = $state(false);
|
|
20
|
+
let errorMessage = $state('');
|
|
21
|
+
let globalError = $state('');
|
|
22
|
+
|
|
23
|
+
// parse/unparse env for editing
|
|
24
|
+
function unparseEnv(envObj: any) {
|
|
25
|
+
if (!envObj) return '';
|
|
26
|
+
return Object.entries(envObj).map(([k, v]) => `${k}=${v}`).join('\n');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseEnv(envStr: string) {
|
|
30
|
+
const obj: Record<string, string> = {};
|
|
31
|
+
const lines = envStr.split('\n');
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const match = line.match(/^([^=]+)=(.*)$/);
|
|
34
|
+
if (match) {
|
|
35
|
+
obj[match[1].trim()] = match[2].trim();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return obj;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function openCreate() {
|
|
42
|
+
isEditing = false;
|
|
43
|
+
agentId = '';
|
|
44
|
+
agentDirectory = '';
|
|
45
|
+
agentEnv = '';
|
|
46
|
+
errorMessage = '';
|
|
47
|
+
globalError = '';
|
|
48
|
+
editAgentOpen = true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function openEdit(agent: any) {
|
|
52
|
+
isEditing = true;
|
|
53
|
+
agentId = agent.id;
|
|
54
|
+
agentDirectory = agent.directory || '';
|
|
55
|
+
agentEnv = unparseEnv(agent.env);
|
|
56
|
+
errorMessage = '';
|
|
57
|
+
globalError = '';
|
|
58
|
+
editAgentOpen = true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function saveAgent() {
|
|
62
|
+
isSaving = true;
|
|
63
|
+
errorMessage = '';
|
|
64
|
+
try {
|
|
65
|
+
const payload: any = {
|
|
66
|
+
directory: agentDirectory.trim(),
|
|
67
|
+
env: parseEnv(agentEnv),
|
|
68
|
+
};
|
|
69
|
+
if (!isEditing) payload.id = agentId;
|
|
70
|
+
|
|
71
|
+
const url = isEditing ? `/api/agents/${agentId}` : '/api/agents';
|
|
72
|
+
const method = isEditing ? 'PUT' : 'POST';
|
|
73
|
+
|
|
74
|
+
const res = await fetch(url, {
|
|
75
|
+
method,
|
|
76
|
+
headers: { 'Content-Type': 'application/json' },
|
|
77
|
+
body: JSON.stringify(payload)
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (res.ok) {
|
|
81
|
+
editAgentOpen = false;
|
|
82
|
+
await invalidate('app:agents');
|
|
83
|
+
} else {
|
|
84
|
+
const d = await res.json();
|
|
85
|
+
errorMessage = d.error || 'Failed to save agent';
|
|
86
|
+
}
|
|
87
|
+
} catch (e) {
|
|
88
|
+
errorMessage = 'Error saving agent';
|
|
89
|
+
} finally {
|
|
90
|
+
isSaving = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function deleteAgent(id: string) {
|
|
95
|
+
if (!confirm(`Are you sure you want to delete agent "${id}"?`)) return;
|
|
96
|
+
globalError = '';
|
|
97
|
+
try {
|
|
98
|
+
const res = await fetch(`/api/agents/${id}`, { method: 'DELETE' });
|
|
99
|
+
if (res.ok) {
|
|
100
|
+
await invalidate('app:agents');
|
|
101
|
+
} else {
|
|
102
|
+
globalError = 'Failed to delete agent';
|
|
103
|
+
}
|
|
104
|
+
} catch (e) {
|
|
105
|
+
globalError = 'Error deleting agent';
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<div class="p-6 max-w-4xl mx-auto flex flex-col gap-6 h-full overflow-y-auto">
|
|
111
|
+
<div class="flex items-center justify-between">
|
|
112
|
+
<h1 class="text-2xl font-bold tracking-tight">Agents</h1>
|
|
113
|
+
<Button onclick={openCreate} size="sm">
|
|
114
|
+
<Plus class="w-4 h-4 mr-2" />
|
|
115
|
+
Create Agent
|
|
116
|
+
</Button>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
{#if globalError}
|
|
120
|
+
<div class="bg-destructive/15 text-destructive text-sm p-3 rounded-md flex items-center">
|
|
121
|
+
{globalError}
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
|
|
125
|
+
{#if agents.length === 0}
|
|
126
|
+
<div class="flex flex-col items-center justify-center p-12 text-center text-muted-foreground border rounded-lg bg-background">
|
|
127
|
+
<Bot class="w-12 h-12 mb-4 text-muted-foreground/50" />
|
|
128
|
+
<p>No agents created yet.</p>
|
|
129
|
+
</div>
|
|
130
|
+
{:else}
|
|
131
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
132
|
+
{#each agents as agent}
|
|
133
|
+
<div class="flex flex-col gap-2 p-4 border rounded-lg bg-background shadow-sm">
|
|
134
|
+
<div class="flex items-center justify-between">
|
|
135
|
+
<h2 class="font-semibold text-lg flex items-center gap-2">
|
|
136
|
+
<Bot class="w-5 h-5 text-primary" />
|
|
137
|
+
{agent.id}
|
|
138
|
+
</h2>
|
|
139
|
+
<div class="flex gap-2">
|
|
140
|
+
<Button variant="outline" size="icon" onclick={() => openEdit(agent)}>
|
|
141
|
+
<Edit class="w-4 h-4" />
|
|
142
|
+
</Button>
|
|
143
|
+
<Button variant="destructive" size="icon" onclick={() => deleteAgent(agent.id)}>
|
|
144
|
+
<Trash class="w-4 h-4" />
|
|
145
|
+
</Button>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
{#if agent.directory}
|
|
149
|
+
<div class="text-sm mt-2">
|
|
150
|
+
<span class="text-muted-foreground font-medium">Directory:</span>
|
|
151
|
+
<code class="ml-1 bg-muted px-1 py-0.5 rounded text-xs">{agent.directory}</code>
|
|
152
|
+
</div>
|
|
153
|
+
{/if}
|
|
154
|
+
{#if agent.env && Object.keys(agent.env).length > 0}
|
|
155
|
+
<div class="text-sm mt-2">
|
|
156
|
+
<span class="text-muted-foreground font-medium">Environment Variables:</span>
|
|
157
|
+
<div class="mt-1 bg-muted p-2 rounded text-xs overflow-x-auto whitespace-pre">
|
|
158
|
+
{Object.entries(agent.env).map(([k, v]) => `${k}=${v}`).join('\n')}
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
</div>
|
|
163
|
+
{/each}
|
|
164
|
+
</div>
|
|
165
|
+
{/if}
|
|
166
|
+
|
|
167
|
+
<Dialog.Root bind:open={editAgentOpen}>
|
|
168
|
+
<Dialog.Content class="sm:max-w-[425px]">
|
|
169
|
+
<Dialog.Header>
|
|
170
|
+
<Dialog.Title>{isEditing ? 'Edit Agent' : 'Create Agent'}</Dialog.Title>
|
|
171
|
+
<Dialog.Description>
|
|
172
|
+
Configure the agent's working directory and environment variables.
|
|
173
|
+
</Dialog.Description>
|
|
174
|
+
</Dialog.Header>
|
|
175
|
+
<div class="grid gap-4 py-4">
|
|
176
|
+
{#if errorMessage}
|
|
177
|
+
<div class="bg-destructive/15 text-destructive text-sm p-3 rounded-md flex items-center">
|
|
178
|
+
{errorMessage}
|
|
179
|
+
</div>
|
|
180
|
+
{/if}
|
|
181
|
+
<div class="flex flex-col gap-2">
|
|
182
|
+
<label class="text-sm font-medium leading-none" for="agent-id">ID</label>
|
|
183
|
+
<Input id="agent-id" bind:value={agentId} disabled={isEditing} placeholder="e.g. backend-agent" />
|
|
184
|
+
</div>
|
|
185
|
+
<div class="flex flex-col gap-2">
|
|
186
|
+
<label class="text-sm font-medium leading-none" for="agent-dir">Directory</label>
|
|
187
|
+
<Input id="agent-dir" bind:value={agentDirectory} placeholder="e.g. ./apps/backend" />
|
|
188
|
+
</div>
|
|
189
|
+
<div class="flex flex-col gap-2">
|
|
190
|
+
<label class="text-sm font-medium leading-none" for="agent-env">Environment Variables (KEY=VALUE)</label>
|
|
191
|
+
<textarea
|
|
192
|
+
id="agent-env"
|
|
193
|
+
bind:value={agentEnv}
|
|
194
|
+
placeholder="PORT=8080 NODE_ENV=development"
|
|
195
|
+
class="flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
196
|
+
></textarea>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
<Dialog.Footer>
|
|
200
|
+
<Button disabled={!agentId || isSaving} onclick={saveAgent}>
|
|
201
|
+
{isSaving ? 'Saving...' : 'Save Agent'}
|
|
202
|
+
</Button>
|
|
203
|
+
</Dialog.Footer>
|
|
204
|
+
</Dialog.Content>
|
|
205
|
+
</Dialog.Root>
|
|
206
|
+
</div>
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { PageData } from './$types';
|
|
3
|
+
import type { ChatMessage } from '$lib/types';
|
|
4
|
+
import { invalidate } from '$app/navigation';
|
|
5
|
+
import { Send, Clock, AlertCircle } from 'lucide-svelte';
|
|
6
|
+
import { Button } from '$lib/components/ui/button/index.js';
|
|
7
|
+
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
|
8
|
+
import { tick, onMount, onDestroy } from 'svelte';
|
|
9
|
+
import { appState } from '$lib/app-state.svelte.js';
|
|
10
|
+
|
|
11
|
+
let { data } = $props<{ data: PageData }>();
|
|
12
|
+
|
|
13
|
+
type PendingStatus = 'sending' | 'pending' | 'failed';
|
|
14
|
+
interface PendingMessage {
|
|
15
|
+
id: string;
|
|
16
|
+
content: string;
|
|
17
|
+
timestamp: string;
|
|
18
|
+
status: PendingStatus;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let inputValue = $state('');
|
|
22
|
+
let liveMessages = $state<ChatMessage[]>([]);
|
|
23
|
+
let filteredMessages = $derived(liveMessages.filter((msg) => {
|
|
24
|
+
if (msg.role === 'user') return true;
|
|
25
|
+
if (appState.verbosityLevel === 'verbose') return true;
|
|
26
|
+
if (appState.verbosityLevel === 'debug') {
|
|
27
|
+
return !msg.level || msg.level === 'default' || msg.level === 'debug';
|
|
28
|
+
}
|
|
29
|
+
return !msg.level || msg.level === 'default';
|
|
30
|
+
}));
|
|
31
|
+
let pendingMessages = $state<PendingMessage[]>([]);
|
|
32
|
+
let chatContainer: HTMLElement | undefined = $state();
|
|
33
|
+
let eventSource: EventSource | null = null;
|
|
34
|
+
let isScrolledToBottom = $state(true);
|
|
35
|
+
let isReconnecting = $state(false);
|
|
36
|
+
let reconnectTimeout: ReturnType<typeof setTimeout>;
|
|
37
|
+
let activeActionId = $state<string | null>(null);
|
|
38
|
+
|
|
39
|
+
async function retryMessage(msgId: string) {
|
|
40
|
+
const msgIndex = pendingMessages.findIndex(m => m.id === msgId);
|
|
41
|
+
if (msgIndex === -1) return;
|
|
42
|
+
|
|
43
|
+
pendingMessages[msgIndex].status = 'sending';
|
|
44
|
+
savePendingMessages(data.id, pendingMessages);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const res = await fetch(`/api/chats/${data.id}/messages`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/json' },
|
|
50
|
+
body: JSON.stringify({ message: pendingMessages[msgIndex].content })
|
|
51
|
+
});
|
|
52
|
+
if (!res.ok) throw new Error('Failed to send');
|
|
53
|
+
|
|
54
|
+
await invalidate(`app:chat:${data.id}`);
|
|
55
|
+
} catch (err) {
|
|
56
|
+
console.error('Failed to retry message:', err);
|
|
57
|
+
const idx = pendingMessages.findIndex(m => m.id === msgId);
|
|
58
|
+
if (idx !== -1) {
|
|
59
|
+
pendingMessages[idx].status = 'failed';
|
|
60
|
+
savePendingMessages(data.id, pendingMessages);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function deleteMessage(msgId: string) {
|
|
66
|
+
pendingMessages = pendingMessages.filter(m => m.id !== msgId);
|
|
67
|
+
savePendingMessages(data.id, pendingMessages);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
$effect(() => {
|
|
71
|
+
const handleOnline = () => {
|
|
72
|
+
pendingMessages.forEach((msg) => {
|
|
73
|
+
if (msg.status === 'pending' || msg.status === 'failed') {
|
|
74
|
+
retryMessage(msg.id);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
window.addEventListener('online', handleOnline);
|
|
80
|
+
return () => {
|
|
81
|
+
window.removeEventListener('online', handleOnline);
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
function checkScroll(e: Event) {
|
|
86
|
+
const target = e.target as HTMLElement;
|
|
87
|
+
// Allow a 10px threshold for being at the bottom
|
|
88
|
+
isScrolledToBottom = Math.abs(target.scrollHeight - target.scrollTop - target.clientHeight) < 10;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function scrollToBottom() {
|
|
92
|
+
if (chatContainer) {
|
|
93
|
+
chatContainer.scrollTop = chatContainer.scrollHeight;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// We sync live messages with initial loaded data whenever the ID changes
|
|
98
|
+
$effect(() => {
|
|
99
|
+
liveMessages = data.messages as ChatMessage[];
|
|
100
|
+
|
|
101
|
+
// Load pending from local storage
|
|
102
|
+
try {
|
|
103
|
+
const stored = localStorage.getItem(`pending_messages_${data.id}`);
|
|
104
|
+
if (stored) {
|
|
105
|
+
pendingMessages = JSON.parse(stored);
|
|
106
|
+
} else {
|
|
107
|
+
pendingMessages = [];
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
pendingMessages = [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
isScrolledToBottom = true;
|
|
114
|
+
setupSSE(data.id);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
function savePendingMessages(chatId: string, messages: PendingMessage[]) {
|
|
118
|
+
localStorage.setItem(`pending_messages_${chatId}`, JSON.stringify(messages));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Auto-scroll on new messages
|
|
122
|
+
$effect(() => {
|
|
123
|
+
if ((liveMessages.length > 0 || pendingMessages.length > 0) && chatContainer && isScrolledToBottom) {
|
|
124
|
+
tick().then(scrollToBottom);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Keep scrolled to bottom if textarea grows
|
|
129
|
+
$effect(() => {
|
|
130
|
+
// depend on inputValue changes
|
|
131
|
+
inputValue;
|
|
132
|
+
if (isScrolledToBottom) {
|
|
133
|
+
tick().then(scrollToBottom);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Keep scrolled to bottom on window or visualViewport resize (e.g., keyboard toggle)
|
|
138
|
+
$effect(() => {
|
|
139
|
+
const handleResize = () => {
|
|
140
|
+
if (isScrolledToBottom) {
|
|
141
|
+
scrollToBottom();
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
window.addEventListener('resize', handleResize);
|
|
146
|
+
window.visualViewport?.addEventListener('resize', handleResize);
|
|
147
|
+
|
|
148
|
+
return () => {
|
|
149
|
+
window.removeEventListener('resize', handleResize);
|
|
150
|
+
window.visualViewport?.removeEventListener('resize', handleResize);
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
async function fetchDeltaMessages() {
|
|
155
|
+
const lastMsgId = liveMessages.length > 0 ? liveMessages[liveMessages.length - 1].id : null;
|
|
156
|
+
if (lastMsgId) {
|
|
157
|
+
try {
|
|
158
|
+
const res = await fetch(`/api/chats/${data.id}?since=${lastMsgId}`);
|
|
159
|
+
if (res.ok) {
|
|
160
|
+
const newMessages: ChatMessage[] = await res.json();
|
|
161
|
+
for (const msg of newMessages) {
|
|
162
|
+
if (!liveMessages.find((m) => m.id === msg.id)) {
|
|
163
|
+
liveMessages = [...liveMessages, msg];
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (e) {
|
|
168
|
+
console.error('Failed to fetch delta messages:', e);
|
|
169
|
+
}
|
|
170
|
+
} else {
|
|
171
|
+
await invalidate(`app:chat:${data.id}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
$effect(() => {
|
|
176
|
+
const handleVisibilityChange = () => {
|
|
177
|
+
if (document.visibilityState === 'visible') {
|
|
178
|
+
fetchDeltaMessages();
|
|
179
|
+
if (!eventSource || eventSource.readyState === EventSource.CLOSED) {
|
|
180
|
+
setupSSE(data.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
document.addEventListener('visibilitychange', handleVisibilityChange);
|
|
186
|
+
return () => {
|
|
187
|
+
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
function setupSSE(chatId: string) {
|
|
192
|
+
if (eventSource) {
|
|
193
|
+
eventSource.close();
|
|
194
|
+
clearTimeout(reconnectTimeout);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
eventSource = new EventSource(`/api/chats/${chatId}/stream`);
|
|
198
|
+
|
|
199
|
+
eventSource.onopen = () => {
|
|
200
|
+
isReconnecting = false;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
eventSource.onerror = () => {
|
|
204
|
+
if (eventSource?.readyState === EventSource.CLOSED || eventSource?.readyState === EventSource.CONNECTING) {
|
|
205
|
+
isReconnecting = true;
|
|
206
|
+
eventSource.close();
|
|
207
|
+
clearTimeout(reconnectTimeout);
|
|
208
|
+
reconnectTimeout = setTimeout(async () => {
|
|
209
|
+
await fetchDeltaMessages();
|
|
210
|
+
setupSSE(chatId);
|
|
211
|
+
}, 3000);
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
eventSource.onmessage = (event) => {
|
|
216
|
+
try {
|
|
217
|
+
const newMessage = JSON.parse(event.data);
|
|
218
|
+
// Ensure we don't duplicate messages we just sent and received via SSE
|
|
219
|
+
if (!liveMessages.find((m) => m.id === newMessage.id)) {
|
|
220
|
+
liveMessages = [...liveMessages, newMessage];
|
|
221
|
+
|
|
222
|
+
if (newMessage.role === 'user') {
|
|
223
|
+
// Remove from pending by matching content
|
|
224
|
+
const idx = pendingMessages.findIndex(m => m.content === newMessage.content);
|
|
225
|
+
if (idx !== -1) {
|
|
226
|
+
pendingMessages = pendingMessages.filter((_, i) => i !== idx);
|
|
227
|
+
savePendingMessages(chatId, pendingMessages);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch (e) {
|
|
232
|
+
console.error('Failed to parse SSE message', e);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
onDestroy(() => {
|
|
238
|
+
if (eventSource) {
|
|
239
|
+
eventSource.close();
|
|
240
|
+
}
|
|
241
|
+
clearTimeout(reconnectTimeout);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
async function sendMessage(e: Event) {
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
if (!inputValue.trim()) return;
|
|
247
|
+
|
|
248
|
+
const currentInput = inputValue;
|
|
249
|
+
inputValue = '';
|
|
250
|
+
|
|
251
|
+
const pendingMsg: PendingMessage = {
|
|
252
|
+
id: `pending-${Date.now()}-${Math.random()}`,
|
|
253
|
+
content: currentInput,
|
|
254
|
+
timestamp: new Date().toISOString(),
|
|
255
|
+
status: navigator.onLine ? 'sending' : 'pending'
|
|
256
|
+
};
|
|
257
|
+
pendingMessages = [...pendingMessages, pendingMsg];
|
|
258
|
+
savePendingMessages(data.id, pendingMessages);
|
|
259
|
+
|
|
260
|
+
if (!navigator.onLine) {
|
|
261
|
+
return; // Offline, stays pending
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
const res = await fetch(`/api/chats/${data.id}/messages`, {
|
|
266
|
+
method: 'POST',
|
|
267
|
+
headers: { 'Content-Type': 'application/json' },
|
|
268
|
+
body: JSON.stringify({ message: currentInput })
|
|
269
|
+
});
|
|
270
|
+
if (!res.ok) throw new Error('Failed to send');
|
|
271
|
+
|
|
272
|
+
// SSE should handle the incoming log and user messages now.
|
|
273
|
+
// But we can still invalidate to be safe.
|
|
274
|
+
await invalidate(`app:chat:${data.id}`);
|
|
275
|
+
} catch (err) {
|
|
276
|
+
console.error('Failed to send message:', err);
|
|
277
|
+
// Mark as failed instead of removing
|
|
278
|
+
pendingMessages = pendingMessages.map((m) => m.id === pendingMsg.id ? { ...m, status: 'failed' } : m);
|
|
279
|
+
savePendingMessages(data.id, pendingMessages);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function formatTime(iso: string) {
|
|
284
|
+
return new Date(iso).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
285
|
+
}
|
|
286
|
+
</script>
|
|
287
|
+
|
|
288
|
+
<div class="flex flex-col flex-1 h-full overflow-hidden relative">
|
|
289
|
+
{#if isReconnecting}
|
|
290
|
+
<div class="absolute top-4 left-1/2 -translate-x-1/2 bg-yellow-500/10 text-yellow-600 dark:text-yellow-400 text-xs px-3 py-1 rounded-full border border-yellow-500/20 backdrop-blur-sm shadow-sm z-10 flex items-center gap-2">
|
|
291
|
+
<span class="inline-block w-3 h-3 rounded-full border-2 border-current border-t-transparent animate-spin"></span>
|
|
292
|
+
Reconnecting...
|
|
293
|
+
</div>
|
|
294
|
+
{/if}
|
|
295
|
+
|
|
296
|
+
<div bind:this={chatContainer} onscroll={checkScroll} class="flex-1 overflow-y-auto p-4">
|
|
297
|
+
<div class="w-full max-w-4xl mx-auto space-y-6 flex flex-col min-h-full">
|
|
298
|
+
{#if liveMessages.length === 0}
|
|
299
|
+
<div class="flex-1 flex items-center justify-center text-muted-foreground text-sm">
|
|
300
|
+
No messages yet. Send a message to start the conversation!
|
|
301
|
+
</div>
|
|
302
|
+
{/if}
|
|
303
|
+
|
|
304
|
+
{#each filteredMessages as msg (msg.id)}
|
|
305
|
+
<div class="flex flex-col gap-1 {msg.role === 'user' ? 'items-end' : 'items-start'}">
|
|
306
|
+
<div class="flex items-baseline gap-2 max-w-[80%] {msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'}">
|
|
307
|
+
{#if msg.role === 'user'}
|
|
308
|
+
<div class="px-4 py-2 rounded-2xl bg-primary text-primary-foreground text-sm" data-testid="user-message">
|
|
309
|
+
{msg.content}
|
|
310
|
+
</div>
|
|
311
|
+
{:else}
|
|
312
|
+
<div class="px-4 py-3 rounded-2xl bg-card border text-card-foreground text-sm shadow-sm {msg.level === 'verbose' ? 'border-primary/50 bg-primary/5 shadow-md' : ''}" data-testid="log-message">
|
|
313
|
+
{#if appState.verbosityLevel === 'verbose'}
|
|
314
|
+
<div class="font-mono text-xs text-muted-foreground mb-2 flex items-center gap-2">
|
|
315
|
+
<span>$ {msg.command}</span>
|
|
316
|
+
{#if msg.exitCode !== 0}
|
|
317
|
+
<span class="text-destructive font-bold">Exit: {msg.exitCode}</span>
|
|
318
|
+
{/if}
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
{#if msg.content}
|
|
322
|
+
<div class="whitespace-pre-wrap">{msg.content}</div>
|
|
323
|
+
{:else if msg.stdout}
|
|
324
|
+
<div class="whitespace-pre-wrap font-mono text-xs mt-2">{msg.stdout}</div>
|
|
325
|
+
{:else}
|
|
326
|
+
<div class="whitespace-pre-wrap italic opacity-50 text-xs mt-2">No output</div>
|
|
327
|
+
{/if}
|
|
328
|
+
|
|
329
|
+
{#if msg.stderr}
|
|
330
|
+
<div class="whitespace-pre-wrap font-mono text-xs mt-2 text-destructive border border-destructive/20 bg-destructive/5 p-2 rounded">
|
|
331
|
+
{msg.stderr}
|
|
332
|
+
</div>
|
|
333
|
+
{/if}
|
|
334
|
+
{:else}
|
|
335
|
+
{#if msg.content}
|
|
336
|
+
<div class="whitespace-pre-wrap">{msg.content}</div>
|
|
337
|
+
{/if}
|
|
338
|
+
{/if}
|
|
339
|
+
</div>
|
|
340
|
+
{/if}
|
|
341
|
+
</div>
|
|
342
|
+
<div class="text-[10px] text-muted-foreground px-2">
|
|
343
|
+
{formatTime(msg.timestamp)}
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
{/each}
|
|
347
|
+
|
|
348
|
+
{#each pendingMessages as msg (msg.id)}
|
|
349
|
+
<div class="flex flex-col gap-1 items-end {msg.status === 'sending' ? 'opacity-50' : ''} transition-opacity">
|
|
350
|
+
<button
|
|
351
|
+
class="flex items-baseline gap-2 max-w-[80%] flex-row-reverse text-left focus:outline-none"
|
|
352
|
+
onclick={() => {
|
|
353
|
+
if (msg.status !== 'sending') {
|
|
354
|
+
activeActionId = activeActionId === msg.id ? null : msg.id;
|
|
355
|
+
}
|
|
356
|
+
}}
|
|
357
|
+
>
|
|
358
|
+
<div class="px-4 py-2 rounded-2xl {msg.status === 'failed' ? 'bg-destructive/90 text-destructive-foreground' : 'bg-primary text-primary-foreground'} text-sm" data-testid="pending-message">
|
|
359
|
+
{msg.content}
|
|
360
|
+
</div>
|
|
361
|
+
</button>
|
|
362
|
+
<div class="text-[10px] px-2 flex items-center gap-1 {msg.status === 'failed' ? 'text-destructive font-medium' : 'text-muted-foreground'}">
|
|
363
|
+
{#if msg.status === 'sending'}
|
|
364
|
+
<span class="inline-block w-2 h-2 rounded-full border border-current border-t-transparent animate-spin"></span>
|
|
365
|
+
Sending...
|
|
366
|
+
{:else if msg.status === 'pending'}
|
|
367
|
+
<Clock class="w-3 h-3" />
|
|
368
|
+
Offline / Pending
|
|
369
|
+
{:else if msg.status === 'failed'}
|
|
370
|
+
<AlertCircle class="w-3 h-3" />
|
|
371
|
+
Failed
|
|
372
|
+
{/if}
|
|
373
|
+
</div>
|
|
374
|
+
{#if activeActionId === msg.id && msg.status !== 'sending'}
|
|
375
|
+
<div class="flex items-center gap-2 mt-1 mr-2 bg-card border rounded-md p-1 shadow-sm text-xs">
|
|
376
|
+
<button class="px-2 py-1 hover:bg-muted rounded text-primary transition-colors focus:outline-none" onclick={(e) => { e.stopPropagation(); activeActionId = null; retryMessage(msg.id); }}>Retry manual send</button>
|
|
377
|
+
<div class="w-px h-3 bg-border"></div>
|
|
378
|
+
<button class="px-2 py-1 hover:bg-muted rounded text-destructive transition-colors focus:outline-none" onclick={(e) => { e.stopPropagation(); activeActionId = null; deleteMessage(msg.id); }}>Delete message</button>
|
|
379
|
+
</div>
|
|
380
|
+
{/if}
|
|
381
|
+
</div>
|
|
382
|
+
{/each}
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div class="p-4 bg-background/80 backdrop-blur-sm border-t shrink-0">
|
|
387
|
+
<form onsubmit={sendMessage} class="flex items-center gap-2 max-w-4xl mx-auto">
|
|
388
|
+
<Textarea
|
|
389
|
+
bind:value={inputValue}
|
|
390
|
+
placeholder="Type your message..."
|
|
391
|
+
class="flex-1 min-h-[0px] resize-none overflow-hidden h-auto"
|
|
392
|
+
onkeydown={(e) => {
|
|
393
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
394
|
+
e.preventDefault();
|
|
395
|
+
sendMessage(e);
|
|
396
|
+
}
|
|
397
|
+
}}
|
|
398
|
+
data-testid="message-input"
|
|
399
|
+
/>
|
|
400
|
+
<Button type="submit" disabled={!inputValue.trim()} size="icon" data-testid="send-button">
|
|
401
|
+
<Send class="w-4 h-4" />
|
|
402
|
+
<span class="sr-only">Send</span>
|
|
403
|
+
</Button>
|
|
404
|
+
</form>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { PageLoad } from './$types';
|
|
2
|
+
import type { ChatMessage } from '$lib/types';
|
|
3
|
+
|
|
4
|
+
export const load: PageLoad = async ({ params, fetch, depends }) => {
|
|
5
|
+
const { id } = params;
|
|
6
|
+
depends(`app:chat:${id}`);
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const res = await fetch(`/api/chats/${id}`);
|
|
10
|
+
if (res.ok) {
|
|
11
|
+
const messages: ChatMessage[] = await res.json();
|
|
12
|
+
return { id, messages };
|
|
13
|
+
}
|
|
14
|
+
} catch (e) {
|
|
15
|
+
console.error(e);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return { id, messages: [] };
|
|
19
|
+
};
|