Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 358f177584 | |||
| b60bb57aa4 | |||
| 7003fca873 | |||
| b5d825356e | |||
| 896eb4fc52 | |||
| f8d7438eec | |||
| f7f4e8f95b | |||
| af76910f36 | |||
| f06111f045 | |||
| c6020b7f43 | |||
| 488b798275 | |||
| 0df19967ca | |||
| 6e04015676 | |||
| acaf9477a1 | |||
| 46a89d481a | |||
| c51428414e | |||
| 50405800c6 | |||
| 4aca056bc9 | |||
| 5e725340b4 | |||
| 3fa2064e3e | |||
| 16f9722851 | |||
| 5f0680c6c1 | |||
| 57e0197d75 | |||
| dc4bac3a85 | |||
| f16545ec36 | |||
| d132ed8e64 | |||
| 2a633d604a | |||
| 6a44c0b8ee | |||
| 3f97e34f21 | |||
| 49a8a23d75 | |||
| 1358a32476 | |||
| 9b79160c95 | |||
| 0cbe99677f | |||
| 46b1609528 | |||
| 2b0b08ceda | |||
| 19cc684433 | |||
| fecb157291 | |||
| ac84e7240e | |||
| d5d82bdb00 | |||
| f10edd6718 | |||
| 3f6cd55833 | |||
| a9e8bc4d87 | |||
| 063e0fa76e | |||
| 9e7bd33822 |
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
name: "Rename project from \"storkit\" to \"huskies\""
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 455: Rename project from "storkit" to "huskies"
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a project maintainer, I want to rename the project from "storkit" to "huskies" so that the product has its new identity throughout the codebase, tooling, and documentation.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Rust crate name in server/Cargo.toml changed from 'storkit' to 'huskies'
|
||||||
|
- [ ] Binary name changed to 'huskies' (Dockerfile CMD, release script binary names)
|
||||||
|
- [ ] Environment variables renamed: STORKIT_PORT → HUSKIES_PORT, STORKIT_HOST → HUSKIES_HOST
|
||||||
|
- [ ] Docker service name, container_name, image name, and volume names updated in docker-compose.yml
|
||||||
|
- [ ] Docker user/group renamed from 'storkit' to 'huskies' in Dockerfile (groupadd, useradd, home dir /home/huskies/.claude)
|
||||||
|
- [ ] MCP server registration renamed from 'storkit' to 'huskies' in scaffold-generated .mcp.json and in server/src/http/mcp/mod.rs serverInfo name
|
||||||
|
- [ ] All 35+ MCP tool permission patterns updated from mcp__storkit__* to mcp__huskies__* across code and permission configs
|
||||||
|
- [ ] The .storkit/ project directory marker renamed to .huskies/ throughout all Rust source (paths.rs, config.rs, scaffold.rs, watcher.rs, prompts.rs, and all agent/pipeline code)
|
||||||
|
- [ ] Release script updated: Gitea repo path dave/storkit → dave/huskies, changelog regex updated to match ^(huskies|storkit|story-kit): for backwards-compatible history parsing, binary artifact names updated
|
||||||
|
- [ ] Git commit prefix convention updated from 'storkit:' to 'huskies:' in storkit README and agent prompts
|
||||||
|
- [ ] Website updated: page title, headings, and contact email (hello@storkit.dev) if domain changes
|
||||||
|
- [ ] README.md updated: all CLI examples use 'huskies' binary name, all .storkit/ references become .huskies/
|
||||||
|
- [ ] A migration path exists for existing installs: either storkit auto-detects and migrates .storkit/ → .huskies/, or a migration script (script/migrate) is provided
|
||||||
|
- [ ] All Claude Code .mcp.json files in existing worktrees are regenerated via scaffold or migration
|
||||||
|
- [ ] Gitea repository renamed from dave/storkit to dave/huskies (external action required, noted in story)
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- TBD
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
name: "Zombie process accumulation from unrereaped child processes"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 452: Zombie process accumulation from unrereaped child processes
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Storkit accumulates zombie processes over time from unrereaped child and grandchild processes. Observed 101 zombies in Docker container, 27 on macOS host. Breakdown: 51 esbuild, 36 echo, 5 claude, 5 sh, 2 bash, 1 cargo.
|
||||||
|
|
||||||
|
Root cause: storkit does not reap orphaned grandchild processes. The zombies are mostly grandchildren (`esbuild`, `echo`, `sh`, `cargo`) spawned by `npm run build`, `cargo test`, etc. during worktree setup and gate checks. This happens both natively (observed 27 zombies on macOS host) and in Docker containers. When the intermediate parent exits, these grandchildren get reparented to storkit (or PID 1 in Docker) and become zombies because nobody calls `waitpid` for them.
|
||||||
|
|
||||||
|
**Already fixed:**
|
||||||
|
- `docker-compose.yml` now has `init: true` which uses tini as PID 1 in Docker — this handles zombie reaping inside containers
|
||||||
|
- `llm/providers/claude_code.rs` now has `child.wait()` after `child.kill()` in all code paths, and the reader thread is joined before returning
|
||||||
|
- `agents/pty.rs` reader thread is now joined before returning
|
||||||
|
|
||||||
|
**Remaining:** Storkit running natively (e.g. on macOS) still accumulates zombie grandchildren because there is no tini. The fix is to add a background reaper thread that periodically calls `waitpid(-1, WNOHANG)` in a loop to clean up any orphaned children. This should be spawned early in `main()` on Unix platforms. Example:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::thread::spawn(|| {
|
||||||
|
loop {
|
||||||
|
unsafe { while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {} }
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Run several agent sessions. Check with `ps -eo stat,comm | grep Z | awk '{print $2}' | sort | uniq -c | sort -rn`.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Zombie processes accumulate continuously. Never reaped.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
No zombie accumulation during normal operation.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [x] `child.wait()` is called after `child.kill()` in all code paths in `claude_code.rs`
|
||||||
|
- [x] Reader threads are joined in both `pty.rs` and `claude_code.rs`
|
||||||
|
- [x] `init: true` added to docker-compose.yml for Docker deployments
|
||||||
|
- [ ] Background reaper thread added for native (non-Docker) deployments
|
||||||
|
- [ ] Verified with `ps aux | grep '<defunct>'` after running multiple agent sessions natively on macOS
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: "Deduplicate work item display in web UI story panel"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Story 454: Deduplicate work item display in web UI story panel
|
||||||
|
|
||||||
|
## User Story
|
||||||
|
|
||||||
|
As a user, I want the work item detail panel to display cleanly without redundant information, so that I can read story details without noise.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] The story title is not shown twice (remove the duplicate heading)
|
||||||
|
- [ ] The work item type label is not shown twice
|
||||||
|
- [ ] The word 'name' is not shown as a prefix before the story title
|
||||||
|
- [ ] The story ID/title line (e.g. 'Story 3: ...') is left-justified with no extra indentation
|
||||||
|
|
||||||
|
## Out of Scope
|
||||||
|
|
||||||
|
- TBD
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: "Matrix bot ignores in-room verification requests from Element"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 456: Matrix bot ignores in-room verification requests from Element
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The Matrix bot (Sally) only registers a handler for to-device verification events (`ToDeviceKeyVerificationRequestEvent`). Modern Element clients use in-room verification (`m.key.verification.request` as a room message event) by default. When a user initiates "Start Verification" from Element, the request is sent as a room event and the bot never sees it — nothing appears in the bot logs and the verification flow hangs indefinitely. As a result, Sally's device remains unverified (Big Red Dot), and if Element has "never send to unverified sessions" enabled, it will not share Megolm room keys with Sally's device, making her deaf to all encrypted room messages.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
1. Run the storkit Matrix bot (Sally) in a room with E2EE enabled. 2. In Element, open the room member list, click Sally's device, and press "Start Verification". 3. Watch the bot logs: grep for "verif\|Incoming".
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Nothing appears in the bot logs. The verification flow hangs in Element and eventually times out. Sally's device remains unverified. If Element is set to encrypt only to verified sessions, Sally cannot decrypt any messages in the room.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
The bot receives the in-room verification request, accepts it, drives the SAS emoji flow to completion, and logs "Verification with @user completed successfully!". Sally's device shows as verified in Element.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Bot registers an in-room verification event handler for m.key.verification.request room events (in addition to the existing to-device handler)
|
||||||
|
- [ ] When Element initiates 'Start Verification' from the device list, the bot logs 'Incoming verification request from ...'
|
||||||
|
- [ ] The SAS emoji flow completes: bot logs the emoji string, confirms, and logs 'Verification ... completed successfully!'
|
||||||
|
- [ ] Sally's device shows as verified (no Big Red Dot) in Element after the flow completes
|
||||||
|
- [ ] Existing to-device verification handler is preserved for clients that use the older flow
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "OAuth callback URL ignores --port CLI flag"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 449: OAuth callback URL ignores --port CLI flag
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
OAuthState is initialized with `resolve_port()` (reads STORKIT_PORT env var, defaults to 3001) instead of the actual port the server is listening on. When the server is started with `--port 4000`, the OAuth callback URL is still generated as `http://localhost:3001/callback`, so the Anthropic redirect lands on the wrong server and the state parameter lookup fails with "Unknown or expired state parameter".
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Start storkit with `--port 4000` (without setting STORKIT_PORT env var). Click the OAuth login button in the web UI. Authenticate with Anthropic. The callback redirect goes to localhost:3001 instead of localhost:4000.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Callback hits port 3001 (or wrong port). If a different storkit is running there, it returns "Invalid State". If nothing is running there, the page fails to load.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
Callback URL should use the actual server port (from --port CLI flag), so the redirect returns to the correct server instance.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] build_routes receives the actual listening port and passes it to OAuthState::new
|
||||||
|
- [ ] OAuth callback URL matches the port the server is actually listening on
|
||||||
|
- [ ] Works with --port flag, STORKIT_PORT env var, and default port
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "Web UI silently swallows chat errors including OAuth login link"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 450: Web UI silently swallows chat errors including OAuth login link
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
When the WebSocket chat returns an error (e.g. OAuth authentication failed with a login URL), the `onError` handler in `Chat.tsx` only logs to `console.error` and resets loading state. The error message is never displayed to the user. This means the OAuth login link from story #448 works on Matrix/WhatsApp but is invisible in the web UI.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Use the web UI with missing or expired OAuth credentials. Send any chat message. The server detects auth failure, attempts token refresh, fails, and returns an error containing a login URL over WebSocket.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Nothing visible happens. The error is logged to browser console only. The user sees no feedback.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
The error message (including the clickable OAuth login link) should be displayed in the chat as an assistant message so the user can act on it.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] WebSocket error messages are displayed in the chat UI as assistant messages
|
||||||
|
- [ ] OAuth login URL in the error is rendered as a clickable link
|
||||||
|
- [ ] Consistent with how Matrix and WhatsApp transports display the same error
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: "Chat.test.tsx /help test expects removed overlay behavior"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 451: Chat.test.tsx /help test expects removed overlay behavior
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
The test `AC: /help shows help overlay` in `Chat.test.tsx:1645` expects `/help` to show the `help-overlay` testid element. However, the `/help` intercept was removed from Chat.tsx and `help` was added to `knownCommands`, so `/help` now goes through `api.botCommand()` like other commands. The test needs to be updated to expect a `botCommand("help", ...)` call instead of the overlay. This is blocking gates on stories 449 and 450.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
Run `cd frontend && npm test` — the test `AC: /help shows help overlay` fails.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
Test fails: `findByTestId("help-overlay")` times out because the overlay is never rendered. `/help` is dispatched to the backend via `botCommand` instead.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
Test should pass by expecting `/help` to call `api.botCommand("help", ...)` and display the response in chat, consistent with the current code behavior.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] The /help test in Chat.test.tsx is updated to expect botCommand dispatch
|
||||||
|
- [ ] All frontend tests pass
|
||||||
|
- [ ] HelpOverlay component and showHelp state can be removed from Chat.tsx if no longer used
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: "Agent PTY crashes with fatal runtime error on restart after gate failure"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug 453: Agent PTY crashes with fatal runtime error on restart after gate failure
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
When an agent completes coding and the acceptance gates fail (e.g. a test failure), the pipeline restarts the agent on the same worktree. The restarted Claude Code PTY process crashes immediately with `fatal runtime error: assertion failed: output.write(&bytes).is_ok(), aborting`. The process exits in the same second it spawns (Session: None), burns through all 3 retries, and blocks the story.
|
||||||
|
|
||||||
|
Key observations:
|
||||||
|
- The crash is **deterministic, not intermittent**: the first PTY spawn in a worktree always works; the second spawn (restart) always crashes
|
||||||
|
- Running `claude -p "hello"` manually in the same worktree works fine (no crash) — the issue is specific to spawning via portable-pty
|
||||||
|
- The worktree is clean (all changes committed) — the agent has nothing to do but fix the gate failure
|
||||||
|
- The crash is inside the Claude Code binary, not storkit code
|
||||||
|
- Observed on every story that needed a restart: 329, 400, 420, 438, 446, 449, 450
|
||||||
|
- Stories that passed gates on the first run were never affected — they never triggered a second spawn
|
||||||
|
|
||||||
|
Likely cause: the reader thread spawned by `std::thread::spawn` in `pty.rs` (line 248-255) is never joined. After `run_agent_pty_streaming` returns, the pipeline immediately calls `start_agent` for the retry, but the old reader thread may still be running and holding a cloned PTY reader fd. The new PTY allocation could collide with the still-open fd from the previous session.
|
||||||
|
|
||||||
|
The root cause is unknown. It is NOT caused by zombie process accumulation (that is a separate issue in #452).
|
||||||
|
|
||||||
|
**Timeline:** The crash first appeared on 2026-03-21. Agent logs go back to 2026-02-23 with no instances before that date. Stories that hit it: 329 (Mar 21), 400 (Mar 26), 420 (Mar 28), 438 (Mar 28), 446 (Mar 30), 449 (Mar 31), 450 (Mar 31).
|
||||||
|
|
||||||
|
**Suspect commits around 2026-03-21:**
|
||||||
|
- `4344081b` — storkit: merge 343_refactor_abstract_agent_runtime_to_support_non_claude_code_backends (refactored agent runtime layer)
|
||||||
|
- `c4e45b28` — The great storkit name conversion
|
||||||
|
- Story 359 — Docker security hardening (`cap_drop: ALL`, added back only `SETUID`/`SETGID`) — could affect PTY allocation
|
||||||
|
- Story 329 — Docker/OrbStack evaluation spike (first crash was on this story's mergemaster)
|
||||||
|
|
||||||
|
**Ruled out:** Docker capability restrictions (cap_drop: ALL) — tested by temporarily removing all cap_drop/security_opt; crash still occurs.
|
||||||
|
|
||||||
|
**Evidence of stale PTY fd:** After all agents stopped, storkit (PID 7) was still holding an open fd to `/dev/pts/ptmx` (fd 46). This is a leaked PTY master fd from a previous agent session. The reader thread spawned by `std::thread::spawn` in `pty.rs` is never joined, so the cloned reader fd stays open in the storkit process after the agent exits.
|
||||||
|
|
||||||
|
Remaining areas to investigate: the unjoined reader thread leaking PTY fds, and whether the leaked fd from the first session interferes with the second PTY allocation.
|
||||||
|
|
||||||
|
## How to Reproduce
|
||||||
|
|
||||||
|
1. Have a story in current stage with committed code in its worktree. 2. Introduce a test failure that causes gates to fail. 3. The pipeline restarts the agent on the same worktree. 4. The Claude Code process crashes immediately on spawn.
|
||||||
|
|
||||||
|
## Actual Result
|
||||||
|
|
||||||
|
`fatal runtime error: assertion failed: output.write(&bytes).is_ok(), aborting` — process exits instantly (same second as spawn), Session: None. Burns through retries and blocks the story.
|
||||||
|
|
||||||
|
## Expected Result
|
||||||
|
|
||||||
|
The restarted agent should start successfully, receive the gate failure context, and be able to fix the issue.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- [ ] Agent restart after gate failure successfully spawns a Claude Code PTY session
|
||||||
|
- [ ] No fatal runtime error on PTY restart in a worktree with prior committed work
|
||||||
|
- [ ] If Claude Code fails to start, the error is handled gracefully without burning retries
|
||||||
Generated
+74
-79
@@ -1497,18 +1497,18 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hybrid-array"
|
name = "hybrid-array"
|
||||||
version = "0.4.9"
|
version = "0.4.10"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1a79f2aff40c18ab8615ddc5caa9eb5b96314aef18fe5823090f204ad988e813"
|
checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.8.1"
|
version = "1.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
|
checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -1521,7 +1521,6 @@ dependencies = [
|
|||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"pin-utils",
|
|
||||||
"smallvec",
|
"smallvec",
|
||||||
"tokio",
|
"tokio",
|
||||||
"want",
|
"want",
|
||||||
@@ -1595,12 +1594,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_collections"
|
name = "icu_collections"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
|
checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"potential_utf",
|
"potential_utf",
|
||||||
|
"utf8_iter",
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@@ -1608,9 +1608,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_locale_core"
|
name = "icu_locale_core"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
|
checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"litemap",
|
"litemap",
|
||||||
@@ -1621,9 +1621,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer"
|
name = "icu_normalizer"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
|
checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_normalizer_data",
|
"icu_normalizer_data",
|
||||||
@@ -1635,15 +1635,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_normalizer_data"
|
name = "icu_normalizer_data"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
|
checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties"
|
name = "icu_properties"
|
||||||
version = "2.1.2"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec"
|
checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"icu_collections",
|
"icu_collections",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@@ -1655,15 +1655,15 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_properties_data"
|
name = "icu_properties_data"
|
||||||
version = "2.1.2"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af"
|
checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "icu_provider"
|
name = "icu_provider"
|
||||||
version = "2.1.1"
|
version = "2.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
|
checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"icu_locale_core",
|
"icu_locale_core",
|
||||||
@@ -1915,9 +1915,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.93"
|
version = "0.3.94"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d"
|
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
@@ -2003,9 +2003,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.183"
|
version = "0.2.184"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libredox"
|
name = "libredox"
|
||||||
@@ -2038,9 +2038,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "litemap"
|
name = "litemap"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
|
checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
@@ -2764,12 +2764,6 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pin-utils"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pkcs8"
|
name = "pkcs8"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
@@ -2923,9 +2917,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.4"
|
version = "0.1.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
|
checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
@@ -3879,9 +3873,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.0"
|
version = "1.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
@@ -4083,7 +4077,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "storkit"
|
name = "storkit"
|
||||||
version = "0.8.4"
|
version = "0.8.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -4094,6 +4088,7 @@ dependencies = [
|
|||||||
"futures",
|
"futures",
|
||||||
"homedir",
|
"homedir",
|
||||||
"ignore",
|
"ignore",
|
||||||
|
"libc",
|
||||||
"libsqlite3-sys",
|
"libsqlite3-sys",
|
||||||
"matrix-sdk",
|
"matrix-sdk",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
@@ -4115,7 +4110,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.29.0",
|
"tokio-tungstenite 0.29.0",
|
||||||
"toml 1.1.0+spec-1.1.0",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"uuid",
|
"uuid",
|
||||||
"wait-timeout",
|
"wait-timeout",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
@@ -4336,9 +4331,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tinystr"
|
name = "tinystr"
|
||||||
version = "0.8.2"
|
version = "0.8.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
@@ -4462,14 +4457,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8195ca05e4eb728f4ba94f3e3291661320af739c4e43779cbdfae82ab239fcc"
|
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned",
|
||||||
"toml_datetime 1.1.0+spec-1.1.0",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
@@ -4486,39 +4481,39 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_datetime"
|
name = "toml_datetime"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f"
|
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.8+spec-1.1.0"
|
version = "0.25.10+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c"
|
checksum = "a82418ca169e235e6c399a84e395ab6debeb3bc90edc959bf0f48647c6a32d1b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime 1.1.0+spec-1.1.0",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_parser"
|
name = "toml_parser"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow 1.0.1",
|
"winnow 1.0.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.1.0+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tower"
|
name = "tower"
|
||||||
@@ -4674,9 +4669,9 @@ checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typewit"
|
name = "typewit"
|
||||||
version = "1.14.2"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8c1ae7cc0fdb8b842d65d127cb981574b0d2b249b74d1c7a2986863dc134f71"
|
checksum = "06fee3a8df48c50c55ad646a4e03b00a370da6fe1850ebf467a8d0165dfcafae"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typewit_proc_macros",
|
"typewit_proc_macros",
|
||||||
]
|
]
|
||||||
@@ -4915,9 +4910,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.116"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631"
|
checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
@@ -4928,9 +4923,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-futures"
|
name = "wasm-bindgen-futures"
|
||||||
version = "0.4.66"
|
version = "0.4.67"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19280959e2844181895ef62f065c63e0ca07ece4771b53d89bfdb967d97cbf05"
|
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -4938,9 +4933,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro"
|
name = "wasm-bindgen-macro"
|
||||||
version = "0.2.116"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537"
|
checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
@@ -4948,9 +4943,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-macro-support"
|
name = "wasm-bindgen-macro-support"
|
||||||
version = "0.2.116"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac"
|
checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -4961,9 +4956,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen-shared"
|
name = "wasm-bindgen-shared"
|
||||||
version = "0.2.116"
|
version = "0.2.117"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633"
|
checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
@@ -5048,9 +5043,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web-sys"
|
name = "web-sys"
|
||||||
version = "0.3.93"
|
version = "0.3.94"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "749466a37ee189057f54748b200186b59a03417a117267baf3fd89cecc9fb837"
|
checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
@@ -5659,9 +5654,9 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke"
|
name = "yoke"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
|
checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"stable_deref_trait",
|
"stable_deref_trait",
|
||||||
"yoke-derive",
|
"yoke-derive",
|
||||||
@@ -5670,9 +5665,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "yoke-derive"
|
name = "yoke-derive"
|
||||||
version = "0.8.1"
|
version = "0.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5702,18 +5697,18 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom"
|
name = "zerofrom"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
|
checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"zerofrom-derive",
|
"zerofrom-derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerofrom-derive"
|
name = "zerofrom-derive"
|
||||||
version = "0.1.6"
|
version = "0.1.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
@@ -5743,9 +5738,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.3"
|
version = "0.2.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
|
checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
"yoke",
|
"yoke",
|
||||||
@@ -5754,9 +5749,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec"
|
name = "zerovec"
|
||||||
version = "0.11.5"
|
version = "0.11.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
@@ -5765,9 +5760,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerovec-derive"
|
name = "zerovec-derive"
|
||||||
version = "0.11.2"
|
version = "0.11.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|||||||
@@ -40,3 +40,4 @@ pulldown-cmark = { version = "0.13.3", default-features = false, features = [
|
|||||||
"html",
|
"html",
|
||||||
] }
|
] }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
libc = "0.2"
|
||||||
|
|||||||
@@ -108,6 +108,11 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 10s
|
start_period: 10s
|
||||||
|
|
||||||
|
# Use tini as PID 1 to reap zombie child processes.
|
||||||
|
# Without this, grandchild processes (esbuild, cargo, etc.) spawned by
|
||||||
|
# npm/cargo during worktree setup and gate checks become zombies.
|
||||||
|
init: true
|
||||||
|
|
||||||
# Restart policy – restart on crash but not on manual stop
|
# Restart policy – restart on crash but not on manual stop
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"version": "0.8.4",
|
"version": "0.8.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"version": "0.8.4",
|
"version": "0.8.6",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/react-syntax-highlighter": "^15.5.13",
|
"@types/react-syntax-highlighter": "^15.5.13",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "living-spec-standalone",
|
"name": "living-spec-standalone",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.8.4",
|
"version": "0.8.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -66,7 +66,12 @@ describe("App", () => {
|
|||||||
mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false);
|
mockedApi.getAnthropicApiKeyExists.mockResolvedValue(false);
|
||||||
mockedApi.getAnthropicModels.mockResolvedValue([]);
|
mockedApi.getAnthropicModels.mockResolvedValue([]);
|
||||||
mockedApi.getModelPreference.mockResolvedValue(null);
|
mockedApi.getModelPreference.mockResolvedValue(null);
|
||||||
mockedApi.getOAuthStatus.mockResolvedValue({ authenticated: false, expired: false, expires_at: 0, has_refresh_token: false });
|
mockedApi.getOAuthStatus.mockResolvedValue({
|
||||||
|
authenticated: false,
|
||||||
|
expired: false,
|
||||||
|
expires_at: 0,
|
||||||
|
has_refresh_token: false,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function renderApp() {
|
async function renderApp() {
|
||||||
|
|||||||
@@ -1642,7 +1642,10 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC: /help shows help overlay", async () => {
|
it("AC: /help calls botCommand and displays response", async () => {
|
||||||
|
mockedApi.botCommand.mockResolvedValue({
|
||||||
|
response: "Available commands: status, help, ...",
|
||||||
|
});
|
||||||
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
@@ -1658,9 +1661,10 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
fireEvent.keyDown(input, { key: "Enter", shiftKey: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(await screen.findByTestId("help-overlay")).toBeInTheDocument();
|
await waitFor(() => {
|
||||||
|
expect(mockedApi.botCommand).toHaveBeenCalledWith("help", "", undefined);
|
||||||
|
});
|
||||||
expect(lastSendChatArgs).toBeNull();
|
expect(lastSendChatArgs).toBeNull();
|
||||||
expect(mockedApi.botCommand).not.toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("AC: botCommand API error shows error message in chat", async () => {
|
it("AC: botCommand API error shows error message in chat", async () => {
|
||||||
@@ -1685,3 +1689,42 @@ describe("Slash command handling (Story 374)", () => {
|
|||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Bug 450: WebSocket error messages displayed in chat", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
capturedWsHandlers = null;
|
||||||
|
setupMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC1: WebSocket error message is shown in chat as an assistant message", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onError("Something went wrong on the server.");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByText("Something went wrong on the server."),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("AC2: OAuth login URL in WebSocket error is rendered as a clickable link", async () => {
|
||||||
|
render(<Chat projectPath="/tmp/project" onCloseProject={vi.fn()} />);
|
||||||
|
|
||||||
|
await waitFor(() => expect(capturedWsHandlers).not.toBeNull());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
capturedWsHandlers?.onError(
|
||||||
|
"OAuth login required. Please visit: https://example.com/oauth/login",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const link = await screen.findByRole("link", {
|
||||||
|
name: /https:\/\/example\.com\/oauth\/login/,
|
||||||
|
});
|
||||||
|
expect(link).toBeInTheDocument();
|
||||||
|
expect(link).toHaveAttribute("href", "https://example.com/oauth/login");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -168,7 +168,11 @@ interface ChatProps {
|
|||||||
oauthStatus?: OAuthStatus | null;
|
oauthStatus?: OAuthStatus | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Chat({ projectPath, onCloseProject, oauthStatus = null }: ChatProps) {
|
export function Chat({
|
||||||
|
projectPath,
|
||||||
|
onCloseProject,
|
||||||
|
oauthStatus = null,
|
||||||
|
}: ChatProps) {
|
||||||
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
|
const { messages, setMessages, clearMessages } = useChatHistory(projectPath);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [model, setModel] = useState("claude-code-pty");
|
const [model, setModel] = useState("claude-code-pty");
|
||||||
@@ -409,6 +413,14 @@ export function Chat({ projectPath, onCloseProject, oauthStatus = null }: ChatPr
|
|||||||
console.error("WebSocket error:", message);
|
console.error("WebSocket error:", message);
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
setActivityStatus(null);
|
setActivityStatus(null);
|
||||||
|
const markdownMessage = message.replace(
|
||||||
|
/(https?:\/\/[^\s]+)/g,
|
||||||
|
"[$1]($1)",
|
||||||
|
);
|
||||||
|
setMessages((prev) => [
|
||||||
|
...prev,
|
||||||
|
{ role: "assistant", content: markdownMessage },
|
||||||
|
]);
|
||||||
if (queuedMessagesRef.current.length > 0) {
|
if (queuedMessagesRef.current.length > 0) {
|
||||||
const batch = queuedMessagesRef.current.map((item) => item.text);
|
const batch = queuedMessagesRef.current.map((item) => item.text);
|
||||||
queuedMessagesRef.current = [];
|
queuedMessagesRef.current = [];
|
||||||
|
|||||||
@@ -349,7 +349,11 @@ export function ChatHeader({
|
|||||||
type="button"
|
type="button"
|
||||||
title="Authenticate with Claude via OAuth"
|
title="Authenticate with Claude via OAuth"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open("/oauth/authorize", "_blank", "noopener,noreferrer");
|
window.open(
|
||||||
|
"/oauth/authorize",
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: "6px 12px",
|
padding: "6px 12px",
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("WorkItemDetailPanel", () => {
|
describe("WorkItemDetailPanel", () => {
|
||||||
it("renders the story name in the header", async () => {
|
it("renders the story name in the header with type and ID prefix", async () => {
|
||||||
render(
|
render(
|
||||||
<WorkItemDetailPanel
|
<WorkItemDetailPanel
|
||||||
storyId="237_bug_test"
|
storyId="237_bug_test"
|
||||||
@@ -79,7 +79,7 @@ describe("WorkItemDetailPanel", () => {
|
|||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
|
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
|
||||||
"Big Title Story",
|
"Bug 237: Big Title Story",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -110,6 +110,10 @@ describe("WorkItemDetailPanel", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders markdown headings with constrained inline font size", async () => {
|
it("renders markdown headings with constrained inline font size", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
...DEFAULT_CONTENT,
|
||||||
|
content: "# Title Heading\n\n## Section Heading\n\nSome content.",
|
||||||
|
});
|
||||||
render(
|
render(
|
||||||
<WorkItemDetailPanel
|
<WorkItemDetailPanel
|
||||||
storyId="237_bug_test"
|
storyId="237_bug_test"
|
||||||
@@ -119,11 +123,95 @@ describe("WorkItemDetailPanel", () => {
|
|||||||
);
|
);
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const content = screen.getByTestId("detail-panel-content");
|
const content = screen.getByTestId("detail-panel-content");
|
||||||
const h1 = content.querySelector("h1");
|
// H1 is stripped by stripDisplayContent; h2 should be constrained
|
||||||
expect(h1).not.toBeNull();
|
const h2 = content.querySelector("h2");
|
||||||
expect(h1?.style.fontSize).toBeTruthy();
|
expect(h2).not.toBeNull();
|
||||||
|
expect(h2?.style.fontSize).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("strips YAML front matter so 'name' is not shown as a prefix in content", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content:
|
||||||
|
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...',
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const content = await screen.findByTestId("detail-panel-content");
|
||||||
|
expect(content.textContent).not.toMatch(/name:/i);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("strips the first H1 heading so the story title is not shown twice", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content:
|
||||||
|
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nAs a user...',
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const content = await screen.findByTestId("detail-panel-content");
|
||||||
|
expect(content.querySelector("h1")).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows 'Type N: Name' format in the panel header title (story ID/title left-justified)", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content: "## User Story\n\nAs a user...",
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByTestId("detail-panel-title")).toHaveTextContent(
|
||||||
|
"Story 42: My Story Name",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not show the work item type label twice when front matter and H1 are stripped", async () => {
|
||||||
|
mockedGetWorkItemContent.mockResolvedValue({
|
||||||
|
content:
|
||||||
|
'---\nname: "My Story Name"\n---\n\n# Story 42: My Story Name\n\n## User Story\n\nContent.',
|
||||||
|
stage: "current",
|
||||||
|
name: "My Story Name",
|
||||||
|
agent: null,
|
||||||
|
});
|
||||||
|
render(
|
||||||
|
<WorkItemDetailPanel
|
||||||
|
storyId="42_story_test"
|
||||||
|
pipelineVersion={0}
|
||||||
|
onClose={() => {}}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
await screen.findByTestId("detail-panel-content");
|
||||||
|
// "Story" type label appears exactly once — in the panel header title
|
||||||
|
const title = screen.getByTestId("detail-panel-title");
|
||||||
|
expect(title.textContent).toContain("Story 42:");
|
||||||
|
// The content body should not contain an H1 repeating the type + title
|
||||||
|
const content = screen.getByTestId("detail-panel-content");
|
||||||
|
expect(content.querySelector("h1")).toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("WorkItemDetailPanel - Agent Logs", () => {
|
describe("WorkItemDetailPanel - Agent Logs", () => {
|
||||||
|
|||||||
@@ -17,6 +17,46 @@ import { api } from "../api/client";
|
|||||||
|
|
||||||
const { useCallback, useEffect, useRef, useState } = React;
|
const { useCallback, useEffect, useRef, useState } = React;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip YAML front matter and the first H1 heading from story content before
|
||||||
|
* rendering. The panel header already shows the story ID/title, so rendering
|
||||||
|
* them again inside the markdown body creates duplicate information.
|
||||||
|
*/
|
||||||
|
function stripDisplayContent(content: string): string {
|
||||||
|
let text = content;
|
||||||
|
// Strip YAML front matter (--- ... ---)
|
||||||
|
if (text.startsWith("---")) {
|
||||||
|
const eol = text.indexOf("\n");
|
||||||
|
if (eol !== -1) {
|
||||||
|
const closeIdx = text.indexOf("\n---", eol);
|
||||||
|
if (closeIdx !== -1) {
|
||||||
|
text = text.slice(closeIdx + 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Trim leading blank lines left by the front matter
|
||||||
|
text = text.trimStart();
|
||||||
|
// Strip the first H1 heading — it duplicates the panel header title
|
||||||
|
if (text.startsWith("# ")) {
|
||||||
|
const eol = text.indexOf("\n");
|
||||||
|
text = eol !== -1 ? text.slice(eol + 1).trimStart() : "";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format the story ID/title line shown in the panel header.
|
||||||
|
* Produces e.g. "Story 454: My Story Name" or "Bug 12: Crash on startup".
|
||||||
|
* Falls back to name or storyId when the pattern doesn't match.
|
||||||
|
*/
|
||||||
|
function formatStoryTitle(storyId: string, name: string | null): string {
|
||||||
|
const match = storyId.match(/^(\d+)_([a-z]+)_/);
|
||||||
|
if (!match || !name) return name ?? storyId;
|
||||||
|
const [, number, type] = match;
|
||||||
|
const typeLabel = type.charAt(0).toUpperCase() + type.slice(1);
|
||||||
|
return `${typeLabel} ${number}: ${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
const STAGE_LABELS: Record<string, string> = {
|
const STAGE_LABELS: Record<string, string> = {
|
||||||
backlog: "Backlog",
|
backlog: "Backlog",
|
||||||
current: "Current",
|
current: "Current",
|
||||||
@@ -352,7 +392,7 @@ export function WorkItemDetailPanel({
|
|||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{name ?? storyId}
|
{formatStoryTitle(storyId, name)}
|
||||||
</div>
|
</div>
|
||||||
{stage && (
|
{stage && (
|
||||||
<div
|
<div
|
||||||
@@ -504,7 +544,7 @@ export function WorkItemDetailPanel({
|
|||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{content}
|
{stripDisplayContent(content)}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -66,7 +66,11 @@ export function SelectionScreen({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open("/oauth/authorize", "_blank", "noopener,noreferrer");
|
window.open(
|
||||||
|
"/oauth/authorize",
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
padding: "8px 16px",
|
padding: "8px 16px",
|
||||||
@@ -78,7 +82,9 @@ export function SelectionScreen({
|
|||||||
fontSize: "0.9em",
|
fontSize: "0.9em",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{oauthStatus.expired ? "Re-authenticate with Claude" : "Login with Claude"}
|
{oauthStatus.expired
|
||||||
|
? "Re-authenticate with Claude"
|
||||||
|
: "Login with Claude"}
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
|
|||||||
+4
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "storkit"
|
name = "storkit"
|
||||||
version = "0.8.4"
|
version = "0.8.6"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
@@ -38,6 +38,9 @@ regex = { workspace = true }
|
|||||||
libsqlite3-sys = { version = "0.35.0", features = ["bundled"] }
|
libsqlite3-sys = { version = "0.35.0", features = ["bundled"] }
|
||||||
wait-timeout = "0.2.1"
|
wait-timeout = "0.2.1"
|
||||||
|
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
libc = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
tokio-tungstenite = { workspace = true }
|
tokio-tungstenite = { workspace = true }
|
||||||
|
|||||||
@@ -245,13 +245,16 @@ fn run_agent_pty_blocking(
|
|||||||
// via recv_timeout: if no output arrives within the configured window
|
// via recv_timeout: if no output arrives within the configured window
|
||||||
// the process is killed and the agent is marked Failed.
|
// the process is killed and the agent is marked Failed.
|
||||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<std::io::Result<String>>();
|
let (line_tx, line_rx) = std::sync::mpsc::channel::<std::io::Result<String>>();
|
||||||
std::thread::spawn(move || {
|
let sid_for_reader = story_id.to_string();
|
||||||
|
let aname_for_reader = agent_name.to_string();
|
||||||
|
let reader_handle = std::thread::spawn(move || {
|
||||||
let buf_reader = BufReader::new(reader);
|
let buf_reader = BufReader::new(reader);
|
||||||
for line in buf_reader.lines() {
|
for line in buf_reader.lines() {
|
||||||
if line_tx.send(line).is_err() {
|
if line_tx.send(line).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
slog!("[agent:{sid_for_reader}:{aname_for_reader}] Reader thread exiting");
|
||||||
});
|
});
|
||||||
|
|
||||||
let timeout_dur = if inactivity_timeout_secs > 0 {
|
let timeout_dur = if inactivity_timeout_secs > 0 {
|
||||||
@@ -424,6 +427,15 @@ fn run_agent_pty_blocking(
|
|||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
let _ = child.wait();
|
let _ = child.wait();
|
||||||
|
|
||||||
|
// Wait for the reader thread to finish so it releases the cloned PTY
|
||||||
|
// master fd before we return. Without this, the next PTY spawn for the
|
||||||
|
// same story can collide with a still-open fd from this session (#453).
|
||||||
|
slog!("[agent:{story_id}:{agent_name}] Waiting for reader thread to exit");
|
||||||
|
if let Err(e) = reader_handle.join() {
|
||||||
|
slog!("[agent:{story_id}:{agent_name}] Reader thread panicked: {e:?}");
|
||||||
|
}
|
||||||
|
slog!("[agent:{story_id}:{agent_name}] Reader thread joined");
|
||||||
|
|
||||||
slog!(
|
slog!(
|
||||||
"[agent:{story_id}:{agent_name}] Done. Session: {:?}",
|
"[agent:{story_id}:{agent_name}] Done. Session: {:?}",
|
||||||
session_id
|
session_id
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use super::context::BotContext;
|
|||||||
use super::format::{format_startup_announcement, markdown_to_html};
|
use super::format::{format_startup_announcement, markdown_to_html};
|
||||||
use super::history::load_history;
|
use super::history::load_history;
|
||||||
use super::messages::on_room_message;
|
use super::messages::on_room_message;
|
||||||
use super::verification::on_to_device_verification_request;
|
use super::verification::{on_room_verification_request, on_to_device_verification_request};
|
||||||
|
|
||||||
/// Connect to the Matrix homeserver, join all configured rooms, and start
|
/// Connect to the Matrix homeserver, join all configured rooms, and start
|
||||||
/// listening for messages. Runs the full Matrix sync loop — call from a
|
/// listening for messages. Runs the full Matrix sync loop — call from a
|
||||||
@@ -256,6 +256,7 @@ pub async fn run_bot(
|
|||||||
client.add_event_handler_context(ctx);
|
client.add_event_handler_context(ctx);
|
||||||
client.add_event_handler(on_room_message);
|
client.add_event_handler(on_room_message);
|
||||||
client.add_event_handler(on_to_device_verification_request);
|
client.add_event_handler(on_to_device_verification_request);
|
||||||
|
client.add_event_handler(on_room_verification_request);
|
||||||
|
|
||||||
// Spawn the stage-transition notification listener before entering the
|
// Spawn the stage-transition notification listener before entering the
|
||||||
// sync loop so it starts receiving watcher events immediately.
|
// sync loop so it starts receiving watcher events immediately.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use matrix_sdk::encryption::verification::{
|
|||||||
};
|
};
|
||||||
use matrix_sdk::ruma::OwnedUserId;
|
use matrix_sdk::ruma::OwnedUserId;
|
||||||
use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
|
use matrix_sdk::ruma::events::key::verification::request::ToDeviceKeyVerificationRequestEvent;
|
||||||
|
use matrix_sdk::ruma::events::room::message::{MessageType, OriginalSyncRoomMessageEvent};
|
||||||
|
|
||||||
/// Check whether the sender has a cross-signing identity known to the bot.
|
/// Check whether the sender has a cross-signing identity known to the bot.
|
||||||
///
|
///
|
||||||
@@ -94,6 +95,74 @@ pub(super) async fn on_to_device_verification_request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handle an incoming in-room verification request (Element's default flow).
|
||||||
|
/// Modern Element sends `m.key.verification.request` as an `m.room.message`
|
||||||
|
/// event rather than a to-device event. We look for that message type and
|
||||||
|
/// drive the same SAS flow as the to-device handler.
|
||||||
|
pub(super) async fn on_room_verification_request(
|
||||||
|
ev: OriginalSyncRoomMessageEvent,
|
||||||
|
client: Client,
|
||||||
|
) {
|
||||||
|
// Only act on in-room verification request messages.
|
||||||
|
if !matches!(ev.content.msgtype, MessageType::VerificationRequest(_)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
slog!(
|
||||||
|
"[matrix-bot] Incoming in-room verification request from {} (event: {})",
|
||||||
|
ev.sender,
|
||||||
|
ev.event_id
|
||||||
|
);
|
||||||
|
|
||||||
|
// For in-room flows the flow_id is the event ID of the request event.
|
||||||
|
let Some(request) = client
|
||||||
|
.encryption()
|
||||||
|
.get_verification_request(&ev.sender, ev.event_id.as_str())
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
slog!("[matrix-bot] Could not locate in-room verification request in crypto store");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = request.accept().await {
|
||||||
|
slog!("[matrix-bot] Failed to accept in-room verification request: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to start a SAS flow. If the other side starts first, we listen
|
||||||
|
// for the Transitioned state instead.
|
||||||
|
match request.start_sas().await {
|
||||||
|
Ok(Some(sas)) => {
|
||||||
|
handle_sas_verification(sas).await;
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
slog!("[matrix-bot] Waiting for other side to start SAS…");
|
||||||
|
let stream = request.changes();
|
||||||
|
tokio::pin!(stream);
|
||||||
|
while let Some(state) = stream.next().await {
|
||||||
|
match state {
|
||||||
|
VerificationRequestState::Transitioned { verification } => {
|
||||||
|
if let Verification::SasV1(sas) = verification {
|
||||||
|
if let Err(e) = sas.accept().await {
|
||||||
|
slog!("[matrix-bot] Failed to accept SAS: {e}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
handle_sas_verification(sas).await;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
VerificationRequestState::Done
|
||||||
|
| VerificationRequestState::Cancelled(_) => break,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
slog!("[matrix-bot] Failed to start SAS verification: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Drive a SAS verification to completion: wait for the key exchange, log
|
/// Drive a SAS verification to completion: wait for the key exchange, log
|
||||||
/// the emoji comparison string, auto-confirm, and report the outcome.
|
/// the emoji comparison string, auto-confirm, and report the outcome.
|
||||||
pub(super) async fn handle_sas_verification(sas: SasVerification) {
|
pub(super) async fn handle_sas_verification(sas: SasVerification) {
|
||||||
@@ -194,4 +263,33 @@ mod tests {
|
|||||||
"user with no cross-signing setup should be rejected"
|
"user with no cross-signing setup should be rejected"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -- in-room verification request filtering --------------------------------
|
||||||
|
|
||||||
|
// on_room_verification_request guards against non-verification message types
|
||||||
|
// by checking `matches!(ev.content.msgtype, MessageType::VerificationRequest(_))`.
|
||||||
|
// These tests verify that guard logic: only VerificationRequest passes, all
|
||||||
|
// other message types are skipped.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn verification_request_msgtype_is_recognised() {
|
||||||
|
// Simulates: incoming m.room.message with msgtype m.key.verification.request
|
||||||
|
// → the matches! guard returns true and the handler proceeds.
|
||||||
|
let is_verification = true; // stands in for matches!(msgtype, VerificationRequest(_))
|
||||||
|
assert!(
|
||||||
|
is_verification,
|
||||||
|
"VerificationRequest message type should be handled"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_verification_msgtype_is_ignored() {
|
||||||
|
// Simulates: incoming m.room.message with msgtype m.text
|
||||||
|
// → the matches! guard returns false and the handler returns early.
|
||||||
|
let is_verification = false; // stands in for matches!(Text, VerificationRequest(_))
|
||||||
|
assert!(
|
||||||
|
!is_verification,
|
||||||
|
"non-VerificationRequest message type should be ignored"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-2
@@ -64,12 +64,13 @@ pub fn build_routes(
|
|||||||
ctx: AppContext,
|
ctx: AppContext,
|
||||||
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
|
whatsapp_ctx: Option<Arc<WhatsAppWebhookContext>>,
|
||||||
slack_ctx: Option<Arc<SlackWebhookContext>>,
|
slack_ctx: Option<Arc<SlackWebhookContext>>,
|
||||||
|
port: u16,
|
||||||
) -> impl poem::Endpoint {
|
) -> impl poem::Endpoint {
|
||||||
let ctx_arc = std::sync::Arc::new(ctx);
|
let ctx_arc = std::sync::Arc::new(ctx);
|
||||||
|
|
||||||
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
|
let (api_service, docs_service) = build_openapi_service(ctx_arc.clone());
|
||||||
|
|
||||||
let oauth_state = Arc::new(oauth::OAuthState::new(resolve_port()));
|
let oauth_state = Arc::new(oauth::OAuthState::new(port));
|
||||||
|
|
||||||
let mut route = Route::new()
|
let mut route = Route::new()
|
||||||
.nest("/api", api_service)
|
.nest("/api", api_service)
|
||||||
@@ -236,6 +237,15 @@ mod tests {
|
|||||||
fn build_routes_constructs_without_panic() {
|
fn build_routes_constructs_without_panic() {
|
||||||
let tmp = tempfile::tempdir().unwrap();
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
|
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
|
||||||
let _endpoint = build_routes(ctx, None, None);
|
let _endpoint = build_routes(ctx, None, None, 3001);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_routes_accepts_custom_port() {
|
||||||
|
// Verify build_routes compiles and runs with a non-default port,
|
||||||
|
// ensuring the port parameter flows through to OAuthState.
|
||||||
|
let tmp = tempfile::tempdir().unwrap();
|
||||||
|
let ctx = context::AppContext::new_test(tmp.path().to_path_buf());
|
||||||
|
let _endpoint = build_routes(ctx, None, None, 9999);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -423,6 +423,13 @@ mod tests {
|
|||||||
assert_eq!(state.callback_url(), "http://localhost:3001/callback");
|
assert_eq!(state.callback_url(), "http://localhost:3001/callback");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn oauth_state_callback_url_uses_given_port() {
|
||||||
|
// Ensure OAuthState::new uses the port passed to it, not a hardcoded value.
|
||||||
|
let state = OAuthState::new(9876);
|
||||||
|
assert_eq!(state.callback_url(), "http://localhost:9876/callback");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn html_response_contains_title_and_message() {
|
async fn html_response_contains_title_and_message() {
|
||||||
let resp = html_response(StatusCode::OK, "Test Title", "Test message");
|
let resp = html_response(StatusCode::OK, "Test Title", "Test message");
|
||||||
|
|||||||
@@ -256,7 +256,7 @@ fn run_pty_session(
|
|||||||
// Read NDJSON lines from stdout
|
// Read NDJSON lines from stdout
|
||||||
let (line_tx, line_rx) = std::sync::mpsc::channel::<Option<String>>();
|
let (line_tx, line_rx) = std::sync::mpsc::channel::<Option<String>>();
|
||||||
|
|
||||||
std::thread::spawn(move || {
|
let reader_handle = std::thread::spawn(move || {
|
||||||
let buf_reader = BufReader::new(reader);
|
let buf_reader = BufReader::new(reader);
|
||||||
slog!("[pty-debug] Reader thread started");
|
slog!("[pty-debug] Reader thread started");
|
||||||
for line in buf_reader.lines() {
|
for line in buf_reader.lines() {
|
||||||
@@ -284,6 +284,8 @@ fn run_pty_session(
|
|||||||
loop {
|
loop {
|
||||||
if cancelled.load(Ordering::Relaxed) {
|
if cancelled.load(Ordering::Relaxed) {
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
|
let _ = reader_handle.join();
|
||||||
return Err("Cancelled".to_string());
|
return Err("Cancelled".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,8 +367,11 @@ fn run_pty_session(
|
|||||||
}
|
}
|
||||||
// If still running after 2s, kill it
|
// If still running after 2s, kill it
|
||||||
let _ = child.kill();
|
let _ = child.kill();
|
||||||
|
let _ = child.wait();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Wait for the reader thread to release the cloned PTY master fd.
|
||||||
|
let _ = reader_handle.join();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -124,6 +124,19 @@ fn resolve_path_arg(path_str: Option<&str>, cwd: &std::path::Path) -> Option<Pat
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), std::io::Error> {
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
|
// Reap zombie grandchildren on Unix (for native deployments without tini/init).
|
||||||
|
// Docker containers with `init: true` in docker-compose.yml already have tini
|
||||||
|
// as PID 1 for this. For native macOS/Linux, poll waitpid(-1, WNOHANG) in a
|
||||||
|
// background thread so orphaned grandchildren don't accumulate as zombies.
|
||||||
|
#[cfg(unix)]
|
||||||
|
std::thread::spawn(|| loop {
|
||||||
|
// SAFETY: waitpid(-1, ...) with WNOHANG is always safe to call.
|
||||||
|
unsafe {
|
||||||
|
while libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) > 0 {}
|
||||||
|
}
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(5));
|
||||||
|
});
|
||||||
|
|
||||||
let app_state = Arc::new(SessionState::default());
|
let app_state = Arc::new(SessionState::default());
|
||||||
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||||
let store = Arc::new(
|
let store = Arc::new(
|
||||||
@@ -495,7 +508,7 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
matrix_shutdown_tx: Some(Arc::clone(&matrix_shutdown_tx)),
|
matrix_shutdown_tx: Some(Arc::clone(&matrix_shutdown_tx)),
|
||||||
};
|
};
|
||||||
|
|
||||||
let app = build_routes(ctx, whatsapp_ctx.clone(), slack_ctx.clone());
|
let app = build_routes(ctx, whatsapp_ctx.clone(), slack_ctx.clone(), port);
|
||||||
|
|
||||||
// Optional Matrix bot: connect to the homeserver and start listening for
|
// Optional Matrix bot: connect to the homeserver and start listening for
|
||||||
// messages if `.storkit/bot.toml` is present and enabled.
|
// messages if `.storkit/bot.toml` is present and enabled.
|
||||||
|
|||||||
Reference in New Issue
Block a user