From 3b041714b71fafdb942c54aa0c0150480e4fbe73 Mon Sep 17 00:00:00 2001 From: jack Date: Tue, 16 Jun 2026 18:50:13 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(web):=20task-centric=20redesign=20?= =?UTF-8?q?=E2=80=94=20enclosed=20shell,=20full-page=20settings,=20multi-p?= =?UTF-8?q?roject=20task=20tree?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UI redesign: - Borderless sidebar + top bar shell wrapping an inset surface chat panel (包裹感); removed the earlier outer window-frame attempt in favor of one continuous shell. - Settings converted from a centered modal to a full page that mirrors the chat shell layout (full-height rail + top bar + inset surface content panel). - Welcome/onboarding (SetupView) rebranded onto orange tokens; unified off-brand emerald/zinc/hardcoded colors to design tokens across TopBar, ProjectSwitcher, DiffViewer, ChatInput; added a saturated --color-success status token. Task architecture — Phase 0 (design: docs/web-task-architecture.md): - GET /api/workspace exposes git branch + dirty (fixes the hardcoded-null branch in TopBar). - GET /api/tasks lists sessions across ALL projects; PATCH /api/tasks/{id} updates pin/archive/unread/title (SessionMeta gained these additive fields). - Sidebar rebuilt as a Workspace > Project > Task tree: expandable projects, per-project counts, pinned-first sorting, unread dots, relative time, and a per-task context menu (pin/rename/archive/mark-unread/delete). - submit-pr builtin skill: the agent commits/pushes/opens a PR when triggered by a user query (no manual git UI). - ⌘K command palette (cross-project task search + quick actions) and browser notifications on run-finished / approval-needed. Deferred (documented): true parallel task execution (Phase 1-2, large server refactor) and composer $/# entry points (in jcode `/` already = skills, and cross-conversation linking needs the Phase-1 task model). Tests: internal/web/tasks_test.go (workspace + cross-project list + metadata), internal/skills/skills_test.go (submit-pr loads). go vet, go build ./..., vue-tsc and vite build all pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/web-task-architecture.md | 103 ++++ internal/session/session.go | 51 ++ internal/skills/builtin/submit-pr/SKILL.md | 38 ++ internal/skills/skills_test.go | 31 ++ internal/web/server.go | 104 ++++ internal/web/tasks_test.go | 155 ++++++ web/src/App.vue | 63 ++- web/src/components/ChatInput.vue | 20 +- web/src/components/CommandPalette.vue | 272 ++++++++++ web/src/components/DiffViewer.vue | 121 +++-- web/src/components/ProjectSwitcher.vue | 113 ++-- web/src/components/SettingsDialog.vue | 132 +++-- web/src/components/SetupView.vue | 471 ++++++++++------- web/src/components/Sidebar.vue | 582 ++++++++++++++------- web/src/components/TopBar.vue | 50 +- web/src/composables/api.ts | 9 +- web/src/composables/notifications.ts | 39 ++ web/src/stores/project.ts | 63 ++- web/src/styles/tokens.css | 7 + web/src/types/api.ts | 26 + 20 files changed, 1925 insertions(+), 525 deletions(-) create mode 100644 docs/web-task-architecture.md create mode 100644 internal/skills/builtin/submit-pr/SKILL.md create mode 100644 internal/skills/skills_test.go create mode 100644 internal/web/tasks_test.go create mode 100644 web/src/components/CommandPalette.vue create mode 100644 web/src/composables/notifications.ts diff --git a/docs/web-task-architecture.md b/docs/web-task-architecture.md new file mode 100644 index 0000000..22a9653 --- /dev/null +++ b/docs/web-task-architecture.md @@ -0,0 +1,103 @@ +# jcode Web 任务化架构设计(任务型 + 并行 + 多项目) + +> 状态:草案(已与维护者讨论定稿核心决策,待实现) +> 对标形态:ZCode 桌面端 —— 会话即"任务",多任务并行,跨项目同时查看/运行。 + +## 目标 + +把 jcode 的 **web 前端 + web 服务端** 从「单会话 / 单项目 / 一次一个 agent」演进为: + +- **任务型(task-centric)**:会话即任务,可置顶 / 归档 / 未读。 +- **并行**:多个任务可同时运行各自的 agent。 +- **多项目**:侧边栏一眼看到所有项目及其最近任务,并能跨项目并行运行。 + +## 已定决策 + +1. **改在 web 编排层(方案 B)**:一个任务 = 一份"引擎实例";并行**只发生在 web 包内**。内核(`session` / `handler` / `runner`)与 TUI / ACP / CLI **一律不动**——它们仍各用一份单跑引擎。 +2. **术语(三层,对齐 ZCode)**: + - **Workspace(工作区)** = 侧边栏容器,装所有 Project。 + - **Project(项目)** = 仓库/文件夹(tpm、jcode…)。← UI 这层明确叫 Project。 + - **Task(任务)** = 某个 Project 下的一次对话/工作线程;置顶/归档/未读/并行都作用在这层。`New chat` → `New task`。 + - 存储与代码保持 `session`(JSONL 转写不变)。只改文案,不改底层。 +3. **并发**:**不设上限**、不排队。jcode 是本地单人工具,不是多人服务,没必要限制。 +4. **项目模型**:每个任务**自带 pwd**(创建时绑定)。去掉"全局当前项目"这一权威态;UI 的"当前项目"只是**新建任务的默认值 + 列表筛选器**。 +5. **资源切分**: + - **每任务一份**:`agent` / `history` / `recorder` / `pwd` / `ctx+cancel` / 审批计数器 / 终端(pty) + - **共享一份**:MCP 连接(工具执行时带任务自己的 cwd)、技能 loader、模型配置 —— v1 先共享,后续按需 per-task + - **审批**:per-task;后台任务需要审批时走**托盘/通知**冒出来,不抢当前任务焦点 + +## 核心模型 + +``` +Server (编排层,只剩传输/HTTP/WS + 共享资源) + ├── tasks map[taskID]*Engine // 每个活跃任务一份引擎 + ├── projects map[path]*ProjectCtx // 按项目缓存 env / 项目技能 + ├── wsBroker (带按 task_id 的订阅过滤) + └── 共享: skillLoader, mcpManager, modelConfig, ptyMgr(按 task 分桶) + +Engine // = 今天 Server 的那些单例字段,按任务实例化一份 + ├── taskID, projectPath(pwd) + ├── agent, history + ├── recorder (该任务的 JSONL) + ├── ctx, cancel + ├── handler: WebHandler(taskID) // 发事件时盖上自己的 task_id + └── approvalCounter, todo/goal 快照 + +Task (持久层/元数据,对用户即"任务") + ├── id(=sessionUUID), projectPath, title + ├── pinned, archived, unread, status(idle/running/done/error) + └── createdAt, updatedAt + 一份 session JSONL 转写 +``` + +关键点:**`AgentEventHandler` 接口不变**——每个任务给一个自己的 `WebHandler` 实例,由它在 emit 时盖 `task_id`,所以 TUI/ACP 共用的接口零改动。 + +## 服务端改造 + +- 从 `internal/web/server.go` 把单例字段(`running / agent / history / recorder / pwd / runCancel`)抽进 `Engine`,`Server` 改持 `map[taskID]*Engine`。 +- 去掉 `handleChat` 的 `CompareAndSwap` 单跑门;`POST /api/chat` 带 `task_id`(缺省则建新任务)。 +- `handleStop` → `/api/stop`(带 `task_id`),查表取对应 `cancel`。 +- per-task 审批:审批计数器/待办表下沉到 `Engine`,审批 ID 带 `task_id`,回传也带。 +- 切项目不再全局拆重建:任务自带 pwd,文件/exec/diff/git 命令都用 `task.pwd`。 + +## WS 协议 + +- 每个事件加 `task_id` 字段。 +- 客户端 `subscribe { task_ids }` / `unsubscribe`;`WSBroker.Broadcast` 按订阅过滤(现在是全广播,多任务会风暴)。 +- 旧客户端忽略未知 `task_id` 字段,平滑兼容。 + +## 持久化与元数据 + +- `SessionMeta` 增加:`Pinned / Archived / Unread / Status / UpdatedAt`。 +- 任务创建即落盘元数据(现在是首条消息才建文件)。 +- 索引文件 `session.json` 加 `version` 字段,做向后兼容迁移。 +- `ListAllSessions()` **已存在但从未被调用**(死代码)→ 接 `GET /api/tasks`(跨项目只读列表),点亮侧边栏多项目树。 + +## Git(走技能,不做手动 git UI) + +- **暴露状态**:`envinfo.GitBranch` 其实已在内存里,只是没 API 返回 → 新增 `GET /api/workspace`(或扩 `/api/status`)返回 `branch / dirty / diff 统计`,修掉 `TopBar.vue` 写死的 `null`。 +- **提交/推送/开 PR**:一个 `submit-pr` 技能(+ 一个 commit/pr 工具)。你一句话触发 agent 跑它,**不做手动 git 按钮**。 + +## 分期落地 + +### Phase 0 — 零架构改动、并行无关的价值(先做) +- [ ] 暴露 git branch/status(`/api/workspace`)→ 修 TopBar 的 null 分支 +- [ ] 接 `ListAllSessions` → `GET /api/tasks` 跨项目只读 → 侧边栏**多项目任务树** +- [ ] `SessionMeta` 加 `pinned/archived/unread/status/updatedAt` + 端点 → 右键菜单 + 未读 +- [ ] `submit-pr` 技能(+ 工具) +- [ ] 命令面板 ⌘K、输入框 `$` 技能 / `#` 关联、浏览器通知(纯前端) + +### Phase 1 — 任务键化(后端,仍串行) +把单例抽成 `Engine`、按 `task_id` 路由、WS 带 `task_id`、前端 store 改 `Map`,并发先按 1 跑通——把"拆单例"和"真并行"解耦验证。 + +### Phase 2 — 真并行(无上限) +放开并发、per-task 审批路由、WS 订阅过滤、任务自带 pwd、注意内存/句柄回收。 + +### Phase 3 — 并行 UX +运行中任务托盘、per-task 未读/通知、并行审批界面。 + +## 最大风险 + +真并行会放大三件事,Phase 1 先串行键化就是为了在没有并发压力时把它们打磨好: +1. **审批串扰** —— 必须 per-task 路由。 +2. **WS 广播风暴** —— 必须订阅过滤。 +3. **资源占用** —— 不设硬上限,但要做内存/文件句柄/PTY 的回收。 diff --git a/internal/session/session.go b/internal/session/session.go index 339e120..50674e3 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -107,6 +107,13 @@ type SessionMeta struct { Model string `json:"model"` StartTime string `json:"start_time"` // RFC3339 Title string `json:"title,omitempty"` + // Task metadata. Additive — legacy index files simply lack these keys, which + // unmarshal to zero values (not pinned / not archived / read). + Pinned bool `json:"pinned,omitempty"` + Archived bool `json:"archived,omitempty"` + Unread bool `json:"unread,omitempty"` + Status string `json:"status,omitempty"` // idle/running/done/error (set by the web layer) + UpdatedAt string `json:"updated_at,omitempty"` // RFC3339 } // sessionIndex is the on-disk structure of session.json. @@ -712,6 +719,50 @@ func ListSessions(project string) ([]SessionMeta, error) { return idx.Sessions[project], nil } +// UpdateSessionMeta finds a session by uuid across all projects, applies mutate +// to its metadata, and persists the index atomically. Returns the updated meta, +// or (nil, nil) if no session with that uuid exists. uuid is only compared in +// memory (never used as a path), so no path validation is required here. +func UpdateSessionMeta(uuid string, mutate func(*SessionMeta)) (*SessionMeta, error) { + indexPath, err := config.SessionsIndexPath() + if err != nil { + return nil, err + } + data, err := os.ReadFile(indexPath) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + var idx sessionIndex + if err := json.Unmarshal(data, &idx); err != nil { + return nil, err + } + for project, metas := range idx.Sessions { + for i := range metas { + if metas[i].UUID == uuid { + mutate(&metas[i]) + idx.Sessions[project] = metas + newData, err := json.MarshalIndent(&idx, "", " ") + if err != nil { + return nil, err + } + tmpPath := indexPath + ".tmp" + if err := os.WriteFile(tmpPath, newData, 0644); err != nil { + return nil, err + } + if err := os.Rename(tmpPath, indexPath); err != nil { + return nil, err + } + updated := metas[i] + return &updated, nil + } + } + } + return nil, nil +} + // ListAllSessions returns all sessions across all projects, keyed by project path. func ListAllSessions() (map[string][]SessionMeta, error) { indexPath, err := config.SessionsIndexPath() diff --git a/internal/skills/builtin/submit-pr/SKILL.md b/internal/skills/builtin/submit-pr/SKILL.md new file mode 100644 index 0000000..d4188fa --- /dev/null +++ b/internal/skills/builtin/submit-pr/SKILL.md @@ -0,0 +1,38 @@ +--- +name: submit-pr +description: Commit the current changes, push the branch, and open a GitHub pull request +slash: /submit-pr +--- + +# Submit PR Skill + +You commit the working changes, push the branch, and open a GitHub pull request. +This is the way the user submits work — there is no manual git UI, so do it +carefully and report the PR URL at the end. + +## Steps + +1. **Check state.** Run `git status --short` and `git diff --stat`. If there is + nothing to submit (no staged, unstaged, or unpushed commits), tell the user + and stop. +2. **Pick a branch.** Run `git rev-parse --abbrev-ref HEAD`. If you are on the + default branch (`main`/`master`), create a feature branch first: + `git checkout -b ` named after the change. +3. **Review before committing.** Read `git status` and the diff so you do not + commit secrets, build artifacts, or unrelated files. Stage intentionally + (`git add `), not blindly with `git add -A` unless you have confirmed + the whole tree is intended. +4. **Commit** with a clear Conventional Commit message (`feat:`/`fix:`/`docs:`…) + summarizing the work. +5. **Push:** `git push -u origin `. +6. **Open the PR:** `gh pr create --title "" --body "<body>"`. The body + should summarize what changed and why, and list the testing/verification done. +7. **Report the PR URL** returned by `gh`. + +## Rules + +- Never force-push, and never push directly to the default branch. +- If the diff is large or ambiguous, summarize it and confirm intent before committing. +- If `gh` is not authenticated, instruct the user to run `gh auth login`. +- Keep the commit message and PR title concise and conventional. +- If a PR already exists for the branch, update it (push) rather than creating a duplicate. diff --git a/internal/skills/skills_test.go b/internal/skills/skills_test.go new file mode 100644 index 0000000..cfb4aa6 --- /dev/null +++ b/internal/skills/skills_test.go @@ -0,0 +1,31 @@ +package skills + +import "testing" + +// The submit-pr builtin skill must be discoverable so the agent can commit, +// push and open a PR when the user triggers it by query (the "Git via skill" +// flow — there is no manual git UI). +func TestSubmitPRSkillLoaded(t *testing.T) { + l := NewLoader() + + sk := l.Get("submit-pr") + if sk == nil { + t.Fatal("submit-pr builtin skill not loaded") + } + if !sk.Builtin { + t.Errorf("submit-pr should be a builtin skill") + } + if sk.Description == "" { + t.Errorf("submit-pr should have a description (used in the system prompt)") + } + + // Reachable via its slash trigger. + if bySlash := l.GetBySlash("/submit-pr"); bySlash == nil || bySlash.Name != "submit-pr" { + t.Fatalf("submit-pr not reachable via /submit-pr slash command") + } + + // Body content is non-empty (load_skill returns it to the agent). + if l.GetContent("submit-pr") == "" { + t.Errorf("submit-pr GetContent returned empty body") + } +} diff --git a/internal/web/server.go b/internal/web/server.go index d9cf2de..75d41d8 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -268,6 +268,9 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("GET /api/files", s.handleListFiles) mux.HandleFunc("GET /api/files/content", s.handleReadFile) mux.HandleFunc("GET /api/status", s.handleStatus) + mux.HandleFunc("GET /api/workspace", s.handleWorkspace) + mux.HandleFunc("GET /api/tasks", s.handleListAllTasks) + mux.HandleFunc("PATCH /api/tasks/{id}", s.handleUpdateTask) mux.HandleFunc("GET /api/models", s.handleListModels) mux.HandleFunc("POST /api/model", s.handleSwitchModel) mux.HandleFunc("POST /api/mode", s.handleSwitchMode) @@ -441,6 +444,26 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { }) } +// handleWorkspace returns lightweight git workspace info (branch + dirty) for +// the current project so the web UI can show the real branch name. Diff stats +// are fetched separately via /api/diff. Empty branch = not a git repo. +func (s *Server) handleWorkspace(w http.ResponseWriter, r *http.Request) { + branchCmd := exec.CommandContext(s.ctx, "git", "rev-parse", "--abbrev-ref", "HEAD") + branchCmd.Dir = s.pwd + branchOut, _ := branchCmd.Output() + branch := strings.TrimSpace(string(branchOut)) + + statusCmd := exec.CommandContext(s.ctx, "git", "status", "--porcelain") + statusCmd.Dir = s.pwd + statusOut, _ := statusCmd.Output() + dirty := strings.TrimSpace(string(statusOut)) != "" + + writeJSON(w, http.StatusOK, map[string]any{ + "branch": branch, + "dirty": dirty, + }) +} + func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) { if s.needsSetup { writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "setup required: please configure a provider first"}) @@ -645,6 +668,87 @@ func (s *Server) submitMessage(message, mode, source, sessionID string, images [ return recorder.UUID() } +// handleListAllTasks returns every session across all projects (flat list, +// each tagged with its project path) so the web sidebar can render a +// Workspace > Project > Task tree without switching the active project. +func (s *Server) handleListAllTasks(w http.ResponseWriter, r *http.Request) { + all, err := session.ListAllSessions() + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + type taskItem struct { + UUID string `json:"uuid"` + Project string `json:"project"` + CreatedAt string `json:"created_at"` + Provider string `json:"provider"` + Model string `json:"model"` + Title string `json:"title,omitempty"` + Pinned bool `json:"pinned"` + Archived bool `json:"archived"` + Unread bool `json:"unread"` + Status string `json:"status,omitempty"` + } + items := make([]taskItem, 0) + for project, metas := range all { + for _, m := range metas { + items = append(items, taskItem{ + UUID: m.UUID, + Project: project, + CreatedAt: m.StartTime, + Provider: m.Provider, + Model: m.Model, + Title: m.Title, + Pinned: m.Pinned, + Archived: m.Archived, + Unread: m.Unread, + Status: m.Status, + }) + } + } + writeJSON(w, http.StatusOK, items) +} + +// handleUpdateTask applies a partial metadata update (pin/archive/unread/title) +// to a task by uuid across all projects. +func (s *Server) handleUpdateTask(w http.ResponseWriter, r *http.Request) { + id := r.PathValue("id") + var req struct { + Pinned *bool `json:"pinned"` + Archived *bool `json:"archived"` + Unread *bool `json:"unread"` + Title *string `json:"title"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + meta, err := session.UpdateSessionMeta(id, func(m *session.SessionMeta) { + if req.Pinned != nil { + m.Pinned = *req.Pinned + } + if req.Archived != nil { + m.Archived = *req.Archived + } + if req.Unread != nil { + m.Unread = *req.Unread + } + if req.Title != nil { + m.Title = *req.Title + } + m.UpdatedAt = time.Now().Format(time.RFC3339) + }) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + if meta == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"}) + return + } + writeJSON(w, http.StatusOK, meta) +} + func (s *Server) handleListSessions(w http.ResponseWriter, r *http.Request) { metas, err := session.ListSessions(s.pwd) if err != nil { diff --git a/internal/web/tasks_test.go b/internal/web/tasks_test.go new file mode 100644 index 0000000..d816a65 --- /dev/null +++ b/internal/web/tasks_test.go @@ -0,0 +1,155 @@ +package web + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/cnjack/jcode/internal/session" +) + +// seedIndex points HOME at a temp dir and writes a sessions index with the +// given project→metas map, so the cross-project task handlers can be tested +// in-process without touching the real ~/.jcode. +func seedIndex(t *testing.T, sessions map[string][]session.SessionMeta) { + t.Helper() + home := t.TempDir() + t.Setenv("HOME", home) + dir := filepath.Join(home, ".jcode", "sessions") + if err := os.MkdirAll(dir, 0o755); err != nil { + t.Fatal(err) + } + data, err := json.Marshal(map[string]any{"sessions": sessions}) + if err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dir, "session.json"), data, 0o644); err != nil { + t.Fatal(err) + } +} + +// P0-1: GET /api/workspace on a non-git directory returns empty branch + not dirty. +func TestWorkspaceNonGit(t *testing.T) { + s := &Server{ctx: context.Background(), pwd: t.TempDir()} + rec := httptest.NewRecorder() + s.handleWorkspace(rec, httptest.NewRequest(http.MethodGet, "/api/workspace", nil)) + if rec.Code != http.StatusOK { + t.Fatalf("code=%d body=%q", rec.Code, rec.Body.String()) + } + var ws struct { + Branch string `json:"branch"` + Dirty bool `json:"dirty"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &ws); err != nil { + t.Fatalf("not JSON: %v", err) + } + if ws.Branch != "" || ws.Dirty { + t.Fatalf("non-git dir: want empty branch + not dirty, got %+v", ws) + } +} + +// P0-2: GET /api/tasks with no index returns an empty array (not null). +func TestListAllTasksEmpty(t *testing.T) { + seedIndex(t, map[string][]session.SessionMeta{}) + s := &Server{} + rec := httptest.NewRecorder() + s.handleListAllTasks(rec, httptest.NewRequest(http.MethodGet, "/api/tasks", nil)) + if rec.Code != http.StatusOK { + t.Fatalf("code=%d", rec.Code) + } + if got := strings.TrimSpace(rec.Body.String()); got != "[]" { + t.Fatalf("want [], got %q", got) + } +} + +// P0-2: GET /api/tasks returns sessions across ALL projects, each tagged with +// its project path. +func TestListAllTasksMultiProject(t *testing.T) { + seedIndex(t, map[string][]session.SessionMeta{ + "/work/tpm": {{UUID: "u-a", Project: "/work/tpm", Title: "task A", Model: "glm-5.2", StartTime: "2026-06-16T10:00:00Z"}}, + "/work/jcode": {{UUID: "u-b", Project: "/work/jcode", Title: "task B"}, {UUID: "u-c", Project: "/work/jcode", Pinned: true}}, + }) + s := &Server{} + rec := httptest.NewRecorder() + s.handleListAllTasks(rec, httptest.NewRequest(http.MethodGet, "/api/tasks", nil)) + if rec.Code != http.StatusOK { + t.Fatalf("code=%d", rec.Code) + } + var items []struct { + UUID string `json:"uuid"` + Project string `json:"project"` + Title string `json:"title"` + Pinned bool `json:"pinned"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &items); err != nil { + t.Fatalf("not JSON: %v", err) + } + if len(items) != 3 { + t.Fatalf("want 3 tasks across projects, got %d: %+v", len(items), items) + } + byID := map[string]struct { + project string + pinned bool + }{} + for _, it := range items { + byID[it.UUID] = struct { + project string + pinned bool + }{it.Project, it.Pinned} + } + if byID["u-a"].project != "/work/tpm" { + t.Fatalf("u-a project = %q", byID["u-a"].project) + } + if byID["u-c"].project != "/work/jcode" || !byID["u-c"].pinned { + t.Fatalf("u-c should be pinned in /work/jcode, got %+v", byID["u-c"]) + } +} + +// P0-3: PATCH /api/tasks/{id} updates pin/archive/unread/title and persists. +func TestUpdateTaskMeta(t *testing.T) { + seedIndex(t, map[string][]session.SessionMeta{ + "/work/tpm": {{UUID: "u-a", Project: "/work/tpm", Title: "orig"}}, + }) + s := &Server{} + + patch := func(body string) *httptest.ResponseRecorder { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPatch, "/api/tasks/u-a", strings.NewReader(body)) + req.SetPathValue("id", "u-a") + s.handleUpdateTask(rec, req) + return rec + } + + // pin + if rec := patch(`{"pinned":true}`); rec.Code != http.StatusOK { + t.Fatalf("pin: code=%d body=%q", rec.Code, rec.Body.String()) + } + // archive + unread + rename + if rec := patch(`{"archived":true,"unread":true,"title":"renamed"}`); rec.Code != http.StatusOK { + t.Fatalf("multi: code=%d", rec.Code) + } + + // Verify persisted via a fresh list. + all, err := session.ListAllSessions() + if err != nil { + t.Fatal(err) + } + m := all["/work/tpm"][0] + if !m.Pinned || !m.Archived || !m.Unread || m.Title != "renamed" { + t.Fatalf("metadata not persisted: %+v", m) + } + + // Unknown id -> 404. + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodPatch, "/api/tasks/missing", strings.NewReader(`{"pinned":true}`)) + req.SetPathValue("id", "missing") + s.handleUpdateTask(rec, req) + if rec.Code != http.StatusNotFound { + t.Fatalf("unknown id should be 404, got %d", rec.Code) + } +} diff --git a/web/src/App.vue b/web/src/App.vue index fabf900..5e69623 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -17,16 +17,26 @@ import TerminalPanel from '@/components/TerminalPanel.vue' import RightPanel from '@/components/RightPanel.vue' import SetupView from '@/components/SetupView.vue' import TopBar from '@/components/TopBar.vue' +import CommandPalette from '@/components/CommandPalette.vue' +import { useNotifications } from '@/composables/notifications' const store = useChatStore() const projectStore = useProjectStore() const { resolvedTheme, toggleTheme } = useTheme() +const { ensurePermission, notify } = useNotifications() const messagesEl = ref<HTMLDivElement | null>(null) const settingsOpen = ref(false) const projectsOpen = ref(false) +const paletteOpen = ref(false) const sidebarCollapsed = ref(false) const needsSetup = ref(false) +function onPaletteAction(name: 'settings' | 'projects' | 'theme') { + if (name === 'settings') settingsOpen.value = true + else if (name === 'projects') projectsOpen.value = true + else if (name === 'theme') toggleTheme() +} + const bottomPanel = ref<'none' | 'terminal'>('none') const bottomPanelHeight = ref(260) const isResizingPanel = ref(false) @@ -60,10 +70,16 @@ const { connected } = useWebSocket({ onToolCall: (data) => store.addToolCall(data.name, data.args, data.tool_call_id, data.display_info), onToolResult: (data) => store.resolveToolCall(data.name, data.output, data.tool_call_id, data.error, data.display_output), onTokenUpdate: (data) => { store.tokenInfo = data }, - onAgentDone: (data) => store.agentDone(data?.error), + onAgentDone: (data) => { + store.agentDone(data?.error) + notify(data?.error ? 'jcode — task failed' : 'jcode — task finished', data?.error || 'The agent finished its run.') + }, onTodoUpdate: () => store.fetchTodos(), onGoalUpdate: (data) => { store.goal = data }, - onApprovalRequest: (data) => store.addApprovalRequest(data), + onApprovalRequest: (data) => { + store.addApprovalRequest(data) + notify('jcode — approval needed', 'The agent is waiting for your approval.') + }, onAskUserRequest: (data) => store.attachAskUserRequest(data.id, data.questions), onSessionReset: () => store.clearChat(), onModelChanged: (data) => { @@ -102,6 +118,11 @@ watch( // Global keyboard shortcuts function handleGlobalKeydown(e: KeyboardEvent) { + if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) { + e.preventDefault() + paletteOpen.value = !paletteOpen.value + return + } if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'N') { e.preventDefault() store.newSession() @@ -146,6 +167,7 @@ function handleGlobalKeydown(e: KeyboardEvent) { onMounted(async () => { document.addEventListener('keydown', handleGlobalKeydown) + ensurePermission() const health = await store.fetchHealth() // Check if setup is needed — health returns needs_setup status if (health?.needs_setup) { @@ -267,6 +289,9 @@ function startResize(e: MouseEvent) { </script> <template> + <!-- One continuous surface: sidebar, top bar and chat area share a single + background with no hard dividers, so the regions read as one enclosed + space (包裹感) rather than separately-bordered panels. --> <div class="flex h-[100dvh] overflow-hidden transition-colors duration-300" style="background: var(--color-background); color: var(--color-foreground);"> <!-- Sidebar --> <transition @@ -287,7 +312,9 @@ function startResize(e: MouseEvent) { /> </transition> - <!-- Main content --> + <!-- Main content — shell tone (same as sidebar); the conversation lives in + an inset surface panel below, so it reads as one continuous shell that + wraps a distinct chat canvas (包裹感). --> <main class="flex-1 flex flex-col min-w-0 relative"> <!-- Top Bar --> <TopBar @@ -301,11 +328,12 @@ function startResize(e: MouseEvent) { @toggle-panel="togglePanel" /> - <!-- Chat area --> - <div class="flex-1 flex flex-col min-h-0"> + <!-- Chat area — inset surface panel: distinct tone, rounded, with margin + above (below the top bar) and below so it reads as a wrapped canvas. --> + <div class="chat-panel flex-1 flex flex-col min-h-0 relative"> <div ref="messagesEl" - class="flex-1 overflow-y-auto scroll-smooth" + class="flex-1 overflow-y-auto scroll-smooth rounded-t-[13px]" @scroll="checkScrollPosition" > <!-- Welcome --> @@ -430,8 +458,31 @@ function startResize(e: MouseEvent) { <SettingsDialog :open="settingsOpen" @close="settingsOpen = false" /> <ProjectSwitcher :open="projectsOpen" @close="projectsOpen = false" @project-switched="onProjectSwitched" /> + <CommandPalette :open="paletteOpen" @close="paletteOpen = false" @action="onPaletteAction" /> <!-- Setup overlay — shown when no providers are configured --> <SetupView v-if="needsSetup" @complete="onSetupComplete" /> </div> </template> + +<style scoped> +/* The conversation + composer live in one inset surface panel so the chat + canvas reads as distinct from the sidebar shell, wrapped with breathing room + above (below the top bar) and below (above the window edge) — 包裹感. */ +.chat-panel { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 14px; + margin: 4px 14px 14px; + /* NOT overflow:hidden — that would clip the composer's upward model/slash + menus on short viewports. The scroll area rounds its own top corners + (rounded-t) and the composer is inset, so the panel corners stay clean. */ + box-shadow: var(--shadow-sm); +} + +@media (max-width: 640px) { + .chat-panel { + margin: 2px 8px 8px; + } +} +</style> diff --git a/web/src/components/ChatInput.vue b/web/src/components/ChatInput.vue index 4c7480b..590eb41 100644 --- a/web/src/components/ChatInput.vue +++ b/web/src/components/ChatInput.vue @@ -407,7 +407,7 @@ watch(() => store.isRunning, (running) => { class="dropdown-item" @click="selectModel(r.provider, r.model)" > - <span class="text-amber-400 mr-1">★</span>{{ getModelDisplayName(r.provider, r.model) }} + <span class="mr-1" style="color: var(--color-primary)">★</span>{{ getModelDisplayName(r.provider, r.model) }} </button> </template> @@ -560,8 +560,9 @@ watch(() => store.isRunning, (running) => { <style scoped> .chat-input-wrapper { - padding: 12px 24px 16px; - background: var(--color-background); + padding: 8px 16px 14px; + /* Sits inside the surface chat panel — transparent so it blends with it. */ + background: transparent; position: relative; } @@ -569,18 +570,17 @@ watch(() => store.isRunning, (running) => { margin: 0 auto; border-radius: 12px; padding: 6px; - background: #F6F7F4; + background: transparent; position: relative; display: flex; flex-direction: column; } -.dark .chat-input-card { - background: var(--color-background); -} - .chat-input-inner { - background: var(--color-surface); + /* Recessed: always one step darker than the surface panel, derived from it so + the depth cue holds on every theme (some light themes have a background + token that is lighter than surface, which would otherwise invert it). */ + background: color-mix(in srgb, var(--color-surface) 90%, #000); border: 1px solid var(--color-border); border-radius: 9px; padding: 14px 16px 0; @@ -840,7 +840,7 @@ watch(() => store.isRunning, (running) => { .fav-star.is-fav { opacity: 1; - color: #fbbf24; + color: var(--color-primary); } .dropdown-item:hover .fav-star { diff --git a/web/src/components/CommandPalette.vue b/web/src/components/CommandPalette.vue new file mode 100644 index 0000000..d18d729 --- /dev/null +++ b/web/src/components/CommandPalette.vue @@ -0,0 +1,272 @@ +<script setup lang="ts"> +import { ref, computed, watch, nextTick } from 'vue' +import { Dialog, DialogPanel, TransitionRoot, TransitionChild } from '@headlessui/vue' +import { Plus, Settings, FolderOpen, SunMoon, MessageSquare, CornerDownLeft } from 'lucide-vue-next' +import { useChatStore } from '@/stores/chat' +import { useProjectStore } from '@/stores/project' +import type { TaskItem } from '@/types/api' + +const props = defineProps<{ open: boolean }>() +const emit = defineEmits<{ + close: [] + action: [name: 'settings' | 'projects' | 'theme'] +}>() + +const store = useChatStore() +const projectStore = useProjectStore() + +const query = ref('') +const selectedIdx = ref(0) +const inputEl = ref<HTMLInputElement | null>(null) + +interface PaletteItem { + id: string + group: string + label: string + hint?: string + icon: unknown + run: () => void | Promise<void> +} + +async function openTask(task: TaskItem) { + emit('close') + if (task.unread) projectStore.updateTaskMeta(task.uuid, { unread: false }) + const cur = projectStore.activeProject?.path || store.pwd + if (cur !== task.project) { + const ok = await projectStore.openProject(task.project) + if (!ok) return + await store.fetchHealth() + } + await store.loadSession(task.uuid) +} + +const actions = computed<PaletteItem[]>(() => [ + { id: 'a-new', group: 'Actions', label: 'New task', icon: Plus, run: () => { emit('close'); store.newSession() } }, + { id: 'a-proj', group: 'Actions', label: 'Open project…', icon: FolderOpen, run: () => { emit('close'); emit('action', 'projects') } }, + { id: 'a-settings', group: 'Actions', label: 'Open settings', icon: Settings, run: () => { emit('close'); emit('action', 'settings') } }, + { id: 'a-theme', group: 'Actions', label: 'Toggle theme', icon: SunMoon, run: () => { emit('action', 'theme') } }, +]) + +const taskItems = computed<PaletteItem[]>(() => + projectStore.allTasks + .filter((t) => !t.archived) + .map((t) => ({ + id: 't-' + t.uuid, + group: 'Tasks', + label: t.title || t.uuid.slice(0, 8) + '…', + hint: projectStore.nameForPath(t.project), + icon: MessageSquare, + run: () => openTask(t), + })), +) + +const results = computed<PaletteItem[]>(() => { + const q = query.value.trim().toLowerCase() + const all = [...actions.value, ...taskItems.value] + if (!q) return [...actions.value, ...taskItems.value.slice(0, 8)] + return all.filter((i) => i.label.toLowerCase().includes(q) || (i.hint || '').toLowerCase().includes(q)) +}) + +// Group results in display order while keeping a flat index for keyboard nav. +const groups = computed(() => { + const order: string[] = [] + const map: Record<string, PaletteItem[]> = {} + results.value.forEach((item) => { + if (!map[item.group]) { + map[item.group] = [] + order.push(item.group) + } + map[item.group]!.push(item) + }) + return order.map((g) => ({ name: g, items: map[g]! })) +}) + +function flatIndex(item: PaletteItem): number { + return results.value.findIndex((i) => i.id === item.id) +} + +watch(() => props.open, async (o) => { + if (o) { + query.value = '' + selectedIdx.value = 0 + projectStore.fetchAllTasks() + await nextTick() + inputEl.value?.focus() + } +}) +watch(results, () => { selectedIdx.value = 0 }) + +function move(delta: number) { + const n = results.value.length + if (n === 0) return + selectedIdx.value = (selectedIdx.value + delta + n) % n +} +function runSelected() { + results.value[selectedIdx.value]?.run() +} +function onKeydown(e: KeyboardEvent) { + if (e.key === 'ArrowDown') { e.preventDefault(); move(1) } + else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1) } + else if (e.key === 'Enter') { e.preventDefault(); runSelected() } +} +</script> + +<template> + <TransitionRoot :show="open" as="template"> + <Dialog @close="emit('close')" class="relative" style="z-index: var(--z-modal)"> + <TransitionChild + enter="ease-out duration-150" enter-from="opacity-0" enter-to="opacity-100" + leave="ease-in duration-100" leave-from="opacity-100" leave-to="opacity-0"> + <div class="fixed inset-0" style="background: rgba(8,8,8,0.45); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px)" /> + </TransitionChild> + + <div class="fixed inset-0 flex items-start justify-center pt-[12vh] px-4"> + <TransitionChild + enter="ease-out duration-150" enter-from="opacity-0 scale-[0.98] -translate-y-1" + enter-to="opacity-100 scale-100 translate-y-0" + leave="ease-in duration-100" leave-from="opacity-100 scale-100" + leave-to="opacity-0 scale-[0.98] -translate-y-1"> + <DialogPanel class="cp-panel"> + <div class="cp-input-row"> + <svg class="cp-search-icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.7"> + <circle cx="9" cy="9" r="6" /><path d="M14 14l3.5 3.5" stroke-linecap="round" /> + </svg> + <input + ref="inputEl" + v-model="query" + class="cp-input" + placeholder="Search tasks or run a command…" + @keydown="onKeydown" + /> + <kbd class="cp-esc">Esc</kbd> + </div> + + <div class="cp-results"> + <div v-if="results.length === 0" class="cp-empty">No results</div> + <template v-for="g in groups" :key="g.name"> + <div class="cp-group-label">{{ g.name }}</div> + <button + v-for="item in g.items" + :key="item.id" + class="cp-item" + :class="{ sel: flatIndex(item) === selectedIdx }" + @click="item.run()" + @mousemove="selectedIdx = flatIndex(item)" + > + <component :is="item.icon" :size="15" class="cp-item-icon" /> + <span class="cp-item-label">{{ item.label }}</span> + <span v-if="item.hint" class="cp-item-hint">{{ item.hint }}</span> + <CornerDownLeft v-if="flatIndex(item) === selectedIdx" :size="13" class="cp-enter" /> + </button> + </template> + </div> + </DialogPanel> + </TransitionChild> + </div> + </Dialog> + </TransitionRoot> +</template> + +<style scoped> +.cp-panel { + width: min(560px, 94vw); + max-height: 60vh; + display: flex; + flex-direction: column; + overflow: hidden; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-xl); + box-shadow: var(--shadow-lg); +} +.cp-input-row { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 14px; + border-bottom: 1px solid var(--color-border); +} +.cp-search-icon { + width: 16px; + height: 16px; + color: var(--color-muted-foreground); + flex-shrink: 0; +} +.cp-input { + flex: 1; + background: transparent; + border: none; + outline: none; + font-size: 14px; + color: var(--color-foreground); + font-family: var(--font-sans); +} +.cp-input::placeholder { + color: var(--color-muted-foreground); +} +.cp-esc { + font-size: 10px; + font-family: var(--font-mono); + padding: 2px 6px; + border-radius: var(--radius-sm); + background: var(--color-secondary); + border: 1px solid var(--color-border); + color: var(--color-muted-foreground); +} +.cp-results { + overflow-y: auto; + padding: 6px; +} +.cp-empty { + text-align: center; + font-size: 13px; + color: var(--color-muted-foreground); + padding: 24px 0; +} +.cp-group-label { + font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-muted-foreground); + padding: 8px 8px 4px; +} +.cp-item { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + border: none; + background: transparent; + border-radius: var(--radius-md); + cursor: pointer; + text-align: left; +} +.cp-item.sel { + background: var(--color-muted); +} +.cp-item-icon { + color: var(--color-muted-foreground); + flex-shrink: 0; +} +.cp-item-label { + flex: 1; + min-width: 0; + font-size: 13px; + color: var(--color-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.cp-item-hint { + font-size: 11px; + font-family: var(--font-mono); + color: var(--color-muted-foreground); + flex-shrink: 0; +} +.cp-enter { + color: var(--color-muted-foreground); + flex-shrink: 0; +} +</style> diff --git a/web/src/components/DiffViewer.vue b/web/src/components/DiffViewer.vue index 563706c..c8055dc 100644 --- a/web/src/components/DiffViewer.vue +++ b/web/src/components/DiffViewer.vue @@ -40,11 +40,11 @@ const totalChanges = computed(() => ({ deletions: entries.value.reduce((sum, e) => sum + e.deletions, 0), })) -function statusBadge(status: string) { +function statusBadge(status: string): { label: string; style: Record<string, string> } { switch (status) { - case 'A': return { label: 'A', cls: 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400' } - case 'D': return { label: 'D', cls: 'bg-red-100 dark:bg-red-500/15 text-red-700 dark:text-red-400' } - default: return { label: 'M', cls: 'bg-amber-100 dark:bg-amber-500/15 text-amber-700 dark:text-amber-400' } + case 'A': return { label: 'A', style: { background: 'var(--color-success-bg)', color: 'var(--color-success-fg)' } } + case 'D': return { label: 'D', style: { background: 'var(--color-error-bg)', color: 'var(--color-error-fg)' } } + default: return { label: 'M', style: { background: 'var(--color-warning-bg)', color: 'var(--color-warning-fg)' } } } } @@ -68,19 +68,17 @@ onMounted(fetchDiff) </script> <template> - <div class="flex flex-col h-full bg-zinc-50 dark:bg-zinc-900"> + <div class="flex flex-col h-full" style="background: var(--color-background)"> <!-- Header --> - <div class="flex items-center justify-between px-3 py-1.5 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-100/80 dark:bg-zinc-800/80"> + <div class="flex items-center justify-between px-3 py-1.5" style="border-bottom: 1px solid var(--color-border); background: var(--color-sidebar-bg)"> <div class="flex items-center gap-2"> - <span class="text-[11px] font-semibold text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Changes</span> + <span class="text-[11px] font-semibold uppercase tracking-wider" style="color: var(--color-muted-foreground)">Changes</span> <div class="flex gap-0.5"> <button v-for="m in modes" :key="m.value" - class="px-1.5 py-0.5 text-[10px] rounded cursor-pointer transition-colors font-medium" - :class="mode === m.value - ? 'bg-emerald-100 dark:bg-emerald-500/15 text-emerald-700 dark:text-emerald-400' - : 'text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 hover:bg-zinc-200 dark:hover:bg-zinc-700'" + class="dv-mode px-1.5 py-0.5 text-[10px] rounded cursor-pointer transition-colors font-medium" + :class="{ active: mode === m.value }" @click="mode = m.value; fetchDiff()" > {{ m.label }} @@ -89,12 +87,12 @@ onMounted(fetchDiff) </div> <div class="flex items-center gap-2"> <span v-if="totalChanges.additions || totalChanges.deletions" class="text-[10px] font-mono"> - <span class="text-emerald-600 dark:text-emerald-400">+{{ totalChanges.additions }}</span> - <span class="text-zinc-300 dark:text-zinc-600 mx-0.5">/</span> - <span class="text-red-500 dark:text-red-400">-{{ totalChanges.deletions }}</span> + <span style="color: var(--color-success-fg)">+{{ totalChanges.additions }}</span> + <span class="mx-0.5" style="color: var(--color-border)">/</span> + <span style="color: var(--color-error-fg)">-{{ totalChanges.deletions }}</span> </span> <button - class="text-[10px] text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 cursor-pointer transition-colors font-medium" + class="dv-mute text-[10px] cursor-pointer transition-colors font-medium" @click="fetchDiff" > ↻ Refresh @@ -104,61 +102,54 @@ onMounted(fetchDiff) <div class="flex flex-col flex-1 min-h-0"> <!-- File list --> - <div class="border-b border-zinc-200 dark:border-zinc-800 overflow-y-auto shrink-0 max-h-[30%]"> - <div v-if="entries.length === 0 && !loading" class="text-center text-[11px] text-zinc-400 dark:text-zinc-500 py-6"> + <div class="overflow-y-auto shrink-0 max-h-[30%]" style="border-bottom: 1px solid var(--color-border)"> + <div v-if="entries.length === 0 && !loading" class="text-center text-[11px] py-6" style="color: var(--color-muted-foreground)"> No changes </div> - <div v-if="loading" class="text-center text-[11px] text-zinc-400 dark:text-zinc-500 py-6 animate-pulse"> + <div v-if="loading" class="text-center text-[11px] py-6 animate-pulse" style="color: var(--color-muted-foreground)"> Loading... </div> <button v-for="entry in entries" :key="entry.file" - class="w-full flex items-center gap-1.5 px-2 py-1.5 text-left cursor-pointer transition-colors" - :class="selectedFile === entry.file - ? 'bg-emerald-50 dark:bg-emerald-500/10 text-zinc-700 dark:text-zinc-200' - : 'text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-800'" + class="dv-file w-full flex items-center gap-1.5 px-2 py-1.5 text-left cursor-pointer transition-colors" + :class="{ active: selectedFile === entry.file }" @click="selectedFile = entry.file" > <span class="text-[9px] font-bold rounded px-1 py-px shrink-0" - :class="statusBadge(entry.status).cls" + :style="statusBadge(entry.status).style" > {{ statusBadge(entry.status).label }} </span> <span class="text-[11px] font-mono truncate">{{ entry.file.split('/').pop() }}</span> <span class="text-[9px] font-mono ml-auto shrink-0"> - <span class="text-emerald-600 dark:text-emerald-400">+{{ entry.additions }}</span> - <span class="text-red-500 dark:text-red-400 ml-0.5">-{{ entry.deletions }}</span> + <span style="color: var(--color-success-fg)">+{{ entry.additions }}</span> + <span class="ml-0.5" style="color: var(--color-error-fg)">-{{ entry.deletions }}</span> </span> </button> </div> <!-- Diff content --> <div class="flex-1 overflow-auto"> - <div v-if="!selectedEntry" class="text-center text-[11px] text-zinc-400 dark:text-zinc-500 py-8"> + <div v-if="!selectedEntry" class="text-center text-[11px] py-8" style="color: var(--color-muted-foreground)"> Select a file to view changes </div> <div v-else> - <div class="px-3 py-1.5 border-b border-zinc-200 dark:border-zinc-800 bg-zinc-100/50 dark:bg-zinc-800/50"> - <span class="text-[11px] font-mono text-zinc-600 dark:text-zinc-300">{{ selectedEntry.file }}</span> + <div class="px-3 py-1.5" style="border-bottom: 1px solid var(--color-border); background: var(--color-muted)"> + <span class="text-[11px] font-mono" style="color: var(--color-foreground)">{{ selectedEntry.file }}</span> <span class="text-[10px] font-mono ml-2"> - <span class="text-emerald-600 dark:text-emerald-400">+{{ selectedEntry.additions }}</span> - <span class="text-zinc-300 dark:text-zinc-600 mx-0.5">/</span> - <span class="text-red-500 dark:text-red-400">-{{ selectedEntry.deletions }}</span> + <span style="color: var(--color-success-fg)">+{{ selectedEntry.additions }}</span> + <span class="mx-0.5" style="color: var(--color-border)">/</span> + <span style="color: var(--color-error-fg)">-{{ selectedEntry.deletions }}</span> </span> </div> <div class="font-mono text-[11px] leading-5"> <div v-for="(line, i) in parsePatchLines(selectedEntry.patch)" :key="i" - class="px-3 border-l-2" - :class="{ - 'bg-emerald-50 dark:bg-emerald-500/10 border-emerald-400 dark:border-emerald-500/40 text-emerald-800 dark:text-emerald-300': line.type === 'add', - 'bg-red-50 dark:bg-red-500/10 border-red-400 dark:border-red-500/40 text-red-800 dark:text-red-300': line.type === 'del', - 'bg-blue-50 dark:bg-blue-500/10 border-blue-300 dark:border-blue-500/40 text-blue-600 dark:text-blue-300': line.type === 'hunk', - 'border-transparent text-zinc-500 dark:text-zinc-400': line.type === 'ctx', - }" + class="dv-line px-3 border-l-2" + :class="`dv-${line.type}`" > <pre class="whitespace-pre-wrap">{{ line.text }}</pre> </div> @@ -168,3 +159,57 @@ onMounted(fetchDiff) </div> </div> </template> + +<style scoped> +.dv-mode { + color: var(--color-muted-foreground); +} +.dv-mode:hover { + color: var(--color-foreground); + background: var(--color-muted); +} +.dv-mode.active { + background: rgba(255, 132, 0, 0.12); + color: var(--color-primary); +} + +.dv-mute { + color: var(--color-muted-foreground); +} +.dv-mute:hover { + color: var(--color-foreground); +} + +.dv-file { + color: var(--color-muted-foreground); +} +.dv-file:hover { + background: var(--color-muted); +} +.dv-file.active { + background: rgba(255, 132, 0, 0.08); + color: var(--color-foreground); +} + +.dv-line { + border-color: transparent; +} +.dv-add { + background: var(--color-success-bg); + border-color: var(--color-success-fg); + color: var(--color-success-fg); +} +.dv-del { + background: var(--color-error-bg); + border-color: var(--color-error-fg); + color: var(--color-error-fg); +} +.dv-hunk { + background: var(--color-info-bg); + border-color: var(--color-info-fg); + color: var(--color-info-fg); +} +.dv-ctx { + color: var(--color-muted-foreground); +} +</style> diff --git a/web/src/components/ProjectSwitcher.vue b/web/src/components/ProjectSwitcher.vue index 33c1f81..bddcf5b 100644 --- a/web/src/components/ProjectSwitcher.vue +++ b/web/src/components/ProjectSwitcher.vue @@ -119,7 +119,7 @@ function deleteProject(id: string) { leave-from="opacity-100" leave-to="opacity-0" > - <div class="fixed inset-0 bg-black/40 dark:bg-black/60 backdrop-blur-sm" /> + <div class="fixed inset-0" style="background: rgba(8,8,8,0.5); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px)" /> </TransitionChild> <div class="fixed inset-0 flex items-start justify-center pt-16 px-4"> @@ -131,10 +131,10 @@ function deleteProject(id: string) { leave-from="opacity-100 translate-y-0" leave-to="opacity-0 translate-y-2" > - <DialogPanel class="w-full max-w-lg bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded shadow-2xl overflow-hidden"> + <DialogPanel class="w-full max-w-lg overflow-hidden" style="background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-xl); box-shadow: var(--shadow-lg)"> <!-- Header --> <div class="px-5 pt-4 pb-2"> - <DialogTitle class="text-sm font-semibold text-zinc-800 dark:text-zinc-100" style="font-family: var(--font-sans)"> + <DialogTitle class="text-sm font-semibold" style="font-family: var(--font-sans); color: var(--color-foreground)"> {{ showBrowser ? 'Open Folder' : 'Projects' }} </DialogTitle> </div> @@ -145,58 +145,62 @@ function deleteProject(id: string) { <input v-model="pathInput" type="text" - class="flex-1 px-3 py-1.5 text-sm font-mono rounded-md border border-zinc-200 dark:border-zinc-700 bg-zinc-50 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-200 outline-none focus:border-emerald-400 dark:focus:border-emerald-500/60 transition-colors" + class="ps-input flex-1 px-3 py-1.5 text-sm font-mono rounded-md outline-none" placeholder="/path/to/folder" @keydown="handlePathKeyDown" /> <button - class="px-3 py-1.5 text-xs font-medium bg-emerald-500 hover:bg-emerald-600 text-white rounded-md cursor-pointer transition-colors" + class="px-3 py-1.5 text-xs font-medium rounded-md cursor-pointer transition-opacity hover:opacity-90" + style="background: var(--color-primary); color: var(--color-on-primary, #fff)" @click="handlePathSubmit" > OK </button> <button - class="px-2 py-1.5 text-xs text-zinc-400 dark:text-zinc-500 hover:text-zinc-600 dark:hover:text-zinc-300 cursor-pointer transition-colors" + class="ps-muted-btn px-2 py-1.5 text-xs cursor-pointer transition-colors" @click="showBrowser = false" > Back </button> </div> - <div class="border border-zinc-200 dark:border-zinc-700 rounded-md overflow-hidden max-h-80 overflow-y-auto bg-zinc-50 dark:bg-zinc-800/60"> + <div class="rounded-md overflow-hidden max-h-80 overflow-y-auto" style="border: 1px solid var(--color-border); background: var(--color-background)"> <button v-if="browsePath && browsePath !== '/'" - class="w-full flex items-center gap-2 px-3 py-2 text-sm text-zinc-500 dark:text-zinc-400 hover:bg-zinc-100 dark:hover:bg-zinc-700 cursor-pointer transition-colors border-b border-zinc-100 dark:border-zinc-700/60" + class="ps-folder w-full flex items-center gap-2 px-3 py-2 text-sm cursor-pointer transition-colors" + style="color: var(--color-muted-foreground); border-bottom: 1px solid var(--color-border)" @click="goUp" > - <span class="text-zinc-400 dark:text-zinc-500">..</span> + <span style="color: var(--color-muted-foreground)">..</span> </button> - <div v-if="browseLoading" class="px-3 py-6 text-center text-xs text-zinc-400 dark:text-zinc-500 animate-pulse"> + <div v-if="browseLoading" class="px-3 py-6 text-center text-xs animate-pulse" style="color: var(--color-muted-foreground)"> Loading... </div> - <div v-else-if="browseFolders.length === 0" class="px-3 py-6 text-center text-xs text-zinc-400 dark:text-zinc-500"> + <div v-else-if="browseFolders.length === 0" class="px-3 py-6 text-center text-xs" style="color: var(--color-muted-foreground)"> No folders found </div> <button v-for="folder in browseFolders" :key="folder.path" - class="w-full flex items-center gap-2 px-3 py-2 text-sm text-left text-zinc-700 dark:text-zinc-300 hover:bg-emerald-50 dark:hover:bg-emerald-500/10 hover:text-emerald-700 dark:hover:text-emerald-400 cursor-pointer transition-colors border-b border-zinc-100 dark:border-zinc-700/60 last:border-0" + class="ps-folder w-full flex items-center gap-2 px-3 py-2 text-sm text-left cursor-pointer transition-colors" + style="color: var(--color-foreground); border-bottom: 1px solid var(--color-border)" @click="navigateTo(folder)" > - <span class="text-zinc-400 dark:text-zinc-500 shrink-0">📁</span> + <span class="shrink-0" style="color: var(--color-muted-foreground)">📁</span> <span class="truncate">{{ folder.name }}</span> </button> </div> <div class="mt-3 flex items-center justify-between"> - <div class="text-[11px] text-zinc-400 dark:text-zinc-500 font-mono truncate flex-1 mr-2"> + <div class="text-[11px] font-mono truncate flex-1 mr-2" style="color: var(--color-muted-foreground)"> {{ displayPath }} </div> <button - class="px-4 py-1.5 text-xs font-medium bg-emerald-500 hover:bg-emerald-600 text-white rounded-md cursor-pointer transition-colors shrink-0" + class="px-4 py-1.5 text-xs font-medium rounded-md cursor-pointer transition-opacity hover:opacity-90 shrink-0" + style="background: var(--color-primary); color: var(--color-on-primary, #fff)" @click="selectCurrentPath" > Open Folder @@ -207,42 +211,40 @@ function deleteProject(id: string) { <!-- Project list mode --> <div v-else> <div v-if="projectStore.switchError" class="px-5 py-2"> - <div class="text-xs text-red-500 dark:text-red-400 bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/20 rounded-md px-3 py-2"> + <div class="text-xs rounded-md px-3 py-2" style="color: var(--color-error-fg); background: var(--color-error-bg); border: 1px solid var(--color-error-fg)"> {{ projectStore.switchError }} </div> </div> - <div v-if="projectStore.switching" class="px-3 py-6 text-center text-xs text-zinc-400 dark:text-zinc-500 animate-pulse"> + <div v-if="projectStore.switching" class="px-3 py-6 text-center text-xs animate-pulse" style="color: var(--color-muted-foreground)"> Switching project... </div> <div v-else class="px-3 pb-2 max-h-72 overflow-y-auto"> - <div v-if="projectStore.projects.length === 0" class="text-xs text-zinc-400 dark:text-zinc-500 py-6 text-center"> + <div v-if="projectStore.projects.length === 0" class="text-xs py-6 text-center" style="color: var(--color-muted-foreground)"> No projects yet </div> <div v-for="p in projectStore.projects" :key="p.id" - class="group flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer transition-colors" - :class="projectStore.activeId === p.id - ? 'bg-emerald-50 dark:bg-emerald-500/10 border border-emerald-200 dark:border-emerald-500/20' - : 'hover:bg-zinc-50 dark:hover:bg-zinc-800 border border-transparent'" + class="ps-project group flex items-center gap-2 px-3 py-2.5 rounded-md cursor-pointer transition-colors" + :class="{ active: projectStore.activeId === p.id }" @click="selectProject(p.id)" > <div class="w-7 h-7 rounded-md flex items-center justify-center text-xs font-bold shrink-0" - :class="projectStore.activeId === p.id - ? 'bg-emerald-100 dark:bg-emerald-500/20 text-emerald-700 dark:text-emerald-400' - : 'bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400'" + :style="projectStore.activeId === p.id + ? { background: 'rgba(255,132,0,0.14)', color: 'var(--color-primary)' } + : { background: 'var(--color-muted)', color: 'var(--color-muted-foreground)' }" > {{ projectStore.projectName(p).charAt(0).toUpperCase() }} </div> <div class="min-w-0 flex-1"> - <div class="text-sm text-zinc-700 dark:text-zinc-200 truncate">{{ projectStore.projectName(p) }}</div> - <div class="text-[10px] text-zinc-400 dark:text-zinc-500 font-mono truncate">{{ p.path }}</div> + <div class="text-sm truncate" style="color: var(--color-foreground)">{{ projectStore.projectName(p) }}</div> + <div class="text-[10px] font-mono truncate" style="color: var(--color-muted-foreground)">{{ p.path }}</div> </div> <button - class="opacity-0 group-hover:opacity-100 p-1 text-zinc-400 dark:text-zinc-500 hover:text-red-500 dark:hover:text-red-400 cursor-pointer transition-all" + class="ps-delete opacity-0 group-hover:opacity-100 p-1 cursor-pointer transition-all" @click.stop="deleteProject(p.id)" title="Remove project" > @@ -253,15 +255,16 @@ function deleteProject(id: string) { </div> </div> - <div class="px-5 py-3 border-t border-zinc-100 dark:border-zinc-800 flex justify-between items-center"> + <div class="px-5 py-3 flex justify-between items-center" style="border-top: 1px solid var(--color-border)"> <button - class="text-xs text-emerald-600 dark:text-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-300 cursor-pointer transition-colors font-medium" + class="text-xs cursor-pointer transition-opacity hover:opacity-80 font-medium" + style="color: var(--color-primary)" @click="openBrowser" > + Open Folder </button> <button - class="px-3 py-1 text-xs text-zinc-500 dark:text-zinc-400 hover:text-zinc-700 dark:hover:text-zinc-200 cursor-pointer transition-colors" + class="ps-muted-btn px-3 py-1 text-xs cursor-pointer transition-colors" @click="emit('close')" > Close @@ -274,3 +277,51 @@ function deleteProject(id: string) { </Dialog> </TransitionRoot> </template> + +<style scoped> +.ps-input { + background: var(--color-background); + border: 1px solid var(--color-border); + color: var(--color-foreground); + transition: border-color 0.15s; +} +.ps-input::placeholder { + color: var(--color-muted-foreground); +} +.ps-input:focus { + border-color: var(--color-primary); +} + +.ps-muted-btn { + color: var(--color-muted-foreground); +} +.ps-muted-btn:hover { + color: var(--color-foreground); +} + +.ps-folder:last-child { + border-bottom: none !important; +} +.ps-folder:hover { + background: rgba(255, 132, 0, 0.08); + color: var(--color-primary) !important; +} + +.ps-project { + border: 1px solid transparent; +} +.ps-project:hover { + background: var(--color-muted); +} +.ps-project.active { + background: rgba(255, 132, 0, 0.08); + border-color: rgba(255, 132, 0, 0.3); +} + +.ps-delete { + color: var(--color-muted-foreground); +} +.ps-delete:hover { + color: var(--color-destructive); +} +</style> diff --git a/web/src/components/SettingsDialog.vue b/web/src/components/SettingsDialog.vue index f40dca6..520e021 100644 --- a/web/src/components/SettingsDialog.vue +++ b/web/src/components/SettingsDialog.vue @@ -500,7 +500,8 @@ const addProviderInfo = () => addProviderList.value.find(p => p.id === addSelect <template> <TransitionRoot :show="open" as="template"> - <Dialog @close="emit('close')" class="relative z-50"> + <Dialog @close="emit('close')" class="relative" style="z-index: var(--z-modal)"> + <!-- Opaque page background: settings is a full page, not a floating modal. --> <TransitionChild enter="ease-out duration-150" enter-from="opacity-0" @@ -508,42 +509,38 @@ const addProviderInfo = () => addProviderList.value.find(p => p.id === addSelect leave="ease-in duration-100" leave-from="opacity-100" leave-to="opacity-0"> - <div class="fixed inset-0" style="background: rgba(8,8,8,0.5); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px)" /> + <div class="fixed inset-0" style="background: var(--color-background)" /> </TransitionChild> - <div class="fixed inset-0 flex items-center justify-center p-4 sm:p-6"> + <!-- Edge-to-edge full page. --> + <div class="fixed inset-0 flex"> <TransitionChild - enter="ease-out duration-150" - enter-from="opacity-0 scale-[0.98] translate-y-1" - enter-to="opacity-100 scale-100 translate-y-0" + class="w-full h-full" + enter="ease-out duration-200" + enter-from="opacity-0 scale-[0.995]" + enter-to="opacity-100 scale-100" leave="ease-in duration-100" - leave-from="opacity-100 scale-100 translate-y-0" - leave-to="opacity-0 scale-[0.98] translate-y-1"> - <!-- Fixed footprint: h-[min(...)] (NOT max-h) so the panel never resizes or re-centers between tabs. --> + leave-from="opacity-100 scale-100" + leave-to="opacity-0 scale-[0.995]"> + <!-- Mirrors the chat page shell: full-height left rail + right column + with a transparent top bar and an inset surface content panel. --> <DialogPanel - class="flex flex-col w-[min(880px,94vw)] h-[min(620px,86vh)] overflow-hidden" - style="border-radius: var(--radius-xl); background-color: var(--color-surface); border: 1px solid var(--color-border); box-shadow: var(--shadow-lg)" + class="flex w-full h-full overflow-hidden" + style="background-color: var(--color-background)" > - <!-- Header --> - <div class="flex items-center gap-2.5 px-5 h-14 shrink-0" style="border-bottom: 1px solid var(--color-border); background-color: var(--color-sidebar-bg)"> - <span class="w-[5px] h-[5px] rounded-[1px]" style="background-color: var(--color-primary)" /> - <DialogTitle class="text-[13px] font-semibold tracking-tight" style="font-family: var(--font-sans); color: var(--color-foreground)">Settings</DialogTitle> + <!-- Left rail (shell tone, like the sidebar): back-to-workspace at + the top, then the section nav. --> + <nav class="settings-rail shrink-0 flex flex-col"> <button - class="ml-auto grid place-items-center w-7 h-7 rounded-md transition-colors cursor-pointer hover:bg-[var(--color-secondary)]" - style="color: var(--color-muted-foreground)" - aria-label="Close" + class="settings-back group flex items-center gap-1.5 h-9 px-2.5 mb-1.5 rounded-md text-[13px] font-medium transition-colors cursor-pointer" @click="emit('close')" > - <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8"> - <path d="M5 5l10 10M15 5L5 15" stroke-linecap="round" /> + <svg class="w-4 h-4 transition-transform group-hover:-translate-x-0.5" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8"> + <path d="M11.5 5L6.5 10l5 5M6.5 10H16" stroke-linecap="round" stroke-linejoin="round" /> </svg> + Back to workspace </button> - </div> - - <!-- Body: nav rail + single scrolling content column. min-h-0 on both levels is required for the scroll fix. --> - <div class="flex flex-1 min-h-0"> - <!-- Nav rail --> - <nav class="shrink-0 w-[200px] py-2 px-2 overflow-y-auto flex flex-col gap-0.5" style="border-right: 1px solid var(--color-border); background-color: var(--color-sidebar-bg)"> + <div class="flex flex-col gap-0.5"> <button v-for="tab in (['general', 'appearance', 'providers', 'mcp', 'skills', 'ssh', 'channels', 'shortcuts'] as const)" :key="tab" @@ -557,11 +554,33 @@ const addProviderInfo = () => addProviderList.value.find(p => p.id === addSelect <svg class="w-3.5 h-3.5 shrink-0" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" v-html="iconFor[tab]" /> <span class="truncate">{{ tabLabel[tab] }}</span> </button> - </nav> + </div> + </nav> + + <!-- Right column: top bar (shell tone) + inset surface content panel. --> + <div class="flex flex-col flex-1 min-w-0"> + <div class="flex items-center gap-2.5 h-[52px] px-4 shrink-0"> + <span class="w-[5px] h-[5px] rounded-[1px]" style="background-color: var(--color-primary)" /> + <div class="flex flex-col min-w-0 leading-tight"> + <DialogTitle class="text-[13px] font-semibold tracking-tight" style="font-family: var(--font-sans); color: var(--color-foreground)">Settings</DialogTitle> + <span class="text-[11px]" style="color: var(--color-muted-foreground)">{{ tabLabel[activeTab] }}</span> + </div> + <button + class="ml-auto grid place-items-center w-7 h-7 rounded-md transition-colors cursor-pointer hover:bg-[var(--color-secondary)]" + style="color: var(--color-muted-foreground)" + aria-label="Close" + @click="emit('close')" + > + <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8"> + <path d="M5 5l10 10M15 5L5 15" stroke-linecap="round" /> + </svg> + </button> + </div> - <!-- Content frame (surface). Only the inner div scrolls. --> - <div class="flex-1 min-w-0 flex flex-col min-h-0" style="background-color: var(--color-surface)"> - <div class="flex-1 min-h-0 overflow-y-auto px-6 py-5"> + <!-- Inset content panel — matches .chat-panel. Only the inner div + scrolls; each tab block is centered and width-capped. --> + <div class="settings-panel flex flex-col flex-1 min-h-0"> + <div class="flex-1 min-h-0 overflow-y-auto px-8 py-7 [&>div]:max-w-3xl [&>div]:mx-auto"> <!-- General tab --> <div v-if="activeTab === 'general'" class="space-y-5"> <div class="flex items-center gap-2"> @@ -1284,24 +1303,47 @@ const addProviderInfo = () => addProviderList.value.find(p => p.id === addSelect </div> </div> </div> - - <!-- Footer: keyboard hint + low-chroma Done --> - <div class="flex items-center px-5 h-12 shrink-0" style="border-top: 1px solid var(--color-border); background-color: var(--color-sidebar-bg)"> - <span class="text-[11px] flex items-center gap-1.5" style="color: var(--color-muted-foreground)"> - Press - <kbd class="px-1.5 py-0.5 text-[10px] font-mono rounded" style="background-color: var(--color-secondary); border: 1px solid var(--color-border); color: var(--color-muted-foreground)">Esc</kbd> - to close - </span> - <button - class="ml-auto h-7 px-3 text-xs font-medium rounded-md cursor-pointer transition-colors hover:bg-[rgba(255,132,0,0.16)]" - style="color: var(--color-primary); background-color: rgba(255,132,0,0.1)" - @click="emit('close')"> - Done - </button> - </div> </DialogPanel> </TransitionChild> </div> </Dialog> </TransitionRoot> </template> + +<style scoped> +/* Left rail mirrors the chat sidebar: same width + shell tone, no border. */ +.settings-rail { + width: var(--sidebar-width); + padding: 12px; + overflow-y: auto; + background: var(--color-background); +} + +.settings-back { + color: var(--color-muted-foreground); +} +.settings-back:hover { + background: var(--color-secondary); + color: var(--color-foreground); +} + +/* Inset content panel — identical treatment to App.vue's .chat-panel so the two + pages read as the same layout. */ +.settings-panel { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: 14px; + margin: 4px 14px 14px; + overflow: hidden; + box-shadow: var(--shadow-sm); +} + +@media (max-width: 640px) { + .settings-rail { + width: 100%; + } + .settings-panel { + margin: 2px 8px 8px; + } +} +</style> diff --git a/web/src/components/SetupView.vue b/web/src/components/SetupView.vue index 8f44ea1..86151e1 100644 --- a/web/src/components/SetupView.vue +++ b/web/src/components/SetupView.vue @@ -146,233 +146,310 @@ function finish() { </script> <template> - <div class="fixed inset-0 bg-zinc-50 dark:bg-zinc-950 flex items-center justify-center z-50"> - <div class="w-full max-w-lg mx-auto px-6"> - <!-- Logo --> - <div class="flex items-center justify-center gap-0 mb-8 select-none" style="font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'Roboto Mono', Menlo, Monaco, monospace; font-size: 32px; font-weight: 700;"> - <span class="text-zinc-400 dark:text-zinc-500">[</span><span style="color: #FF8400;">J</span><span class="text-zinc-900 dark:text-zinc-300">CODE</span><span class="text-zinc-400 dark:text-zinc-500">]</span> - </div> - - <!-- Done state --> - <div v-if="step === 'done'" class="text-center animate-fade-in"> - <div class="w-16 h-16 rounded-full bg-emerald-100 dark:bg-emerald-500/15 flex items-center justify-center mx-auto mb-5"> - <svg class="w-8 h-8 text-emerald-600 dark:text-emerald-400" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"> - <path d="M5 13l4 4L19 7" /> - </svg> + <!-- Onboarding is a full-page, framed window so first-run matches the app's + enclosed look and brand palette (orange), not a one-off green theme. --> + <div class="setup-viewport" style="z-index: var(--z-modal)"> + <div class="setup-frame"> + <div class="w-full max-w-lg mx-auto px-6"> + <!-- Logo --> + <div class="flex items-center justify-center gap-0 mb-8 select-none" style="font-family: var(--font-mono); font-size: 32px; font-weight: 700;"> + <span style="color: var(--color-muted-foreground)">[</span><span style="color: var(--color-primary)">J</span><span style="color: var(--color-foreground)">CODE</span><span style="color: var(--color-muted-foreground)">]</span> </div> - <h2 class="text-xl font-semibold text-zinc-800 dark:text-zinc-100 mb-2" style="font-family: var(--font-sans)">You're all set!</h2> - <p class="text-sm text-zinc-500 dark:text-zinc-400 mb-6"> - Using <span class="font-mono text-zinc-700 dark:text-zinc-300">{{ selectedModel }}</span> via <span class="font-mono text-zinc-700 dark:text-zinc-300">{{ selectedProviderInfo?.name || selectedProvider }}</span> - </p> - <button - class="px-6 py-2.5 bg-emerald-500 hover:bg-emerald-600 text-white rounded-lg text-sm font-medium transition-colors cursor-pointer shadow-sm" - @click="finish" - > - Start coding - </button> - </div> - <!-- Setup steps --> - <div v-else class="bg-white dark:bg-zinc-900 border border-zinc-200 dark:border-zinc-700 rounded-xl shadow-sm overflow-hidden"> - <!-- Step indicator --> - <div class="flex items-center gap-2 px-6 pt-5 pb-3"> - <div class="flex items-center gap-1.5"> - <div - v-for="s in (['provider', 'model', 'apikey'] as const)" - :key="s" - class="w-2 h-2 rounded-full transition-colors" - :class="step === s ? 'bg-emerald-500' : ['provider', 'model', 'apikey'].indexOf(step) > ['provider', 'model', 'apikey'].indexOf(s) ? 'bg-emerald-400' : 'bg-zinc-300 dark:bg-zinc-600'" - /> + <!-- Done state --> + <div v-if="step === 'done'" class="text-center animate-fade-in"> + <div class="w-16 h-16 rounded-full flex items-center justify-center mx-auto mb-5" style="background: var(--color-success-bg)"> + <svg class="w-8 h-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="color: var(--color-success-fg)"> + <path d="M5 13l4 4L19 7" /> + </svg> </div> - <span class="text-[10px] text-zinc-400 dark:text-zinc-500 uppercase tracking-wider ml-auto"> - {{ step === 'provider' ? 'Step 1: Choose Provider' : step === 'model' ? 'Step 2: Choose Model' : 'Step 3: API Key' }} - </span> + <h2 class="text-xl font-semibold mb-2" style="font-family: var(--font-sans); color: var(--color-foreground)">You're all set!</h2> + <p class="text-sm mb-6" style="color: var(--color-muted-foreground)"> + Using <span class="font-mono" style="color: var(--color-foreground)">{{ selectedModel }}</span> via <span class="font-mono" style="color: var(--color-foreground)">{{ selectedProviderInfo?.name || selectedProvider }}</span> + </p> + <button + class="px-6 py-2.5 rounded-lg text-sm font-medium transition-opacity cursor-pointer shadow-sm hover:opacity-90" + style="background: var(--color-primary); color: var(--color-on-primary, #fff)" + @click="finish" + > + Start coding + </button> </div> - <!-- Provider selection --> - <div v-if="step === 'provider'" class="px-6 pb-5"> - <h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100 mb-1" style="font-family: var(--font-sans)">Choose a Provider</h2> - <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-3">Select the AI provider you'd like to use.</p> - - <input - v-model="providerSearch" - type="text" - placeholder="Search providers..." - class="w-full px-3 py-2 text-sm bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg mb-3 outline-none focus:border-emerald-400 dark:focus:border-emerald-500 transition-colors" - /> - - <div v-if="loading" class="text-center py-8 text-sm text-zinc-400 animate-pulse">Loading providers...</div> - <div v-else-if="filteredProviders.length === 0" class="text-center py-8 text-sm text-zinc-400">No providers found</div> - <div v-else class="space-y-1.5 max-h-72 overflow-y-auto pr-1"> - <button - v-for="p in filteredProviders" - :key="p.id" - class="w-full px-4 py-3 text-left rounded-lg border transition-all cursor-pointer group" - :class="selectedProvider === p.id - ? 'border-emerald-400 dark:border-emerald-500 bg-emerald-50 dark:bg-emerald-500/10' - : 'border-zinc-200 dark:border-zinc-700 hover:border-emerald-300 dark:hover:border-emerald-600 hover:bg-zinc-50 dark:hover:bg-zinc-800'" - @click="selectProvider(p.id)" - > - <div class="flex items-center justify-between"> - <div> - <div class="text-sm font-medium text-zinc-800 dark:text-zinc-100">{{ p.name }}</div> - <div v-if="p.doc" class="text-[10px] text-zinc-400 dark:text-zinc-500 mt-0.5">{{ p.doc }}</div> - </div> - <div class="flex items-center gap-2"> - <span v-if="p.tag === 'recommended'" class="text-[10px] px-1.5 py-0.5 rounded-full bg-amber-100 dark:bg-amber-500/15 text-amber-600 dark:text-amber-400 font-medium">Recommended</span> - <span v-if="p.tag === 'local'" class="text-[10px] px-1.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-500/15 text-blue-600 dark:text-blue-400 font-medium">Local</span> - <span v-if="p.configured" class="text-[10px] px-1.5 py-0.5 rounded-full bg-emerald-100 dark:bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 font-medium">configured</span> - <svg class="w-4 h-4 text-zinc-300 dark:text-zinc-600 group-hover:text-emerald-400 transition-colors" viewBox="0 0 20 20" fill="currentColor"> - <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> - </svg> - </div> - </div> - </button> + <!-- Setup steps --> + <div v-else class="rounded-xl overflow-hidden" style="background: var(--color-surface); border: 1px solid var(--color-border); box-shadow: var(--shadow-sm)"> + <!-- Step indicator --> + <div class="flex items-center gap-2 px-6 pt-5 pb-3"> + <div class="flex items-center gap-1.5"> + <div + v-for="s in (['provider', 'model', 'apikey'] as const)" + :key="s" + class="w-2 h-2 rounded-full transition-colors" + :style="{ backgroundColor: step === s + ? 'var(--color-primary)' + : (['provider', 'model', 'apikey'].indexOf(step) > ['provider', 'model', 'apikey'].indexOf(s) ? 'rgba(255,132,0,0.45)' : 'var(--color-border)') }" + /> + </div> + <span class="text-[10px] uppercase tracking-wider ml-auto" style="color: var(--color-muted-foreground)"> + {{ step === 'provider' ? 'Step 1: Choose Provider' : step === 'model' ? 'Step 2: Choose Model' : 'Step 3: API Key' }} + </span> </div> - </div> - <!-- Model selection --> - <div v-if="step === 'model'" class="px-6 pb-5"> - <div class="flex items-center gap-2 mb-1"> - <button class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors cursor-pointer" @click="goBack"> - <svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> - <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> - </svg> - </button> - <h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100" style="font-family: var(--font-sans)">Choose a Model</h2> - </div> - <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-3 ml-6">For <span class="font-mono">{{ selectedProviderInfo?.name }}</span></p> - - <input - v-model="modelSearch" - type="text" - placeholder="Search models..." - class="w-full px-3 py-2 text-sm bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg mb-3 outline-none focus:border-emerald-400 dark:focus:border-emerald-500 transition-colors" - /> - - <div v-if="loading" class="text-center py-8 text-sm text-zinc-400 animate-pulse">Loading models...</div> - <div v-else-if="filteredModels.length === 0" class="text-center py-8 text-sm text-zinc-400">No models found</div> - <div v-else class="space-y-1 max-h-72 overflow-y-auto pr-1"> - <button - v-for="m in filteredModels" - :key="m.id" - class="w-full px-4 py-2.5 text-left rounded-lg border transition-all cursor-pointer" - :class="selectedModel === m.id - ? 'border-emerald-400 dark:border-emerald-500 bg-emerald-50 dark:bg-emerald-500/10' - : 'border-zinc-200 dark:border-zinc-700 hover:border-emerald-300 dark:hover:border-emerald-600 hover:bg-zinc-50 dark:hover:bg-zinc-800'" - @click="selectModel(m.id)" - > - <div class="flex items-center justify-between"> - <div> - <div class="text-sm font-medium text-zinc-800 dark:text-zinc-100 font-mono">{{ m.id }}</div> - <div v-if="m.name && m.name !== m.id" class="text-[10px] text-zinc-400 dark:text-zinc-500 mt-0.5">{{ m.name }}</div> - </div> - <div class="flex items-center gap-2"> - <span v-if="m.context_limit" class="text-[10px] text-zinc-400 dark:text-zinc-500">{{ (m.context_limit / 1000).toFixed(0) }}k ctx</span> - <span v-if="m.reasoning" class="text-[10px] px-1.5 py-0.5 rounded-full bg-blue-100 dark:bg-blue-500/15 text-blue-600 dark:text-blue-400">reasoning</span> - <svg class="w-4 h-4 text-zinc-300 dark:text-zinc-600" viewBox="0 0 20 20" fill="currentColor"> - <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> - </svg> + <!-- Provider selection --> + <div v-if="step === 'provider'" class="px-6 pb-5"> + <h2 class="text-base font-semibold mb-1" style="font-family: var(--font-sans); color: var(--color-foreground)">Choose a Provider</h2> + <p class="text-xs mb-3" style="color: var(--color-muted-foreground)">Select the AI provider you'd like to use.</p> + + <input + v-model="providerSearch" + type="text" + placeholder="Search providers..." + class="setup-input w-full px-3 py-2 text-sm rounded-lg mb-3 outline-none" + /> + + <div v-if="loading" class="text-center py-8 text-sm animate-pulse" style="color: var(--color-muted-foreground)">Loading providers...</div> + <div v-else-if="filteredProviders.length === 0" class="text-center py-8 text-sm" style="color: var(--color-muted-foreground)">No providers found</div> + <div v-else class="space-y-1.5 max-h-72 overflow-y-auto pr-1"> + <button + v-for="p in filteredProviders" + :key="p.id" + class="setup-option w-full px-4 py-3 text-left rounded-lg cursor-pointer group" + :class="{ selected: selectedProvider === p.id }" + @click="selectProvider(p.id)" + > + <div class="flex items-center justify-between"> + <div> + <div class="text-sm font-medium" style="color: var(--color-foreground)">{{ p.name }}</div> + <div v-if="p.doc" class="text-[10px] mt-0.5" style="color: var(--color-muted-foreground)">{{ p.doc }}</div> + </div> + <div class="flex items-center gap-2"> + <span v-if="p.tag === 'recommended'" class="text-[10px] px-1.5 py-0.5 rounded-full font-medium" style="background: rgba(255,132,0,0.12); color: var(--color-primary)">Recommended</span> + <span v-if="p.tag === 'local'" class="text-[10px] px-1.5 py-0.5 rounded-full font-medium" style="background: var(--color-info-bg); color: var(--color-info-fg)">Local</span> + <span v-if="p.configured" class="text-[10px] px-1.5 py-0.5 rounded-full font-medium" style="background: var(--color-success-bg); color: var(--color-success-fg)">configured</span> + <svg class="setup-chevron w-4 h-4 transition-colors" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> + </svg> + </div> </div> - </div> - </button> + </button> + </div> </div> - </div> - <!-- API Key input --> - <div v-if="step === 'apikey'" class="px-6 pb-5"> - <div class="flex items-center gap-2 mb-1"> - <button class="text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 transition-colors cursor-pointer" @click="goBack"> - <svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> - <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> - </svg> - </button> - <h2 class="text-base font-semibold text-zinc-800 dark:text-zinc-100" style="font-family: var(--font-sans)">Enter API Key</h2> + <!-- Model selection --> + <div v-if="step === 'model'" class="px-6 pb-5"> + <div class="flex items-center gap-2 mb-1"> + <button class="setup-back transition-colors cursor-pointer" @click="goBack"> + <svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> + </svg> + </button> + <h2 class="text-base font-semibold" style="font-family: var(--font-sans); color: var(--color-foreground)">Choose a Model</h2> + </div> + <p class="text-xs mb-3 ml-6" style="color: var(--color-muted-foreground)">For <span class="font-mono">{{ selectedProviderInfo?.name }}</span></p> + + <input + v-model="modelSearch" + type="text" + placeholder="Search models..." + class="setup-input w-full px-3 py-2 text-sm rounded-lg mb-3 outline-none" + /> + + <div v-if="loading" class="text-center py-8 text-sm animate-pulse" style="color: var(--color-muted-foreground)">Loading models...</div> + <div v-else-if="filteredModels.length === 0" class="text-center py-8 text-sm" style="color: var(--color-muted-foreground)">No models found</div> + <div v-else class="space-y-1 max-h-72 overflow-y-auto pr-1"> + <button + v-for="m in filteredModels" + :key="m.id" + class="setup-option w-full px-4 py-2.5 text-left rounded-lg cursor-pointer" + :class="{ selected: selectedModel === m.id }" + @click="selectModel(m.id)" + > + <div class="flex items-center justify-between"> + <div> + <div class="text-sm font-medium font-mono" style="color: var(--color-foreground)">{{ m.id }}</div> + <div v-if="m.name && m.name !== m.id" class="text-[10px] mt-0.5" style="color: var(--color-muted-foreground)">{{ m.name }}</div> + </div> + <div class="flex items-center gap-2"> + <span v-if="m.context_limit" class="text-[10px]" style="color: var(--color-muted-foreground)">{{ (m.context_limit / 1000).toFixed(0) }}k ctx</span> + <span v-if="m.reasoning" class="text-[10px] px-1.5 py-0.5 rounded-full" style="background: var(--color-info-bg); color: var(--color-info-fg)">reasoning</span> + <svg class="setup-chevron w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" clip-rule="evenodd" /> + </svg> + </div> + </div> + </button> + </div> </div> - <p class="text-xs text-zinc-500 dark:text-zinc-400 mb-4 ml-6"> - For <span class="font-mono">{{ selectedProviderInfo?.name }}</span> · <span class="font-mono">{{ selectedModel }}</span> - </p> - <div class="space-y-3 ml-6"> - <div v-if="selectedProviderInfo?.env?.length" class="px-3 py-2 rounded-md bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700"> - <div class="text-[10px] text-zinc-400 dark:text-zinc-500 mb-1">Environment variable</div> - <div class="text-xs font-mono text-zinc-600 dark:text-zinc-300">{{ selectedProviderInfo.env[0] }}</div> + <!-- API Key input --> + <div v-if="step === 'apikey'" class="px-6 pb-5"> + <div class="flex items-center gap-2 mb-1"> + <button class="setup-back transition-colors cursor-pointer" @click="goBack"> + <svg class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" clip-rule="evenodd" /> + </svg> + </button> + <h2 class="text-base font-semibold" style="font-family: var(--font-sans); color: var(--color-foreground)">Enter API Key</h2> </div> + <p class="text-xs mb-4 ml-6" style="color: var(--color-muted-foreground)"> + For <span class="font-mono">{{ selectedProviderInfo?.name }}</span> · <span class="font-mono">{{ selectedModel }}</span> + </p> + + <div class="space-y-3 ml-6"> + <div v-if="selectedProviderInfo?.env?.length" class="px-3 py-2 rounded-md" style="background: var(--color-muted); border: 1px solid var(--color-border)"> + <div class="text-[10px] mb-1" style="color: var(--color-muted-foreground)">Environment variable</div> + <div class="text-xs font-mono" style="color: var(--color-foreground)">{{ selectedProviderInfo.env[0] }}</div> + </div> + + <div> + <label class="block text-[10px] uppercase tracking-wider mb-1 font-medium" style="color: var(--color-muted-foreground)">API Key</label> + <div class="relative"> + <input + v-model="apiKey" + :type="showApiKey ? 'text' : 'password'" + placeholder="sk-..." + class="setup-input w-full px-3 py-2 text-sm font-mono rounded-lg outline-none pr-10" + :class="{ valid: validationResult?.valid, invalid: validationResult?.valid === false }" + @keydown.enter="submitSetup" + /> + <button + class="setup-back absolute right-2 top-1/2 -translate-y-1/2 cursor-pointer" + @click="showApiKey = !showApiKey" + > + <svg v-if="!showApiKey" class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> + <path d="M10 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z" /> + <path fill-rule="evenodd" d="M.664 10.59a1.651 1.651 0 010-1.186A10.004 10.004 0 0110 3c4.257 0 7.893 2.66 9.336 6.41.147.381.146.804 0 1.186A10.004 10.004 0 0110 17c-4.257 0-7.893-2.66-9.336-6.41zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" /> + </svg> + <svg v-else class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> + <path fill-rule="evenodd" d="M3.28 2.22a.75.75 0 00-1.06 1.06l14.5 14.5a.75.75 0 101.06-1.06l-14.5-14.5z" clip-rule="evenodd" /> + <path d="M4.262 6.49A8.97 8.97 0 002.175 10.3a1.655 1.655 0 000 .4 10.004 10.004 0 007.548 5.953 8.97 8.97 0 004.988-.628l-1.446-1.446a4.003 4.003 0 01-5.54-5.54L4.262 6.49z" /> + </svg> + </button> + </div> + </div> - <div> - <label class="block text-[10px] text-zinc-400 dark:text-zinc-500 uppercase tracking-wider mb-1 font-medium">API Key</label> - <div class="relative"> + <div> + <label class="block text-[10px] uppercase tracking-wider mb-1 font-medium" style="color: var(--color-muted-foreground)">Base URL <span class="normal-case">(optional)</span></label> <input - v-model="apiKey" - :type="showApiKey ? 'text' : 'password'" - placeholder="sk-..." - class="w-full px-3 py-2 text-sm font-mono bg-zinc-50 dark:bg-zinc-800 border rounded-lg outline-none transition-colors pr-10" - :class="validationResult?.valid ? 'border-emerald-400 dark:border-emerald-500' : validationResult?.valid === false ? 'border-red-300 dark:border-red-500' : 'border-zinc-200 dark:border-zinc-700 focus:border-emerald-400 dark:focus:border-emerald-500'" + v-model="baseURL" + type="text" + :placeholder="selectedProviderInfo?.api || 'https://api.example.com/v1'" + class="setup-input w-full px-3 py-2 text-sm font-mono rounded-lg outline-none" @keydown.enter="submitSetup" /> + </div> + + <!-- Validate connection --> + <div class="flex items-center gap-2"> <button - class="absolute right-2 top-1/2 -translate-y-1/2 text-zinc-400 hover:text-zinc-600 dark:hover:text-zinc-300 cursor-pointer" - @click="showApiKey = !showApiKey" + :disabled="validating || !apiKey.trim()" + class="setup-secondary px-3 py-1.5 text-xs rounded-lg disabled:opacity-50 cursor-pointer transition-colors" + @click="validateConnection" > - <svg v-if="!showApiKey" class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> - <path d="M10 12.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5z" /> - <path fill-rule="evenodd" d="M.664 10.59a1.651 1.651 0 010-1.186A10.004 10.004 0 0110 3c4.257 0 7.893 2.66 9.336 6.41.147.381.146.804 0 1.186A10.004 10.004 0 0110 17c-4.257 0-7.893-2.66-9.336-6.41zM14 10a4 4 0 11-8 0 4 4 0 018 0z" clip-rule="evenodd" /> - </svg> - <svg v-else class="w-4 h-4" viewBox="0 0 20 20" fill="currentColor"> - <path fill-rule="evenodd" d="M3.28 2.22a.75.75 0 00-1.06 1.06l14.5 14.5a.75.75 0 101.06-1.06l-14.5-14.5z" clip-rule="evenodd" /> - <path d="M4.262 6.49A8.97 8.97 0 002.175 10.3a1.655 1.655 0 000 .4 10.004 10.004 0 007.548 5.953 8.97 8.97 0 004.988-.628l-1.446-1.446a4.003 4.003 0 01-5.54-5.54L4.262 6.49z" /> - </svg> + {{ validating ? 'Checking...' : 'Test Connection' }} </button> + <span v-if="validationResult?.valid" class="text-xs flex items-center gap-1" style="color: var(--color-success-fg)"> + <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /></svg> + Connected + </span> + <span v-if="validationResult?.valid === false" class="text-xs" style="color: var(--color-error-fg)">{{ validationResult.error }}</span> </div> - </div> - <div> - <label class="block text-[10px] text-zinc-400 dark:text-zinc-500 uppercase tracking-wider mb-1 font-medium">Base URL <span class="normal-case">(optional)</span></label> - <input - v-model="baseURL" - type="text" - :placeholder="selectedProviderInfo?.api || 'https://api.example.com/v1'" - class="w-full px-3 py-2 text-sm font-mono bg-zinc-50 dark:bg-zinc-800 border border-zinc-200 dark:border-zinc-700 rounded-lg outline-none focus:border-emerald-400 dark:focus:border-emerald-500 transition-colors" - @keydown.enter="submitSetup" - /> - </div> + <!-- Error --> + <div v-if="error" class="px-3 py-2 rounded-md" style="background: var(--color-error-bg); border: 1px solid var(--color-error-fg)"> + <span class="text-xs" style="color: var(--color-error-fg)">{{ error }}</span> + </div> - <!-- Validate connection --> - <div class="flex items-center gap-2"> <button - :disabled="validating || !apiKey.trim()" - class="px-3 py-1.5 text-xs border border-zinc-200 dark:border-zinc-700 rounded-lg hover:bg-zinc-50 dark:hover:bg-zinc-800 disabled:opacity-50 cursor-pointer transition-colors text-zinc-600 dark:text-zinc-300" - @click="validateConnection" + :disabled="loading || !apiKey.trim()" + class="w-full px-4 py-2.5 rounded-lg text-sm font-medium transition-opacity cursor-pointer shadow-sm hover:opacity-90 disabled:opacity-50 disabled:cursor-not-allowed" + style="background: var(--color-primary); color: var(--color-on-primary, #fff)" + @click="submitSetup" > - {{ validating ? 'Checking...' : 'Test Connection' }} + {{ loading ? 'Setting up...' : 'Complete Setup' }} </button> - <span v-if="validationResult?.valid" class="text-xs text-emerald-600 dark:text-emerald-400 flex items-center gap-1"> - <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z" clip-rule="evenodd" /></svg> - Connected - </span> - <span v-if="validationResult?.valid === false" class="text-xs text-red-500">{{ validationResult.error }}</span> - </div> - - <!-- Error --> - <div v-if="error" class="px-3 py-2 rounded-md bg-red-50 dark:bg-red-500/10 border border-red-200 dark:border-red-500/30"> - <span class="text-xs text-red-600 dark:text-red-400">{{ error }}</span> </div> - - <button - :disabled="loading || !apiKey.trim()" - class="w-full px-4 py-2.5 bg-emerald-500 hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-sm font-medium transition-colors cursor-pointer shadow-sm" - @click="submitSetup" - > - {{ loading ? 'Setting up...' : 'Complete Setup' }} - </button> </div> </div> - </div> - <!-- Footer hint --> - <p v-if="step !== 'done'" class="text-center text-[10px] text-zinc-400 dark:text-zinc-600 mt-4"> - Configuration saved to <span class="font-mono">~/.jcode/config.json</span> - </p> + <!-- Footer hint --> + <p v-if="step !== 'done'" class="text-center text-[10px] mt-4" style="color: var(--color-muted-foreground)"> + Configuration saved to <span class="font-mono">~/.jcode/config.json</span> + </p> + </div> </div> </div> </template> + +<style scoped> +.setup-viewport { + position: fixed; + inset: 0; + background: var(--color-background); + display: flex; +} + +.setup-frame { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + overflow: auto; +} + +.setup-input { + background: var(--color-background); + border: 1px solid var(--color-border); + color: var(--color-foreground); + transition: border-color 0.15s; +} +.setup-input::placeholder { + color: var(--color-muted-foreground); +} +.setup-input:focus { + border-color: var(--color-primary); +} +.setup-input.valid { + border-color: var(--color-success-fg); +} +.setup-input.invalid { + border-color: var(--color-error-fg); +} + +.setup-option { + border: 1px solid var(--color-border); + background: transparent; + transition: border-color 0.15s, background 0.15s; +} +.setup-option:hover { + border-color: color-mix(in srgb, var(--color-primary) 55%, var(--color-border)); + background: var(--color-muted); +} +.setup-option.selected { + border-color: var(--color-primary); + background: rgba(255, 132, 0, 0.08); +} + +.setup-chevron { + color: var(--color-border); +} +.setup-option:hover .setup-chevron { + color: var(--color-primary); +} + +.setup-back { + color: var(--color-muted-foreground); +} +.setup-back:hover { + color: var(--color-foreground); +} + +.setup-secondary { + border: 1px solid var(--color-border); + color: var(--color-muted-foreground); +} +.setup-secondary:hover { + background: var(--color-muted); + color: var(--color-foreground); +} +</style> diff --git a/web/src/components/Sidebar.vue b/web/src/components/Sidebar.vue index 5561ae3..391bc0f 100644 --- a/web/src/components/Sidebar.vue +++ b/web/src/components/Sidebar.vue @@ -1,8 +1,31 @@ <!-- eslint-disable vue/multi-word-component-names --> <script setup lang="ts"> +import { ref, computed, onMounted, watch } from 'vue' +import { + Menu as HMenu, + MenuButton, + MenuItems, + MenuItem, +} from '@headlessui/vue' +import { + ChevronRight, + Folder, + FolderOpen, + Plus, + MoreHorizontal, + Pin, + Archive, + ArchiveRestore, + Pencil, + Trash2, + MailOpen, +} from 'lucide-vue-next' import { useChatStore } from '@/stores/chat' +import { useProjectStore } from '@/stores/project' +import type { TaskItem } from '@/types/api' const store = useChatStore() +const projectStore = useProjectStore() defineProps<{ resolvedTheme: 'light' | 'dark' @@ -15,91 +38,206 @@ const emit = defineEmits<{ toggleTheme: [] }>() -async function handleDelete(uuid: string) { - await store.deleteSession(uuid) +// Expanded project paths. The active project is auto-expanded. +const expanded = ref<Set<string>>(new Set()) +const showArchived = ref(false) + +const activePath = computed(() => projectStore.activeProject?.path || store.pwd) + +function isExpanded(path: string): boolean { + return expanded.value.has(path) +} +function toggle(path: string) { + const next = new Set(expanded.value) + if (next.has(path)) next.delete(path) + else next.add(path) + expanded.value = next +} + +// Project nodes sorted with the active project first, then alphabetically. +const projectNodes = computed(() => { + const nodes = [...projectStore.projectsForTree] + return nodes.sort((a, b) => { + if (a.path === activePath.value) return -1 + if (b.path === activePath.value) return 1 + return a.name.localeCompare(b.name) + }) +}) + +function tasksFor(path: string): TaskItem[] { + const list = projectStore.tasksByProject[path] || [] + return showArchived.value ? list : list.filter((t) => !t.archived) +} + +function visibleCount(path: string): number { + return tasksFor(path).length } -function formatDate(ts: string): string { - const d = new Date(ts) - const now = new Date() - if (d.toDateString() === now.toDateString()) { - return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) +async function refresh() { + await projectStore.fetchAllTasks() +} + +onMounted(() => { + refresh() + if (activePath.value) expanded.value = new Set([activePath.value]) +}) + +// Keep the active project expanded and the tree fresh as the active session / +// session list changes (new task, send, delete). +watch(activePath, (p) => { + if (p && !expanded.value.has(p)) toggle(p) +}) +watch(() => store.sessions.length, refresh) +watch(() => store.currentSessionId, refresh) + +async function openTask(task: TaskItem) { + if (task.unread) projectStore.updateTaskMeta(task.uuid, { unread: false }) + if (activePath.value !== task.project) { + const ok = await projectStore.openProject(task.project) + if (!ok) return + await store.fetchHealth() } - return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + await store.loadSession(task.uuid) +} + +function isActiveTask(task: TaskItem): boolean { + return task.uuid === store.currentSessionId && activePath.value === task.project +} + +async function handleDelete(task: TaskItem) { + await store.deleteSession(task.uuid) + await refresh() +} + +async function renameTask(task: TaskItem) { + const title = window.prompt('Rename task', task.title || '') + if (title != null && title.trim()) { + await projectStore.updateTaskMeta(task.uuid, { title: title.trim() }) + } +} + +function taskTitle(t: TaskItem): string { + return t.title || t.uuid.slice(0, 8) + '…' +} + +function relativeTime(ts: string): string { + if (!ts) return '' + const then = new Date(ts).getTime() + if (Number.isNaN(then)) return '' + const mins = Math.floor((Date.now() - then) / 60000) + if (mins < 1) return 'now' + if (mins < 60) return `${mins}m` + const hrs = Math.floor(mins / 60) + if (hrs < 24) return `${hrs}h` + const days = Math.floor(hrs / 24) + if (days < 30) return `${days}d` + return new Date(ts).toLocaleDateString([], { month: 'short', day: 'numeric' }) } </script> <template> <aside class="sidebar"> - <!-- Project header --> + <!-- New task --> <div class="sidebar-header"> - <button - class="project-btn" - @click="emit('openProjects')" - > - <div class="project-logo"> - J - </div> - <div class="project-info"> - <div class="project-name">{{ store.projectName || 'jcode' }}</div> - <div class="project-path">{{ store.pwd }}</div> - </div> - <svg class="chevron-icon" viewBox="0 0 20 20" fill="currentColor"> - <path fill-rule="evenodd" d="M10 3a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02L10 4.852 7.3 7.76a.75.75 0 01-1.1-1.02l3.25-3.5A.75.75 0 0110 3zm-3.76 9.2a.75.75 0 011.06.04l2.7 2.908 2.7-2.908a.75.75 0 111.1 1.02l-3.25 3.5a.75.75 0 01-1.1 0l-3.25-3.5a.75.75 0 01.04-1.06z" clip-rule="evenodd" /> - </svg> - </button> - - <button - class="new-chat-btn" - @click="store.newSession()" - > - <svg class="w-4 h-4" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.5"> - <path d="M13.586 3.586a2 2 0 112.828 2.828l-.793.793-2.828-2.828.793-.793zM11.379 5.793L3 14.172V17h2.828l8.38-8.379-2.83-2.828z" /> - </svg> - <span>New chat</span> + <button class="new-task-btn" @click="store.newSession()"> + <Plus :size="16" /> + <span>New task</span> </button> </div> - <!-- Sessions list --> - <div class="sessions-list"> - <div v-if="store.sessions.length === 0" class="empty-state"> - No conversations yet - </div> - <div - v-for="s in store.sessions" - :key="s.uuid" - class="session-item" - :class="{ active: s.uuid === store.currentSessionId }" - @click="store.loadSession(s.uuid)" - > - <div class="session-content"> - <div class="session-title">{{ s.title || s.uuid.slice(0, 8) + '…' }}</div> - <div class="session-subtitle">{{ s.model }} · {{ formatDate(s.created_at) }}</div> + <!-- Workspace tree --> + <div class="tree"> + <div class="tree-head"> + <span class="tree-label">Workspace</span> + <div class="tree-head-actions"> + <button + class="tree-icon-btn" + :class="{ on: showArchived }" + title="Show archived" + @click="showArchived = !showArchived" + > + <Archive :size="13" /> + </button> + <button class="tree-icon-btn" title="Open project" @click="emit('openProjects')"> + <Plus :size="14" /> + </button> </div> - <button - class="session-delete" - @click.stop="handleDelete(s.uuid)" - title="Delete" - > - <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor"> - <path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 006 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 10.23 1.482l.149-.022.841 10.518A2.75 2.75 0 007.596 19h4.807a2.75 2.75 0 002.742-2.53l.841-10.519.149.023a.75.75 0 00.23-1.482A41.03 41.03 0 0014 4.193V3.75A2.75 2.75 0 0011.25 1h-2.5zM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4zM8.58 7.72a.75.75 0 00-1.5.06l.3 7.5a.75.75 0 101.5-.06l-.3-7.5zm4.34.06a.75.75 0 10-1.5-.06l-.3 7.5a.75.75 0 101.5.06l.3-7.5z" clip-rule="evenodd" /> - </svg> + </div> + + <div v-if="projectNodes.length === 0" class="empty-state">No projects yet</div> + + <div v-for="proj in projectNodes" :key="proj.path" class="project-group"> + <button class="project-row" :class="{ active: proj.path === activePath }" @click="toggle(proj.path)"> + <ChevronRight :size="14" class="proj-chevron" :class="{ open: isExpanded(proj.path) }" /> + <component :is="proj.path === activePath ? FolderOpen : Folder" :size="15" class="proj-icon" /> + <span class="proj-name">{{ proj.name }}</span> + <span v-if="visibleCount(proj.path) > 0" class="proj-count">{{ visibleCount(proj.path) }}</span> </button> + + <div v-show="isExpanded(proj.path)" class="task-list"> + <div v-if="visibleCount(proj.path) === 0" class="task-empty">No tasks</div> + <div + v-for="task in tasksFor(proj.path)" + :key="task.uuid" + class="task-row" + :class="{ active: isActiveTask(task), archived: task.archived }" + @click="openTask(task)" + > + <span class="task-dot" :class="{ unread: task.unread }" aria-hidden="true" /> + <Pin v-if="task.pinned" :size="11" class="task-pin" /> + <span class="task-title">{{ taskTitle(task) }}</span> + <span class="task-time">{{ relativeTime(task.created_at) }}</span> + + <HMenu as="div" class="task-menu" @click.stop> + <MenuButton class="task-menu-btn" title="Task actions" @click.stop> + <MoreHorizontal :size="14" /> + </MenuButton> + <transition + enter-active-class="pop-enter-active" + enter-from-class="pop-enter-from" + leave-active-class="pop-leave-active" + leave-to-class="pop-leave-to" + > + <MenuItems class="task-menu-items"> + <MenuItem v-slot="{ active }"> + <button class="tmi" :class="{ hl: active }" @click.stop="projectStore.updateTaskMeta(task.uuid, { pinned: !task.pinned })"> + <Pin :size="14" /> {{ task.pinned ? 'Unpin' : 'Pin' }} + </button> + </MenuItem> + <MenuItem v-slot="{ active }"> + <button class="tmi" :class="{ hl: active }" @click.stop="renameTask(task)"> + <Pencil :size="14" /> Rename + </button> + </MenuItem> + <MenuItem v-slot="{ active }"> + <button class="tmi" :class="{ hl: active }" @click.stop="projectStore.updateTaskMeta(task.uuid, { archived: !task.archived })"> + <component :is="task.archived ? ArchiveRestore : Archive" :size="14" /> {{ task.archived ? 'Unarchive' : 'Archive' }} + </button> + </MenuItem> + <MenuItem v-slot="{ active }"> + <button class="tmi" :class="{ hl: active }" @click.stop="projectStore.updateTaskMeta(task.uuid, { unread: !task.unread })"> + <MailOpen :size="14" /> {{ task.unread ? 'Mark read' : 'Mark unread' }} + </button> + </MenuItem> + <div class="tmi-sep" /> + <MenuItem v-slot="{ active }"> + <button class="tmi danger" :class="{ hl: active }" @click.stop="handleDelete(task)"> + <Trash2 :size="14" /> Delete + </button> + </MenuItem> + </MenuItems> + </transition> + </HMenu> + </div> + </div> </div> </div> <!-- Footer --> <div class="sidebar-footer"> - <div class="footer-model"> - {{ store.modelName || 'no model' }} - </div> + <div class="footer-model">{{ store.modelName || 'no model' }}</div> <div class="footer-actions"> - <!-- Theme toggle --> - <button - class="footer-btn" - @click="emit('toggleTheme')" - :title="resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'" - > + <button class="footer-btn" @click="emit('toggleTheme')" :title="resolvedTheme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'"> <svg v-if="resolvedTheme === 'dark'" class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor"> <path d="M10 2a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 2zM10 15a.75.75 0 01.75.75v1.5a.75.75 0 01-1.5 0v-1.5A.75.75 0 0110 15zM10 7a3 3 0 100 6 3 3 0 000-6zM15.657 5.404a.75.75 0 10-1.06-1.06l-1.061 1.06a.75.75 0 001.06 1.06l1.06-1.06zM6.464 14.596a.75.75 0 10-1.06-1.06l-1.06 1.06a.75.75 0 001.06 1.06l1.06-1.06zM18 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 0118 10zM5 10a.75.75 0 01-.75.75h-1.5a.75.75 0 010-1.5h1.5A.75.75 0 015 10zM14.596 15.657a.75.75 0 001.06-1.06l-1.06-1.061a.75.75 0 10-1.06 1.06l1.06 1.06zM5.404 6.464a.75.75 0 001.06-1.06l-1.06-1.06a.75.75 0 10-1.06 1.06l1.06 1.06z" /> </svg> @@ -107,12 +245,7 @@ function formatDate(ts: string): string { <path fill-rule="evenodd" d="M7.455 2.004a.75.75 0 01.26.77 7 7 0 009.958 7.967.75.75 0 011.067.853A8.5 8.5 0 116.647 1.921a.75.75 0 01.808.083z" clip-rule="evenodd" /> </svg> </button> - <!-- Settings --> - <button - class="footer-btn" - @click="emit('openSettings')" - title="Settings" - > + <button class="footer-btn" @click="emit('openSettings')" title="Settings"> <svg class="w-3.5 h-3.5" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M7.84 1.804A1 1 0 018.82 1h2.36a1 1 0 01.98.804l.331 1.652a6.993 6.993 0 011.929 1.115l1.598-.54a1 1 0 011.186.447l1.18 2.044a1 1 0 01-.205 1.251l-1.267 1.113a7.047 7.047 0 010 2.228l1.267 1.113a1 1 0 01.206 1.25l-1.18 2.045a1 1 0 01-1.187.447l-1.598-.54a6.993 6.993 0 01-1.929 1.115l-.33 1.652a1 1 0 01-.98.804H8.82a1 1 0 01-.98-.804l-.331-1.652a6.993 6.993 0 01-1.929-1.115l-1.598.54a1 1 0 01-1.186-.447l-1.18-2.044a1 1 0 01.205-1.251l1.267-1.114a7.05 7.05 0 010-2.227L1.821 7.773a1 1 0 01-.206-1.25l1.18-2.045a1 1 0 011.187-.447l1.598.54A6.993 6.993 0 017.51 3.456l.33-1.652zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd" /> </svg> @@ -129,188 +262,280 @@ function formatDate(ts: string): string { flex-direction: column; flex-shrink: 0; position: relative; - background: var(--color-sidebar-bg); - border-right: 1px solid var(--color-border); + background: var(--color-background); } .sidebar-header { - padding: 12px 12px 0; + padding: 12px 12px 8px; } -.project-btn { +.new-task-btn { display: flex; align-items: center; - gap: 10px; + justify-content: center; + gap: 6px; width: 100%; - padding: 8px; - margin-bottom: 12px; - border: none; + padding: 9px 0; + border: 1px solid var(--color-border); background: transparent; border-radius: 8px; + font-size: 13px; + font-weight: 500; + color: var(--color-muted-foreground); cursor: pointer; - text-align: left; - transition: background 0.15s; -} - -.project-btn:hover { - background: var(--color-muted); + transition: border-color 0.15s, color 0.15s; } - -.project-logo { - width: 28px; - height: 28px; - border-radius: 6px; - display: flex; - align-items: center; - justify-content: center; - font-size: 14px; - font-weight: 700; - font-family: var(--font-mono); - background: var(--color-foreground); - color: var(--color-background); - flex-shrink: 0; +.new-task-btn:hover { + border-color: var(--color-foreground); + color: var(--color-foreground); } -.project-info { - min-width: 0; +/* ─── Tree ─── */ +.tree { flex: 1; + overflow-y: auto; + padding: 4px 8px 8px; } -.project-name { - font-size: 13px; - font-weight: 600; - color: var(--color-foreground); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-family: var(--font-sans); +.tree-head { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 6px 4px; } - -.project-path { +.tree-label { font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; color: var(--color-muted-foreground); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-family: var(--font-mono); -} - -.chevron-icon { - width: 16px; - height: 16px; - color: var(--color-muted-foreground); - flex-shrink: 0; } - -.new-chat-btn { +.tree-head-actions { display: flex; - align-items: center; - justify-content: center; - gap: 6px; - width: 100%; - padding: 9px 0; - border: 1px solid var(--color-border); + gap: 2px; +} +.tree-icon-btn { + display: grid; + place-items: center; + width: 22px; + height: 22px; + border: none; background: transparent; - border-radius: 8px; - font-size: 13px; - font-weight: 500; + border-radius: 5px; color: var(--color-muted-foreground); cursor: pointer; - transition: border-color 0.15s, color 0.15s; - margin-bottom: 12px; + transition: background 0.15s, color 0.15s; } - -.new-chat-btn:hover { - border-color: var(--color-foreground); +.tree-icon-btn:hover { + background: var(--color-muted); color: var(--color-foreground); } - -.sessions-list { - flex: 1; - overflow-y: auto; - padding: 0 8px; +.tree-icon-btn.on { + color: var(--color-primary); } .empty-state { text-align: center; font-size: 11px; - padding: 40px 0; + padding: 24px 0; color: var(--color-muted-foreground); } -.session-item { +.project-group { + margin-bottom: 2px; +} + +.project-row { display: flex; align-items: center; - gap: 8px; - padding: 10px 10px; - border-radius: 8px; + gap: 6px; + width: 100%; + padding: 6px 6px; + border: none; + background: transparent; + border-radius: 6px; cursor: pointer; + text-align: left; transition: background 0.15s; - min-height: 50px; } - -.session-item:hover { +.project-row:hover { background: var(--color-muted); } - -.session-item.active { - background: var(--color-muted); +.proj-chevron { + color: var(--color-muted-foreground); + flex-shrink: 0; + transition: transform 0.15s; } - -.session-content { - min-width: 0; - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; +.proj-chevron.open { + transform: rotate(90deg); } - -.session-title { +.proj-icon { + color: var(--color-muted-foreground); + flex-shrink: 0; +} +.project-row.active .proj-icon { + color: var(--color-primary); +} +.proj-name { + flex: 1; + min-width: 0; font-size: 13px; font-weight: 500; color: var(--color-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - line-height: 1.3; +} +.proj-count { + font-size: 10px; + font-family: var(--font-mono); + color: var(--color-muted-foreground); + flex-shrink: 0; } -.session-subtitle { +.task-list { + padding-left: 14px; +} +.task-empty { font-size: 11px; color: var(--color-muted-foreground); + padding: 5px 8px; +} + +.task-row { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 8px; + margin-left: 6px; + border-left: 1px solid var(--color-border); + cursor: pointer; + transition: background 0.15s; + position: relative; +} +.task-row:hover { + background: var(--color-muted); +} +.task-row.active { + background: rgba(255, 132, 0, 0.08); + border-left-color: var(--color-primary); +} +.task-row.archived { + opacity: 0.55; +} + +.task-dot { + width: 6px; + height: 6px; + border-radius: 9999px; + flex-shrink: 0; + background: transparent; +} +.task-dot.unread { + background: var(--color-primary); +} +.task-pin { + color: var(--color-primary); + flex-shrink: 0; +} +.task-title { + flex: 1; + min-width: 0; + font-size: 12.5px; + color: var(--color-foreground); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - line-height: 1.3; +} +.task-time { + font-size: 10px; + font-family: var(--font-mono); + color: var(--color-muted-foreground); + flex-shrink: 0; } -.session-delete { - opacity: 0; +/* ─── Task action menu ─── */ +.task-menu { + position: relative; + display: inline-flex; flex-shrink: 0; - width: 24px; - height: 24px; +} +.task-menu-btn { + display: grid; + place-items: center; + width: 20px; + height: 20px; + border: none; + background: transparent; + border-radius: 5px; + color: var(--color-muted-foreground); + cursor: pointer; + opacity: 0; + transition: opacity 0.15s, background 0.15s, color 0.15s; +} +.task-row:hover .task-menu-btn { + opacity: 1; +} +.task-menu-btn:hover { + background: var(--color-secondary); + color: var(--color-foreground); +} +.task-menu-items { + position: absolute; + top: calc(100% + 4px); + right: 0; + min-width: 160px; + padding: 4px; + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + box-shadow: var(--shadow-md); + z-index: var(--z-dropdown); + outline: none; +} +.tmi { display: flex; align-items: center; - justify-content: center; - border-radius: 4px; + gap: 8px; + width: 100%; + padding: 7px 8px; border: none; background: transparent; - color: var(--color-muted-foreground); + border-radius: var(--radius-md); + color: var(--color-foreground); + font-size: 12.5px; + text-align: left; cursor: pointer; - transition: opacity 0.15s; +} +.tmi.hl { + background: var(--color-muted); +} +.tmi.danger { + color: var(--color-destructive); +} +.tmi-sep { + height: 1px; + margin: 4px 0; + background: var(--color-border); } -.session-item:hover .session-delete { - opacity: 1; +.pop-enter-active, +.pop-leave-active { + transition: opacity 0.12s ease, transform 0.12s ease; +} +.pop-enter-from, +.pop-leave-to { + opacity: 0; + transform: translateY(-4px); } +/* ─── Footer ─── */ .sidebar-footer { padding: 10px 12px; display: flex; align-items: center; justify-content: space-between; } - .footer-model { font-size: 11px; font-family: var(--font-mono); @@ -320,13 +545,11 @@ function formatDate(ts: string): string { text-overflow: ellipsis; max-width: 160px; } - .footer-actions { display: flex; align-items: center; gap: 2px; } - .footer-btn { width: 28px; height: 28px; @@ -340,7 +563,6 @@ function formatDate(ts: string): string { cursor: pointer; transition: color 0.15s; } - .footer-btn:hover { color: var(--color-foreground); } diff --git a/web/src/components/TopBar.vue b/web/src/components/TopBar.vue index 2ae0449..9eaf631 100644 --- a/web/src/components/TopBar.vue +++ b/web/src/components/TopBar.vue @@ -50,9 +50,13 @@ const emit = defineEmits<{ }>() const statusColor = computed(() => { - if (props.isRunning) return '#f59e0b' - if (props.wsConnected) return '#22c55e' - return '#9ca3af' + // Unified with the design tokens: orange = actively working (matches the + // Thinking dots), green = connected/idle, muted = offline. Uses the saturated + // --color-success (not the dark success-fg text token) so the dot reads as a + // bright "online" light in every theme. + if (props.isRunning) return 'var(--color-primary)' + if (props.wsConnected) return 'var(--color-success)' + return 'var(--color-muted-foreground)' }) const statusLabel = computed(() => { @@ -82,11 +86,18 @@ const panelButtons = [ { panel: 'terminal' as PanelType, icon: SquareTerminal, label: 'Terminal', shortcut: '⌘`' }, ] -// Branch name is not exposed to the web frontend. The backend computes it -// (internal/util/envinfo.go GitBranch via `git rev-parse --abbrev-ref HEAD`) -// but no /api endpoint returns it, so we render the chip without a branch -// label rather than fabricating one. See followups. -const branchName = computed<string | null>(() => null) +// Real branch name from /api/workspace (git rev-parse). Null when not a git +// repo or before the first fetch — the chip then renders without a label. +const branchName = ref<string | null>(null) + +async function loadWorkspace() { + try { + const ws = await api.workspace() + branchName.value = ws.branch || null + } catch { + branchName.value = null + } +} // Diff stats are fetched on demand from the real /api/diff endpoint (working // tree). We never fabricate numbers: if the fetch fails or returns nothing, @@ -120,11 +131,17 @@ function openChanges(close: () => void) { // Show the diff stat on first paint and refresh it whenever a run finishes (the // working tree likely changed), not only when the chip is clicked. -onMounted(loadDiffStat) +onMounted(() => { + loadDiffStat() + loadWorkspace() +}) watch( () => props.isRunning, (running, was) => { - if (was && !running) loadDiffStat() + if (was && !running) { + loadDiffStat() + loadWorkspace() + } }, ) </script> @@ -185,8 +202,8 @@ watch( <template v-if="diffStat"> <span class="chip-divider" /> <span class="chip-stat"> - <span class="stat-add text-emerald-600 dark:text-emerald-400">+{{ diffStat.additions }}</span> - <span class="stat-del text-red-500 dark:text-red-400">-{{ diffStat.deletions }}</span> + <span class="stat-add" style="color: var(--color-success-fg)">+{{ diffStat.additions }}</span> + <span class="stat-del" style="color: var(--color-error-fg)">-{{ diffStat.deletions }}</span> </span> </template> <span class="chip-divider" /> @@ -206,8 +223,8 @@ watch( <span class="ws-label">Changes</span> <span class="ws-right"> <span v-if="diffStat" class="chip-stat"> - <span class="stat-add text-emerald-600 dark:text-emerald-400">+{{ diffStat.additions }}</span> - <span class="stat-del text-red-500 dark:text-red-400">-{{ diffStat.deletions }}</span> + <span class="stat-add" style="color: var(--color-success-fg)">+{{ diffStat.additions }}</span> + <span class="stat-del" style="color: var(--color-error-fg)">-{{ diffStat.deletions }}</span> </span> <button class="ws-action" @click="openChanges(close)">Review</button> </span> @@ -251,8 +268,9 @@ watch( <style scoped> .topbar { height: 52px; - background: var(--color-sidebar-bg); - border-bottom: 1px solid var(--color-border); + /* No background/border of its own: flows into the chat area below as one + continuous surface (打通). */ + background: transparent; padding: 0 14px; display: flex; align-items: center; diff --git a/web/src/composables/api.ts b/web/src/composables/api.ts index 4536f98..7c57174 100644 --- a/web/src/composables/api.ts +++ b/web/src/composables/api.ts @@ -1,5 +1,5 @@ // API client for jcode backend -import type { ModelsResponse, AgentMode, ExecResponse, DiffResponse, MCPListResponse, MCPServerRequest, MCPLoginStatus, BrowseResponse, SSHListResponse, SkillInfo, SlashCommandInfo, TodoItem, Goal, SessionItem, SessionEntry, FileItem, SetupProvider, SetupModel, ProviderDetail, ModelStateResponse, ChatImage, AskUserAnswer, AskUserRequestData } from '@/types/api' +import type { ModelsResponse, AgentMode, ExecResponse, DiffResponse, WorkspaceInfo, TaskItem, TaskMetaPatch, MCPListResponse, MCPServerRequest, MCPLoginStatus, BrowseResponse, SSHListResponse, SkillInfo, SlashCommandInfo, TodoItem, Goal, SessionItem, SessionEntry, FileItem, SetupProvider, SetupModel, ProviderDetail, ModelStateResponse, ChatImage, AskUserAnswer, AskUserRequestData } from '@/types/api' const BASE = '' @@ -102,6 +102,13 @@ export const api = { const q = mode ? `?mode=${encodeURIComponent(mode)}` : '' return request<DiffResponse>(`/api/diff${q}`) }, + workspace: () => request<WorkspaceInfo>('/api/workspace'), + tasks: () => request<TaskItem[]>('/api/tasks'), + updateTask: (id: string, patch: TaskMetaPatch) => + request<TaskItem>(`/api/tasks/${encodeURIComponent(id)}`, { + method: 'PATCH', + body: JSON.stringify(patch), + }), mcpList: () => request<MCPListResponse>('/api/mcp'), mcpToggle: (name: string, enabled: boolean) => request<{ status: string }>(`/api/mcp/${encodeURIComponent(name)}/toggle`, { diff --git a/web/src/composables/notifications.ts b/web/src/composables/notifications.ts new file mode 100644 index 0000000..57c8770 --- /dev/null +++ b/web/src/composables/notifications.ts @@ -0,0 +1,39 @@ +// Browser notifications for long-running agent events. Fires only when the tab +// is in the background (document.hidden) so we never interrupt the user while +// they are watching the run. No-op when the Notification API is unavailable or +// permission was denied. +import { ref } from 'vue' + +const permission = ref<NotificationPermission>( + typeof Notification !== 'undefined' ? Notification.permission : 'denied', +) + +export function useNotifications() { + const supported = typeof Notification !== 'undefined' + + async function ensurePermission() { + if (!supported || permission.value !== 'default') return + try { + permission.value = await Notification.requestPermission() + } catch { + /* ignore */ + } + } + + function notify(title: string, body?: string) { + if (!supported || permission.value !== 'granted') return + // Only notify when the user isn't already looking at the page. + if (typeof document !== 'undefined' && !document.hidden) return + try { + const n = new Notification(title, { body, icon: '/icon.svg', tag: 'jcode' }) + n.onclick = () => { + window.focus() + n.close() + } + } catch { + /* ignore */ + } + } + + return { supported, permission, ensurePermission, notify } +} diff --git a/web/src/stores/project.ts b/web/src/stores/project.ts index abfd458..2773372 100644 --- a/web/src/stores/project.ts +++ b/web/src/stores/project.ts @@ -1,7 +1,7 @@ // Project management store using localStorage import { defineStore } from 'pinia' import { ref, computed } from 'vue' -import type { Project } from '@/types/api' +import type { Project, TaskItem, TaskMetaPatch } from '@/types/api' import { api } from '@/composables/api' const STORAGE_KEY = 'jcode_projects' @@ -99,6 +99,61 @@ export const useProjectStore = defineStore('project', () => { return p.path.split('/').filter(Boolean).pop() || p.path } + function nameForPath(path: string): string { + return path.split('/').filter(Boolean).pop() || path + } + + // ─── Cross-project tasks (for the sidebar tree) ─── + const allTasks = ref<TaskItem[]>([]) + + async function fetchAllTasks() { + try { + allTasks.value = await api.tasks() + } catch { + allTasks.value = [] + } + } + + // Tasks grouped by project path, sorted newest-first, pinned on top. + const tasksByProject = computed(() => { + const map: Record<string, TaskItem[]> = {} + for (const t of allTasks.value) { + ;(map[t.project] ??= []).push(t) + } + for (const path in map) { + const list = map[path] + if (!list) continue + list.sort((a, b) => { + if (a.pinned !== b.pinned) return a.pinned ? -1 : 1 + return (b.created_at || '').localeCompare(a.created_at || '') + }) + } + return map + }) + + // The project nodes to render: known (localStorage) projects unioned with any + // project path that has tasks, so nothing is hidden just because it wasn't + // explicitly opened. + const projectsForTree = computed(() => { + const paths = new Set<string>(projects.value.map((p) => p.path)) + for (const t of allTasks.value) paths.add(t.project) + return [...paths].map((path) => { + const known = projects.value.find((p) => p.path === path) + return { id: known?.id ?? '', path, name: nameForPath(path) } + }) + }) + + async function updateTaskMeta(uuid: string, patch: TaskMetaPatch) { + // Optimistic local update, then persist. + const t = allTasks.value.find((x) => x.uuid === uuid) + if (t) Object.assign(t, patch) + try { + await api.updateTask(uuid, patch) + } catch { + await fetchAllTasks() // resync on failure + } + } + return { projects, activeId, @@ -112,5 +167,11 @@ export const useProjectStore = defineStore('project', () => { openProject, ensureCurrentProject, projectName, + nameForPath, + allTasks, + fetchAllTasks, + tasksByProject, + projectsForTree, + updateTaskMeta, } }) diff --git a/web/src/styles/tokens.css b/web/src/styles/tokens.css index 996e335..c7d271f 100644 --- a/web/src/styles/tokens.css +++ b/web/src/styles/tokens.css @@ -21,6 +21,12 @@ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.12); --shadow-lg: 0 4px 16px rgba(0, 0, 0, 0.15); + /* ─── Status indicators ─── */ + /* Saturated "online" green for small status dots. Distinct from + * --color-success-fg (a dark text token tuned for the success chip), which + * reads near-black on a tiny dot in light mode. */ + --color-success: #16A34A; + /* ─── Z-index scale ─── */ --z-sidebar: 30; --z-header: 20; @@ -100,6 +106,7 @@ --color-info-fg: #B2B2FF; --color-destructive: #FF5C33; --color-sidebar-bg: #1A1A1A; + --color-success: #22C55E; /* ─── Syntax highlighting (dark) ─── */ --hljs-fg: #e4e4e7; diff --git a/web/src/types/api.ts b/web/src/types/api.ts index 60382b2..a48e5ac 100644 --- a/web/src/types/api.ts +++ b/web/src/types/api.ts @@ -138,6 +138,32 @@ export interface DiffResponse { entries: DiffEntry[] } +export interface WorkspaceInfo { + branch: string // empty if not a git repo + dirty: boolean +} + +// A task = a conversation, listed across all projects for the sidebar tree. +export interface TaskItem { + uuid: string + project: string // project path + created_at: string + provider: string + model: string + title?: string + pinned: boolean + archived: boolean + unread: boolean + status?: string +} + +export interface TaskMetaPatch { + pinned?: boolean + archived?: boolean + unread?: boolean + title?: string +} + // MCP types export interface MCPServerInfo { name: string From 828f7feb0b354ac750f74bc3274ae52407dea3f2 Mon Sep 17 00:00:00 2001 From: jack <admin@nightc.com> Date: Tue, 16 Jun 2026 19:04:09 +0800 Subject: [PATCH 2/5] fix(web): cross-project task delete + request-scoped git context - DeleteSessionByUUID resolves the owning project across all projects so a task deleted from the sidebar tree that is NOT in the active project is actually removed from the index (previously a silent no-op left a ghost entry while the JSONL was still deleted). Only removes the file when the uuid exists in the index, which also prevents a crafted uuid from deleting an arbitrary file. Adds TestDeleteTaskCrossProject. (found by self-review) - handleWorkspace now uses r.Context() instead of s.ctx so the git commands are cancelled when the client disconnects. (CodeRabbit review feedback on #82) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --- internal/session/session.go | 58 +++++++++++++++++++++++++++++++++++++ internal/web/server.go | 10 +++++-- internal/web/tasks_test.go | 30 +++++++++++++++++++ 3 files changed, 95 insertions(+), 3 deletions(-) diff --git a/internal/session/session.go b/internal/session/session.go index 50674e3..634f1c2 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -664,6 +664,64 @@ func DeleteSession(project, uuid string) error { return nil } +// DeleteSessionByUUID removes a session (index entry + JSONL file) located by +// uuid across ALL projects. The web task tree can delete a task that does not +// belong to the active project, so we must not assume a single project key. +// Returns false if no session with that uuid exists. The JSONL file is only +// removed when the uuid was actually found in the index, which also prevents a +// crafted uuid from deleting an arbitrary file. +func DeleteSessionByUUID(uuid string) (bool, error) { + indexPath, err := config.SessionsIndexPath() + if err != nil { + return false, err + } + data, err := os.ReadFile(indexPath) + if err != nil { + if os.IsNotExist(err) { + return false, nil + } + return false, err + } + var idx sessionIndex + if err := json.Unmarshal(data, &idx); err != nil { + return false, err + } + found := false + for project, metas := range idx.Sessions { + filtered := make([]SessionMeta, 0, len(metas)) + for _, m := range metas { + if m.UUID == uuid { + found = true + continue + } + filtered = append(filtered, m) + } + idx.Sessions[project] = filtered + } + if !found { + return false, nil + } + newData, err := json.MarshalIndent(&idx, "", " ") + if err != nil { + return true, err + } + tmpPath := indexPath + ".tmp" + if err := os.WriteFile(tmpPath, newData, 0644); err != nil { + return true, err + } + if err := os.Rename(tmpPath, indexPath); err != nil { + return true, err + } + dir, err := config.SessionsDir() + if err != nil { + return true, err + } + if rmErr := os.Remove(filepath.Join(dir, uuid+".json")); rmErr != nil && !os.IsNotExist(rmErr) { + return true, fmt.Errorf("delete session file: %w", rmErr) + } + return true, nil +} + func removeFromIndex(project, uuid string) error { indexPath, err := config.SessionsIndexPath() if err != nil { diff --git a/internal/web/server.go b/internal/web/server.go index 75d41d8..ebd57f8 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -448,12 +448,14 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { // the current project so the web UI can show the real branch name. Diff stats // are fetched separately via /api/diff. Empty branch = not a git repo. func (s *Server) handleWorkspace(w http.ResponseWriter, r *http.Request) { - branchCmd := exec.CommandContext(s.ctx, "git", "rev-parse", "--abbrev-ref", "HEAD") + // Use the request context so the git commands are cancelled if the client + // disconnects (CodeRabbit review feedback on PR #82). + branchCmd := exec.CommandContext(r.Context(), "git", "rev-parse", "--abbrev-ref", "HEAD") branchCmd.Dir = s.pwd branchOut, _ := branchCmd.Output() branch := strings.TrimSpace(string(branchOut)) - statusCmd := exec.CommandContext(s.ctx, "git", "status", "--porcelain") + statusCmd := exec.CommandContext(r.Context(), "git", "status", "--porcelain") statusCmd.Dir = s.pwd statusOut, _ := statusCmd.Output() dirty := strings.TrimSpace(string(statusOut)) != "" @@ -794,7 +796,9 @@ func (s *Server) handleDeleteSession(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "session id is required"}) return } - if err := session.DeleteSession(s.pwd, id); err != nil { + // Resolve the owning project across all projects: a task deleted from the + // sidebar tree may not belong to the active project (s.pwd). + if _, err := session.DeleteSessionByUUID(id); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } diff --git a/internal/web/tasks_test.go b/internal/web/tasks_test.go index d816a65..6d2a19a 100644 --- a/internal/web/tasks_test.go +++ b/internal/web/tasks_test.go @@ -153,3 +153,33 @@ func TestUpdateTaskMeta(t *testing.T) { t.Fatalf("unknown id should be 404, got %d", rec.Code) } } + +// Regression: deleting a task that belongs to a project OTHER than the active +// one (s.pwd) must still remove it from the index — the sidebar tree can delete +// across projects. Previously this silently no-op'd, leaving a ghost task. +func TestDeleteTaskCrossProject(t *testing.T) { + seedIndex(t, map[string][]session.SessionMeta{ + "/work/active": {{UUID: "act-1", Project: "/work/active"}}, + "/work/other": {{UUID: "oth-1", Project: "/work/other"}, {UUID: "oth-2", Project: "/work/other"}}, + }) + // Active project is /work/active; delete a task in /work/other. + s := &Server{pwd: "/work/active"} + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodDelete, "/api/sessions/oth-1", nil) + req.SetPathValue("id", "oth-1") + s.handleDeleteSession(rec, req) + if rec.Code != http.StatusOK { + t.Fatalf("delete: code=%d body=%q", rec.Code, rec.Body.String()) + } + + all, err := session.ListAllSessions() + if err != nil { + t.Fatal(err) + } + if len(all["/work/other"]) != 1 || all["/work/other"][0].UUID != "oth-2" { + t.Fatalf("oth-1 should be deleted and oth-2 kept, got %+v", all["/work/other"]) + } + if len(all["/work/active"]) != 1 { + t.Fatalf("active project tasks should be untouched, got %+v", all["/work/active"]) + } +} From a9da92fb841defb11a97dcc7b9f11effd931e3b3 Mon Sep 17 00:00:00 2001 From: jack <admin@nightc.com> Date: Sun, 21 Jun 2026 00:47:19 +0800 Subject: [PATCH 3/5] feat(desktop): native Tauri app reusing the web UI + SSH remote + UI polish Add a Tauri 2 desktop app under desktop/ that embeds the jcode Go binary as a sidecar and loads the existing Vue web UI over loopback, with native notifications, menu-bar tray, single-instance focus, window-state memory, and a global toggle shortcut. The same bundle degrades to a plain browser via the feature-detected useDesktop.ts bridge. Includes the supporting work this branch accumulated: - Web SSH remote-connect wizard (internal/remote + /api/remote/*, ssh:// sessions) - Request-scoped web-server origin gate (internal/web/server.go) closing the loopback RCE surface; cors_test.go coverage - Inline workspace/branch pickers, unified borderless titlebar chrome, Codex-style composer, hidden-but-scrollable scrollbars, non-selectable app chrome - Phantom-empty-session fix (gate todo/goal OnUpdate on HasRecording) Docs: docs/desktop.md plus updates across the docs set. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --- Makefile | 35 +- README.md | 11 + desktop/.gitignore | 2 + desktop/package.json | 16 + desktop/pnpm-lock.yaml | 140 + desktop/pnpm-workspace.yaml | 13 + desktop/splash/index.html | 84 + desktop/src-tauri/.gitignore | 8 + desktop/src-tauri/Cargo.lock | 5380 +++++++++++++++++ desktop/src-tauri/Cargo.toml | 36 + desktop/src-tauri/build.rs | 3 + desktop/src-tauri/capabilities/default.json | 24 + desktop/src-tauri/icons/128x128.png | Bin 0 -> 2565 bytes desktop/src-tauri/icons/128x128@2x.png | Bin 0 -> 5412 bytes desktop/src-tauri/icons/32x32.png | Bin 0 -> 776 bytes desktop/src-tauri/icons/64x64.png | Bin 0 -> 1391 bytes desktop/src-tauri/icons/Square107x107Logo.png | Bin 0 -> 2325 bytes desktop/src-tauri/icons/Square142x142Logo.png | Bin 0 -> 2878 bytes desktop/src-tauri/icons/Square150x150Logo.png | Bin 0 -> 3028 bytes desktop/src-tauri/icons/Square284x284Logo.png | Bin 0 -> 6249 bytes desktop/src-tauri/icons/Square30x30Logo.png | Bin 0 -> 726 bytes desktop/src-tauri/icons/Square310x310Logo.png | Bin 0 -> 6654 bytes desktop/src-tauri/icons/Square44x44Logo.png | Bin 0 -> 1062 bytes desktop/src-tauri/icons/Square71x71Logo.png | Bin 0 -> 1464 bytes desktop/src-tauri/icons/Square89x89Logo.png | Bin 0 -> 1853 bytes desktop/src-tauri/icons/StoreLogo.png | Bin 0 -> 1131 bytes .../android/mipmap-anydpi-v26/ic_launcher.xml | 5 + .../icons/android/mipmap-hdpi/ic_launcher.png | Bin 0 -> 2056 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 0 -> 3300 bytes .../android/mipmap-hdpi/ic_launcher_round.png | Bin 0 -> 1817 bytes .../icons/android/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2072 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 0 -> 2219 bytes .../android/mipmap-mdpi/ic_launcher_round.png | Bin 0 -> 1818 bytes .../android/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 3853 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 0 -> 4391 bytes .../mipmap-xhdpi/ic_launcher_round.png | Bin 0 -> 3305 bytes .../android/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 5592 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 0 -> 7072 bytes .../mipmap-xxhdpi/ic_launcher_round.png | Bin 0 -> 4521 bytes .../android/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 7505 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 0 -> 9917 bytes .../mipmap-xxxhdpi/ic_launcher_round.png | Bin 0 -> 6135 bytes .../android/values/ic_launcher_background.xml | 4 + desktop/src-tauri/icons/icon.icns | Bin 0 -> 70302 bytes desktop/src-tauri/icons/icon.ico | Bin 0 -> 13041 bytes desktop/src-tauri/icons/icon.png | Bin 0 -> 11784 bytes .../src-tauri/icons/ios/AppIcon-20x20@1x.png | Bin 0 -> 514 bytes .../icons/ios/AppIcon-20x20@2x-1.png | Bin 0 -> 943 bytes .../src-tauri/icons/ios/AppIcon-20x20@2x.png | Bin 0 -> 943 bytes .../src-tauri/icons/ios/AppIcon-20x20@3x.png | Bin 0 -> 1290 bytes .../src-tauri/icons/ios/AppIcon-29x29@1x.png | Bin 0 -> 690 bytes .../icons/ios/AppIcon-29x29@2x-1.png | Bin 0 -> 1343 bytes .../src-tauri/icons/ios/AppIcon-29x29@2x.png | Bin 0 -> 1343 bytes .../src-tauri/icons/ios/AppIcon-29x29@3x.png | Bin 0 -> 1775 bytes .../src-tauri/icons/ios/AppIcon-40x40@1x.png | Bin 0 -> 943 bytes .../icons/ios/AppIcon-40x40@2x-1.png | Bin 0 -> 1639 bytes .../src-tauri/icons/ios/AppIcon-40x40@2x.png | Bin 0 -> 1639 bytes .../src-tauri/icons/ios/AppIcon-40x40@3x.png | Bin 0 -> 2386 bytes .../src-tauri/icons/ios/AppIcon-512@2x.png | Bin 0 -> 26898 bytes .../src-tauri/icons/ios/AppIcon-60x60@2x.png | Bin 0 -> 2386 bytes .../src-tauri/icons/ios/AppIcon-60x60@3x.png | Bin 0 -> 3592 bytes .../src-tauri/icons/ios/AppIcon-76x76@1x.png | Bin 0 -> 1617 bytes .../src-tauri/icons/ios/AppIcon-76x76@2x.png | Bin 0 -> 3038 bytes .../icons/ios/AppIcon-83.5x83.5@2x.png | Bin 0 -> 3415 bytes desktop/src-tauri/src/main.rs | 164 + desktop/src-tauri/src/sidecar.rs | 145 + desktop/src-tauri/src/tray.rs | 51 + desktop/src-tauri/tauri.conf.json | 48 + docs/commands.md | 2 +- docs/configuration.md | 2 +- docs/desktop.md | 154 + docs/goal.md | 2 +- docs/themes.md | 2 +- docs/web-interface.md | 2 + internal/command/ssh.go | 55 +- internal/command/web.go | 51 + internal/remote/ssh.go | 182 + internal/remote/ssh_test.go | 106 + internal/session/index_test.go | 41 + internal/tools/env.go | 6 + internal/web/cors_test.go | 43 + internal/web/git.go | 108 + internal/web/remote.go | 314 + internal/web/server.go | 84 +- web/package.json | 4 + web/pnpm-lock.yaml | 69 + web/src/App.vue | 404 +- web/src/components/BranchPicker.vue | 392 ++ web/src/components/ChatInput.vue | 301 +- web/src/components/ChatMessage.vue | 14 +- web/src/components/CommandPalette.vue | 2 +- web/src/components/DiffViewer.vue | 4 +- web/src/components/FileTreePanel.vue | 4 +- web/src/components/FileViewer.vue | 88 +- web/src/components/GoalBanner.vue | 5 +- web/src/components/MCPPanel.vue | 179 +- web/src/components/ProjectSwitcher.vue | 133 +- web/src/components/RemoteConnectWizard.vue | 828 +++ web/src/components/RightPanel.vue | 8 +- web/src/components/SettingsDialog.vue | 147 +- web/src/components/SetupView.vue | 6 +- web/src/components/Sidebar.vue | 122 +- web/src/components/TaskList.vue | 2 +- web/src/components/TerminalInstance.vue | 2 +- web/src/components/TerminalPanel.vue | 6 +- web/src/components/ToolCallCard.vue | 44 +- web/src/components/TopBar.vue | 512 +- web/src/components/WorkspacePicker.vue | 501 ++ web/src/composables/api.ts | 35 +- web/src/composables/notifications.ts | 31 +- web/src/composables/useBranch.ts | 66 + web/src/composables/useDesktop.ts | 117 + web/src/composables/useFolderBrowser.ts | 68 + web/src/main.ts | 4 + web/src/stores/chat.ts | 13 + web/src/stores/project.ts | 56 +- web/src/style.css | 117 +- web/src/styles/animations.css | 4 +- web/src/styles/tokens.css | 39 +- web/src/types/api.ts | 54 + 120 files changed, 10713 insertions(+), 991 deletions(-) create mode 100644 desktop/.gitignore create mode 100644 desktop/package.json create mode 100644 desktop/pnpm-lock.yaml create mode 100644 desktop/pnpm-workspace.yaml create mode 100644 desktop/splash/index.html create mode 100644 desktop/src-tauri/.gitignore create mode 100644 desktop/src-tauri/Cargo.lock create mode 100644 desktop/src-tauri/Cargo.toml create mode 100644 desktop/src-tauri/build.rs create mode 100644 desktop/src-tauri/capabilities/default.json create mode 100644 desktop/src-tauri/icons/128x128.png create mode 100644 desktop/src-tauri/icons/128x128@2x.png create mode 100644 desktop/src-tauri/icons/32x32.png create mode 100644 desktop/src-tauri/icons/64x64.png create mode 100644 desktop/src-tauri/icons/Square107x107Logo.png create mode 100644 desktop/src-tauri/icons/Square142x142Logo.png create mode 100644 desktop/src-tauri/icons/Square150x150Logo.png create mode 100644 desktop/src-tauri/icons/Square284x284Logo.png create mode 100644 desktop/src-tauri/icons/Square30x30Logo.png create mode 100644 desktop/src-tauri/icons/Square310x310Logo.png create mode 100644 desktop/src-tauri/icons/Square44x44Logo.png create mode 100644 desktop/src-tauri/icons/Square71x71Logo.png create mode 100644 desktop/src-tauri/icons/Square89x89Logo.png create mode 100644 desktop/src-tauri/icons/StoreLogo.png create mode 100644 desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png create mode 100644 desktop/src-tauri/icons/android/values/ic_launcher_background.xml create mode 100644 desktop/src-tauri/icons/icon.icns create mode 100644 desktop/src-tauri/icons/icon.ico create mode 100644 desktop/src-tauri/icons/icon.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@2x-1.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-20x20@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@2x-1.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@2x-1.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-40x40@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-512@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-76x76@2x.png create mode 100644 desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png create mode 100644 desktop/src-tauri/src/main.rs create mode 100644 desktop/src-tauri/src/sidecar.rs create mode 100644 desktop/src-tauri/src/tray.rs create mode 100644 desktop/src-tauri/tauri.conf.json create mode 100644 docs/desktop.md create mode 100644 internal/remote/ssh.go create mode 100644 internal/remote/ssh_test.go create mode 100644 internal/session/index_test.go create mode 100644 internal/web/cors_test.go create mode 100644 internal/web/git.go create mode 100644 internal/web/remote.go create mode 100644 web/src/components/BranchPicker.vue create mode 100644 web/src/components/RemoteConnectWizard.vue create mode 100644 web/src/components/WorkspacePicker.vue create mode 100644 web/src/composables/useBranch.ts create mode 100644 web/src/composables/useDesktop.ts create mode 100644 web/src/composables/useFolderBrowser.ts diff --git a/Makefile b/Makefile index 4be3270..bca8dab 100644 --- a/Makefile +++ b/Makefile @@ -13,7 +13,7 @@ LDFLAGS := -s -w \ export GOFLAGS := -buildvcs=false -.PHONY: build build-binary run doctor version install clean build-web fmt lint lint-go lint-web generate setup-hooks +.PHONY: build build-binary run doctor version install clean build-web fmt lint lint-go lint-web generate setup-hooks desktop-icons desktop-sidecar desktop-dev desktop-build desktop-clean fmt: @echo "Formatting Go..." @@ -65,3 +65,36 @@ clean: setup-hooks: @git config core.hooksPath .githooks @echo "Git hooks installed (core.hooksPath = .githooks)" + +# ─── Desktop app (Tauri) ─── +# The desktop app embeds the same jcode binary as a sidecar: Tauri renders the +# UI and provides native system integration, while the Go server (with the web +# UI baked in) runs on a loopback port. See docs/desktop.md. +DESKTOP_DIR := desktop +SIDECAR_DIR := $(DESKTOP_DIR)/src-tauri/binaries +RUST_TARGET := $(shell rustc -vV 2>/dev/null | sed -n 's/^host: //p') +# Tauri's externalBin resolver requires the OS executable suffix, so Windows +# sidecars must be jcode-<triple>.exe. +SIDECAR_EXE := $(if $(findstring windows,$(RUST_TARGET)),.exe,) + +# Regenerate the app icon set from the brand mark. +desktop-icons: + cd $(DESKTOP_DIR) && npx --yes @tauri-apps/cli@2 icon ../web/public/icon.svg -o src-tauri/icons + +# Build the sidecar binary (frontend embedded) named for the host target triple, +# which is what Tauri's externalBin resolver expects. +desktop-sidecar: generate build-web + @echo "Building jcode sidecar for $(RUST_TARGET)..." + @mkdir -p $(SIDECAR_DIR) + go build -ldflags "$(LDFLAGS)" -o $(SIDECAR_DIR)/jcode-$(RUST_TARGET)$(SIDECAR_EXE) $(PKG) + +# Run the desktop app in development (hot window; rebuilds the sidecar first). +desktop-dev: desktop-sidecar + cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && pnpm tauri dev + +# Produce a distributable bundle (.app/.dmg on macOS, .msi on Windows, etc.). +desktop-build: desktop-sidecar + cd $(DESKTOP_DIR) && (pnpm install 2>/dev/null || npm install) && pnpm tauri build + +desktop-clean: + rm -rf $(SIDECAR_DIR) $(DESKTOP_DIR)/src-tauri/target diff --git a/README.md b/README.md index 50a64a3..d7a0a5d 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,17 @@ Start a browser-based UI with `jcode web`. Chat interface, file browser, built-i <img src="docs/asset/web-screenshot.png" alt="jcode Web UI" width="800"> </p> +### 🖥 Desktop App + +A native desktop app (built with [Tauri](https://tauri.app)) wraps the same web UI in a real OS window with native integration: OS notifications, a menu-bar tray, close-to-tray, single-instance focus, window-state memory, and a native folder picker. The Go backend runs as an embedded sidecar — no separate server to start. + +```bash +make desktop-dev # run the app in development +make desktop-build # build a distributable bundle (.app/.dmg/.msi) +``` + +See the [Desktop App guide](https://cnjack.github.io/jcode/desktop) for the architecture and details. + ### 🧭 Context Awareness At startup the agent automatically detects: diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 0000000..f06235c --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/desktop/package.json b/desktop/package.json new file mode 100644 index 0000000..950e7d6 --- /dev/null +++ b/desktop/package.json @@ -0,0 +1,16 @@ +{ + "name": "jcode-desktop", + "private": true, + "version": "0.5.3", + "type": "module", + "description": "Tauri desktop shell for jcode. The real UI is served by the embedded Go sidecar; this package only drives the Tauri CLI.", + "scripts": { + "tauri": "tauri", + "dev": "tauri dev", + "build": "tauri build", + "icon": "tauri icon ../../web/public/icon.svg -o src-tauri/icons" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.11.3" + } +} diff --git a/desktop/pnpm-lock.yaml b/desktop/pnpm-lock.yaml new file mode 100644 index 0000000..ed89ddf --- /dev/null +++ b/desktop/pnpm-lock.yaml @@ -0,0 +1,140 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@tauri-apps/cli': + specifier: ^2.11.3 + version: 2.11.3 + +packages: + + '@tauri-apps/cli-darwin-arm64@2.11.3': + resolution: {integrity: sha512-BxpaM8bsCoXs3wd4WKYhas/G1gs7+r7B+e4WnyRk2GEoVOouJB1hoL6E6YLXZDXbYci6VFdrNnobQwd2uVL4ew==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tauri-apps/cli-darwin-x64@2.11.3': + resolution: {integrity: sha512-DbZYuPB1ZEzcAHYeyCvo3ltzM27+aXwPloCrtexPnmgPgulYJm3TOq6aC4S+wPhSXteddg8zImtNkvx/gQzmwg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.3': + resolution: {integrity: sha512-741NduqBmz1XkdU8yz3OI/kBZtqHbvxo9F9ytIeWYU69/Ba9dcZEbqOU++Dp0G/XU8vAI0TfTywEl+p+BbLvaA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tauri-apps/cli-linux-arm64-gnu@2.11.3': + resolution: {integrity: sha512-RWAXT8pTqIczXcoic+LXlo6uEbAXGB0cgh6Pg7Y9xVnEbzryQ1JHtRGj9SxzrKSemBIDBH6Qc24kK2G69i8ofA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-arm64-musl@2.11.3': + resolution: {integrity: sha512-qomqYS+yAkd0gXMRmhguWXc7RfVN+XKKXaEwbf5QmKURwydLFOTldd6F8/WoZDSsBMrV8dpNxz0YneGLmobiSA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.3': + resolution: {integrity: sha512-jOCXbDqeDj5XcclsOBAaXjtTgwZCVg8zEZ+dbPUCoADOgljFgL0rOkYTc96vUYgOrYEfuHYihWMxIDGaD6GwJw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-gnu@2.11.3': + resolution: {integrity: sha512-+u3HO/F3gHwL48t9gWN/urqZvpaEJzBFmTaq5eSIhvy8TOvnhb+LgJr3Q3BG+5JxuBrCUjqtOEz6gMttdJFSBA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tauri-apps/cli-linux-x64-musl@2.11.3': + resolution: {integrity: sha512-spr5Jpr6KF/vehkLwJ0YmdGv8QwpWU+uw7J8bgijO0sox6ZCYsSNMbcsQjTqPi4xl+p0woIYpWXgChgHYpAc8g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tauri-apps/cli-win32-arm64-msvc@2.11.3': + resolution: {integrity: sha512-abkoRQih5xBa3vz2spWaex0kP/MzVzVPQHom2f8jnCq46R/luOD6Uy85EMU9/bfzf6ZzdorWJsgO+OMX90Fx2w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tauri-apps/cli-win32-ia32-msvc@2.11.3': + resolution: {integrity: sha512-Vy6AvzFm1G40hg3r+OYDB3jkuu7R4wnMzbQBKuun9v6Cgg8IierpLL7toMzrZKs/8NlG8Sg4x1iLFR52oknyHg==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@tauri-apps/cli-win32-x64-msvc@2.11.3': + resolution: {integrity: sha512-GlciF75GdbseajOyib2aCHwE3BXIqZ1liGKWLFRvCdN5wm8h8hFssEVKQ/6E+2jsMLg9v7LCTb983YFnn0QSww==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tauri-apps/cli@2.11.3': + resolution: {integrity: sha512-EElQe8z8uD7Pi5++tJ/UfEwWuK08rd3oCDYdeIbJAb6pZRrxlqmoF5gh5H5YvzmUPhS4IRCaLSsQhvWkrfK+GQ==} + engines: {node: '>= 10'} + hasBin: true + +snapshots: + + '@tauri-apps/cli-darwin-arm64@2.11.3': + optional: true + + '@tauri-apps/cli-darwin-x64@2.11.3': + optional: true + + '@tauri-apps/cli-linux-arm-gnueabihf@2.11.3': + optional: true + + '@tauri-apps/cli-linux-arm64-gnu@2.11.3': + optional: true + + '@tauri-apps/cli-linux-arm64-musl@2.11.3': + optional: true + + '@tauri-apps/cli-linux-riscv64-gnu@2.11.3': + optional: true + + '@tauri-apps/cli-linux-x64-gnu@2.11.3': + optional: true + + '@tauri-apps/cli-linux-x64-musl@2.11.3': + optional: true + + '@tauri-apps/cli-win32-arm64-msvc@2.11.3': + optional: true + + '@tauri-apps/cli-win32-ia32-msvc@2.11.3': + optional: true + + '@tauri-apps/cli-win32-x64-msvc@2.11.3': + optional: true + + '@tauri-apps/cli@2.11.3': + optionalDependencies: + '@tauri-apps/cli-darwin-arm64': 2.11.3 + '@tauri-apps/cli-darwin-x64': 2.11.3 + '@tauri-apps/cli-linux-arm-gnueabihf': 2.11.3 + '@tauri-apps/cli-linux-arm64-gnu': 2.11.3 + '@tauri-apps/cli-linux-arm64-musl': 2.11.3 + '@tauri-apps/cli-linux-riscv64-gnu': 2.11.3 + '@tauri-apps/cli-linux-x64-gnu': 2.11.3 + '@tauri-apps/cli-linux-x64-musl': 2.11.3 + '@tauri-apps/cli-win32-arm64-msvc': 2.11.3 + '@tauri-apps/cli-win32-ia32-msvc': 2.11.3 + '@tauri-apps/cli-win32-x64-msvc': 2.11.3 diff --git a/desktop/pnpm-workspace.yaml b/desktop/pnpm-workspace.yaml new file mode 100644 index 0000000..dd8f46e --- /dev/null +++ b/desktop/pnpm-workspace.yaml @@ -0,0 +1,13 @@ +minimumReleaseAgeExclude: + - '@tauri-apps/cli-darwin-arm64@2.11.3' + - '@tauri-apps/cli-darwin-x64@2.11.3' + - '@tauri-apps/cli-linux-arm-gnueabihf@2.11.3' + - '@tauri-apps/cli-linux-arm64-gnu@2.11.3' + - '@tauri-apps/cli-linux-arm64-musl@2.11.3' + - '@tauri-apps/cli-linux-riscv64-gnu@2.11.3' + - '@tauri-apps/cli-linux-x64-gnu@2.11.3' + - '@tauri-apps/cli-linux-x64-musl@2.11.3' + - '@tauri-apps/cli-win32-arm64-msvc@2.11.3' + - '@tauri-apps/cli-win32-ia32-msvc@2.11.3' + - '@tauri-apps/cli-win32-x64-msvc@2.11.3' + - '@tauri-apps/cli@2.11.3' diff --git a/desktop/splash/index.html b/desktop/splash/index.html new file mode 100644 index 0000000..235058f --- /dev/null +++ b/desktop/splash/index.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<html lang="zh-CN"> + <head> + <meta charset="UTF-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> + <title>jcode + + + +
+ +
jcode
+
正在启动本地服务…
+
+ + diff --git a/desktop/src-tauri/.gitignore b/desktop/src-tauri/.gitignore new file mode 100644 index 0000000..bf8d3fb --- /dev/null +++ b/desktop/src-tauri/.gitignore @@ -0,0 +1,8 @@ +# Rust / Cargo build output +/target + +# Bundled sidecar binaries (built from the Go source by `make desktop-sidecar`) +/binaries + +# Tauri-generated schemas and platform projects +/gen/schemas diff --git a/desktop/src-tauri/Cargo.lock b/desktop/src-tauri/Cargo.lock new file mode 100644 index 0000000..e6fd6c8 --- /dev/null +++ b/desktop/src-tauri/Cargo.lock @@ -0,0 +1,5380 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "atk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b" +dependencies = [ + "atk-sys", + "glib", + "libc", +] + +[[package]] +name = "atk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc91aac060a7a1e25823bdccbfb6af1875b88f17c6daac97894eed8207166b3" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae3f5d315924270530207e2a68396c3cc547f6dca3fbdca317cfb1a51edb593" +dependencies = [ + "serde", +] + +[[package]] +name = "cairo-rs" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2" +dependencies = [ + "bitflags 2.13.0", + "cairo-sys-rs", + "glib", + "libc", + "once_cell", + "thiserror 1.0.69", +] + +[[package]] +name = "cairo-sys-rs" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "camino" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce8d3bd5823c7504d3f579f13e7b2f3da252fcb938c594d5680ee508bf846f" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "cargo_toml" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374b7c592d9c00c1f4972ea58390ac6b18cbb6ab79011f3bdc90a0b82ca06b77" +dependencies = [ + "serde", + "toml 0.9.12+spec-1.1.0", +] + +[[package]] +name = "cc" +version = "1.2.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38f2da7a0a2c4ccf0065be06397cc26a81f4e528be095826eee9d4adbb8c60f" +dependencies = [ + "byteorder", + "fnv", + "uuid", +] + +[[package]] +name = "cfg-expr" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" +dependencies = [ + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cssparser" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dae61cf9c0abb83bd659dab65b7e4e38d8236824c85f0f804f173567bda257d2" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "ctor" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "352d39c2f7bef1d6ad73db6f5160efcaed66d94ef8c6c573a8410c00bf909a98" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.118", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dbus" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b942602992bb7acfd1f51c49811c58a610ef9181b6e66f3e519d79b540a3bf73" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "serde_core", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.118", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "dom_query" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521e380c0c8afb8d9a1e83a1822ee03556fc3e3e7dbc1fd30be14e37f9cb3f89" +dependencies = [ + "bit-set", + "cssparser", + "foldhash", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" +dependencies = [ + "serde", +] + +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + +[[package]] +name = "dtor" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1057d6c64987086ff8ed0fd3fbf377a6b7d205cc7715868cd401705f715cbe4" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "embed-resource" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31a88c8d26de40ed18fe748c547845aa39de1db3afd958f8cb91579f3644bcb" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml 1.1.2+spec-1.1.0", + "vswhom", + "winreg", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "erased-serde" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2add8a07dd6a8d93ff627029c51de145e12686fbc36ecb298ac22e74cf02dec" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset", + "rustc_version", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "gdk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691" +dependencies = [ + "cairo-rs", + "gdk-pixbuf", + "gdk-sys", + "gio", + "glib", + "libc", + "pango", +] + +[[package]] +name = "gdk-pixbuf" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec" +dependencies = [ + "gdk-pixbuf-sys", + "gio", + "glib", + "libc", + "once_cell", +] + +[[package]] +name = "gdk-pixbuf-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gdk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7" +dependencies = [ + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkwayland-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "140071d506d223f7572b9f09b5e155afbd77428cd5cc7af8f2694c41d98dfe69" +dependencies = [ + "gdk-sys", + "glib-sys", + "gobject-sys", + "libc", + "pkg-config", + "system-deps", +] + +[[package]] +name = "gdkx11" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3caa00e14351bebbc8183b3c36690327eb77c49abc2268dd4bd36b856db3fbfe" +dependencies = [ + "gdk", + "gdkx11-sys", + "gio", + "glib", + "libc", + "x11", +] + +[[package]] +name = "gdkx11-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e7445fe01ac26f11601db260dd8608fe172514eb63b3b5e261ea6b0f4428d" +dependencies = [ + "gdk-sys", + "glib-sys", + "libc", + "system-deps", + "x11", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix", + "windows-link 0.2.1", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "gio" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "gio-sys", + "glib", + "libc", + "once_cell", + "pin-project-lite", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "gio-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", + "winapi", +] + +[[package]] +name = "glib" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5" +dependencies = [ + "bitflags 2.13.0", + "futures-channel", + "futures-core", + "futures-executor", + "futures-task", + "futures-util", + "gio-sys", + "glib-macros", + "glib-sys", + "gobject-sys", + "libc", + "memchr", + "once_cell", + "smallvec", + "thiserror 1.0.69", +] + +[[package]] +name = "glib-macros" +version = "0.18.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" +dependencies = [ + "heck 0.4.1", + "proc-macro-crate 2.0.2", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "glib-sys" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898" +dependencies = [ + "libc", + "system-deps", +] + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "global-hotkey" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2", + "objc2-app-kit", + "once_cell", + "serde", + "thiserror 2.0.18", + "windows-sys 0.59.0", + "x11rb", + "xkeysym", +] + +[[package]] +name = "gobject-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + +[[package]] +name = "gtk" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a" +dependencies = [ + "atk", + "cairo-rs", + "field-offset", + "futures-channel", + "gdk", + "gdk-pixbuf", + "gio", + "glib", + "gtk-sys", + "gtk3-macros", + "libc", + "pango", + "pkg-config", +] + +[[package]] +name = "gtk-sys" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414" +dependencies = [ + "atk-sys", + "cairo-sys-rs", + "gdk-pixbuf-sys", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "pango-sys", + "system-deps", +] + +[[package]] +name = "gtk3-macros" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1054432bae2f14e0061e33d23402fbaa67a921d319d56adc6bcf887ddad1cbc2" +dependencies = [ + "log", + "markup5ever", +] + +[[package]] +name = "http" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ico" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" +dependencies = [ + "byteorder", + "png 0.17.16", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.1", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "infer" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a588916bfdfd92e71cacef98a63d9b1f0d74d6599980d11894290e7ddefffcf7" +dependencies = [ + "cfb", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "javascriptcore-rs" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca5671e9ffce8ffba57afc24070e906da7fc4b1ba66f2cabebf61bf2ea257fcc" +dependencies = [ + "bitflags 1.3.2", + "glib", + "javascriptcore-rs-sys", +] + +[[package]] +name = "javascriptcore-rs-sys" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1be78d14ffa4b75b66df31840478fef72b51f8c2465d4ca7c194da9f7a5124" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "jcode-desktop" +version = "0.5.3" +dependencies = [ + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-dialog", + "tauri-plugin-global-shortcut", + "tauri-plugin-notification", + "tauri-plugin-opener", + "tauri-plugin-shell", + "tauri-plugin-single-instance", + "tauri-plugin-window-state", +] + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.118", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "json-patch" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863726d7afb6bc2590eeff7135d923545e5e964f004c2ccf8716c25e70a86f08" +dependencies = [ + "jsonptr", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "jsonptr" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dea2b27dd239b2556ed7a25ba842fe47fd602e7fc7433c2a8d6106d4d9edd70" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "keyboard-types" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a" +dependencies = [ + "bitflags 2.13.0", + "serde", + "unicode-segmentation", +] + +[[package]] +name = "libappindicator" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a" +dependencies = [ + "glib", + "gtk", + "gtk-sys", + "libappindicator-sys", + "log", +] + +[[package]] +name = "libappindicator-sys" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf" +dependencies = [ + "gtk-sys", + "libloading", + "once_cell", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2", + "objc2-foundation", + "time", + "uuid", +] + +[[package]] +name = "markup5ever" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8983d30f2915feeaaab2d6babdd6bc7e9ed1a00b66b5e6d74df19aa9c0e91862" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + +[[package]] +name = "memchr" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "muda" +version = "0.19.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd04e60bc0b07438a6771710ee1698f98f6ebbc7f89b61264af1563b8aeb878" +dependencies = [ + "crossbeam-channel", + "dpi", + "gtk", + "keyboard-types", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + +[[package]] +name = "notify-rust" +version = "4.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", + "objc2-exception-helper", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ad74d880bb43877038da939b7427bba67e9dd42004a18b809ba7d87cee241c" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-data" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b402a653efbb5e82ce4df10683b6b28027616a2715e90009947d50b8dd298fa" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d563b38d2b97209f8e861173de434bd0214cf020e3423a52624cd1d989f006" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-text" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde0dfb48d25d2b4862161a4d5fcc0e3c24367869ad306b0c9ec0073bfed92d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-exception-helper" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7a1c5fbb72d7735b076bb47b578523aedc40f3c439bea6dfd595c089d79d98a" +dependencies = [ + "cc", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "block2", + "libc", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.13.0", + "objc2", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", + "objc2-foundation", +] + +[[package]] +name = "objc2-web-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2e5aaab980c433cf470df9d7af96a7b46a9d892d521a2cbbb2f8a4c16751e7f" +dependencies = [ + "bitflags 2.13.0", + "block2", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plist" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092791278e026273c1b65bbdcfbba3a300f2994c896bd01ab01da613c29c46f1" +dependencies = [ + "base64 0.22.1", + "indexmap 2.14.0", + "quick-xml 0.39.4", + "serde", + "time", +] + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24" +dependencies = [ + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 2.0.18", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "regex" +version = "1.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "sync_wrapper", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schemars" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "url", + "uuid", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 2.0.118", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-untagged" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9faf48a4a2d2693be24c6289dbe26552776eb7737074e6722891fadbe6c5058" +dependencies = [ + "erased-serde", + "serde", + "serde_core", + "typeid", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_spanned" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +dependencies = [ + "base64 0.22.1", + "bs58", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "serialize-to-javascript" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f3666a07a197cdb77cdf306c32be9b7f598d7060d50cfd4d5aa04bfd92f6c5" +dependencies = [ + "serde", + "serde_json", + "serialize-to-javascript-impl", +] + +[[package]] +name = "serialize-to-javascript-impl" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772ee033c0916d670af7860b6e1ef7d658a4629a6d0b4c8c3e67f09b3765b75d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shared_child" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7" +dependencies = [ + "libc", + "sigchld", + "windows-sys 0.60.2", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "sigchld" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1" +dependencies = [ + "libc", + "os_pipe", + "signal-hook", +] + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "softbuffer" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac18da81ebbf05109ab275b157c22a653bb3c12cf884450179942f81bcbf6c3" +dependencies = [ + "bytemuck", + "js-sys", + "ndk", + "objc2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "objc2-quartz-core", + "raw-window-handle", + "redox_syscall", + "tracing", + "wasm-bindgen", + "web-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "soup3" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "471f924a40f31251afc77450e781cb26d55c0b650842efafc9c6cbd2f7cc4f9f" +dependencies = [ + "futures-channel", + "gio", + "glib", + "libc", + "soup3-sys", +] + +[[package]] +name = "soup3-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ebe8950a680a12f24f15ebe1bf70db7af98ad242d9db43596ad3108aab86c27" +dependencies = [ + "gio-sys", + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "swift-rs" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4057c98e2e852d51fdcfca832aac7b571f6b351ad159f9eda5db1655f8d0c4d7" +dependencies = [ + "base64 0.21.7", + "serde", + "serde_json", +] + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation", + "core-graphics", + "crossbeam-channel", + "dbus", + "dispatch2", + "dlopen2", + "dpi", + "gdkwayland-sys", + "gdkx11-sys", + "gtk", + "jni", + "libc", + "log", + "ndk", + "ndk-sys", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "once_cell", + "parking_lot", + "percent-encoding", + "raw-window-handle", + "tao-macros", + "unicode-segmentation", + "url", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "tao-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e16beb8b2ac17db28eab8bca40e62dbfbb34c0fcdc6d9826b11b7b5d047dfd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "tauri" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2616f96cb644bf2c5c456d9de4d5d5100e592d7424c74d8b55c5cb96e359e93" +dependencies = [ + "anyhow", + "bytes", + "cookie", + "dirs", + "dunce", + "embed_plist", + "getrandom 0.3.4", + "glob", + "gtk", + "heck 0.5.0", + "http", + "image", + "jni", + "libc", + "log", + "mime", + "muda", + "objc2", + "objc2-app-kit", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "percent-encoding", + "plist", + "raw-window-handle", + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serialize-to-javascript", + "swift-rs", + "tauri-build", + "tauri-macros", + "tauri-runtime", + "tauri-runtime-wry", + "tauri-utils", + "thiserror 2.0.18", + "tokio", + "tray-icon", + "url", + "webkit2gtk", + "webview2-com", + "window-vibrancy", + "windows", +] + +[[package]] +name = "tauri-build" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc9ce40b16101cb6ea63d3e221567affd1c3a9205f95d7bc574941a10636b632" +dependencies = [ + "anyhow", + "cargo_toml", + "dirs", + "glob", + "heck 0.5.0", + "json-patch", + "schemars 0.8.22", + "semver", + "serde", + "serde_json", + "tauri-utils", + "tauri-winres", + "walkdir", +] + +[[package]] +name = "tauri-codegen" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08279169ff42f8fc45a1dbc9dcae888893ba95288142e5880c59b93a26d2cfc5" +dependencies = [ + "base64 0.22.1", + "brotli", + "ico", + "json-patch", + "plist", + "png 0.17.16", + "proc-macro2", + "quote", + "semver", + "serde", + "serde_json", + "sha2", + "syn 2.0.118", + "tauri-utils", + "thiserror 2.0.18", + "time", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-macros" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8b394794f399a421811d06966343e7933fcae92d59f5180b9388d1174497a45" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "tauri-codegen", + "tauri-utils", +] + +[[package]] +name = "tauri-plugin" +version = "2.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74be5dd4bed9afbd145e5716b5fa2ec28cbc29c34ffa61c258c9273d896c8020" +dependencies = [ + "anyhow", + "glob", + "plist", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri-utils", + "walkdir", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4dd9f4c5136c09cd962da0c86dc4accd4666db2ea591cf16e6597435843bd2b" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-notification" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01fc2c5ff41105bd1f7242d8201fdf3efd70749b82fa013a17f2126357d194cc" +dependencies = [ + "log", + "notify-rust", + "rand", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "time", + "url", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-shell" +version = "2.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8457dbf9e2bab1edd8df22bb2c20857a59a9868e79cb3eac5ed639eec4d0c73b" +dependencies = [ + "encoding_rs", + "log", + "open", + "os_pipe", + "regex", + "schemars 0.8.22", + "serde", + "serde_json", + "shared_child", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "tokio", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[package]] +name = "tauri-plugin-window-state" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73736611e14142408d15353e21e3cca2f12a3cfb523ad0ce85999b6d2ef1a704" +dependencies = [ + "bitflags 2.13.0", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-runtime" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b4bc95aed361b0019067d189a1174a603d460d0f6c72606512d59fc9c12ec8" +dependencies = [ + "cookie", + "dpi", + "gtk", + "http", + "jni", + "objc2", + "objc2-ui-kit", + "objc2-web-kit", + "raw-window-handle", + "serde", + "serde_json", + "tauri-utils", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webview2-com", + "windows", +] + +[[package]] +name = "tauri-runtime-wry" +version = "2.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe41e015bf8fc4d6477ff4926a0ef769dc64ff34c7b0038b6f7cacae892acb5c" +dependencies = [ + "gtk", + "http", + "jni", + "log", + "objc2", + "objc2-app-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "softbuffer", + "tao", + "tauri-runtime", + "tauri-utils", + "url", + "webkit2gtk", + "webview2-com", + "windows", + "wry", +] + +[[package]] +name = "tauri-utils" +version = "2.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e176a18e67764923c4f1ce66f25ae4abe5f688384d5eb1a0fa6c77f3d90f887" +dependencies = [ + "anyhow", + "brotli", + "cargo_metadata", + "ctor", + "dom_query", + "dunce", + "glob", + "http", + "infer", + "json-patch", + "log", + "memchr", + "phf", + "plist", + "proc-macro2", + "quote", + "regex", + "schemars 0.8.22", + "semver", + "serde", + "serde-untagged", + "serde_json", + "serde_with", + "swift-rs", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", + "urlpattern", + "uuid", + "walkdir", +] + +[[package]] +name = "tauri-winres" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc65d45c68858bfe420dd29e834b5d15dbecf8a07a8a16cf4d532c7b1f69d4b6" +dependencies = [ + "dunce", + "embed-resource", + "toml 1.1.2+spec-1.1.0", +] + +[[package]] +name = "tauri-winrt-notification" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e66e07de489fe43a46678dd0b8df65e0c973909df1b60ba33874e297ba9b9" +dependencies = [ + "quick-xml 0.37.5", + "thiserror 2.0.18", + "windows", + "windows-version", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "tendril" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4790fc369d5a530f4b544b094e31388b9b3a37c0f4652ade4505945f5660d24" +dependencies = [ + "new_debug_unreachable", + "utf-8", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "time" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711a53c2d47bbd818258c498c8dbfe186a2526c631495cfe7e078567f86b8469" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71c652a3727a9cbb9a02f707f530b618ce00d0ccd762009c8c23bd191df3c17d" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "185d8ab0dfbb35cf1399a6344d8484209c088f75f8f68230da55d48d95d43e3d" +dependencies = [ + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "toml_edit 0.20.2", +] + +[[package]] +name = "toml" +version = "0.9.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 0.7.5+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 0.7.15", +] + +[[package]] +name = "toml" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee" +dependencies = [ + "indexmap 2.14.0", + "serde_core", + "serde_spanned 1.1.1", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow 1.0.3", +] + +[[package]] +name = "toml_datetime" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "396e4d48bbb2b7554c944bde63101b5ae446cff6ec4a24227428f15eb72ef338" +dependencies = [ + "indexmap 2.14.0", + "serde", + "serde_spanned 0.6.9", + "toml_datetime 0.6.3", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_writer" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "tray-icon" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65ba1e5f6b9ef9fd87e21b9c6f351554dbd717960089168fcfdef854686961dc" +dependencies = [ + "crossbeam-channel", + "dirs", + "libappindicator", + "muda", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation", + "once_cell", + "png 0.18.1", + "serde", + "thiserror 2.0.18", + "windows-sys 0.61.2", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" + +[[package]] +name = "unic-ucd-ident" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e230a37c0381caa9219d67cf063aa3a375ffed5bf541a452db16e744bdab6987" +dependencies = [ + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlpattern" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70acd30e3aa1450bc2eece896ce2ad0d178e9c079493819301573dae3c37ba6d" +dependencies = [ + "regex", + "serde", + "unic-ucd-ident", + "url", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "version-compare" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503b14d284f2c8dac03b819967e155ea753f573586193b2b2c95990cb5d69280" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.118", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6430a72df5eb332242960fe84b3002a241163998241eb596d4f739b9757061d" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + +[[package]] +name = "webkit2gtk" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1027150013530fb2eaf806408df88461ae4815a45c541c8975e61d6f2fc4793" +dependencies = [ + "bitflags 1.3.2", + "cairo-rs", + "gdk", + "gdk-sys", + "gio", + "gio-sys", + "glib", + "glib-sys", + "gobject-sys", + "gtk", + "gtk-sys", + "javascriptcore-rs", + "libc", + "once_cell", + "soup3", + "webkit2gtk-sys", +] + +[[package]] +name = "webkit2gtk-sys" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916a5f65c2ef0dfe12fff695960a2ec3d4565359fdbb2e9943c974e06c734ea5" +dependencies = [ + "bitflags 1.3.2", + "cairo-sys-rs", + "gdk-sys", + "gio-sys", + "glib-sys", + "gobject-sys", + "gtk-sys", + "javascriptcore-rs-sys", + "libc", + "pkg-config", + "soup3-sys", + "system-deps", +] + +[[package]] +name = "webview2-com" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7130243a7a5b33c54a444e54842e6a9e133de08b5ad7b5861cd8ed9a6a5bc96a" +dependencies = [ + "webview2-com-macros", + "webview2-com-sys", + "windows", + "windows-core 0.61.2", + "windows-implement", + "windows-interface", +] + +[[package]] +name = "webview2-com-macros" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a921c1b6914c367b2b823cd4cde6f96beec77d30a939c8199bb377cf9b9b54" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "webview2-com-sys" +version = "0.38.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "381336cfffd772377d291702245447a5251a2ffa5bad679c99e61bc48bacbf9c" +dependencies = [ + "thiserror 2.0.18", + "windows", + "windows-core 0.61.2", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "window-vibrancy" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c" +dependencies = [ + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "windows-sys 0.59.0", + "windows-version", +] + +[[package]] +name = "windows" +version = "0.61.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +dependencies = [ + "windows-collections", + "windows-core 0.61.2", + "windows-future", + "windows-link 0.1.3", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" +dependencies = [ + "windows-core 0.61.2", +] + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings 0.4.2", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[package]] +name = "windows-future" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" +dependencies = [ + "windows-core 0.61.2", + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-version" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4060a1da109b9d0326b7262c8e12c84df67cc0dbc9e33cf49e01ccc2eb63631" +dependencies = [ + "windows-link 0.2.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.55.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb5a765337c50e9ec252c2069be9bf91c7df47afb103b642ba3a53bf8101be97" +dependencies = [ + "cfg-if", + "windows-sys 0.59.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wry" +version = "0.55.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186f9871daa55fd9c016578b810d149de58367113db7fb72b462d2323ce19514" +dependencies = [ + "base64 0.22.1", + "block2", + "cookie", + "crossbeam-channel", + "dirs", + "dom_query", + "dpi", + "dunce", + "gdkx11", + "gtk", + "http", + "javascriptcore-rs", + "jni", + "libc", + "ndk", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "objc2-ui-kit", + "objc2-web-kit", + "once_cell", + "percent-encoding", + "raw-window-handle", + "sha2", + "soup3", + "tao-macros", + "thiserror 2.0.18", + "url", + "webkit2gtk", + "webkit2gtk-sys", + "webview2-com", + "windows", + "windows-core 0.61.2", + "windows-version", + "x11-dl", +] + +[[package]] +name = "x11" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce1022995ff5ff5d841ad7d994facc23098cd40152f2c1d11cd607c6f530653f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ae7f38b72ec2a254e2b87ef277cf2cd4fb97cbebf944faa6f33354da0867930" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.118", + "winnow 1.0.3", +] diff --git a/desktop/src-tauri/Cargo.toml b/desktop/src-tauri/Cargo.toml new file mode 100644 index 0000000..8c9a294 --- /dev/null +++ b/desktop/src-tauri/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "jcode-desktop" +version = "0.5.3" +description = "jcode desktop — native shell around the embedded jcode web app" +authors = ["cnjack"] +edition = "2021" +rust-version = "1.77" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon", "image-png"] } +tauri-plugin-shell = "2" +tauri-plugin-notification = "2" +tauri-plugin-opener = "2" +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# Desktop-only plugins (no Android/iOS support). +# NOTE: the updater plugin is intentionally absent — it panics at startup unless +# `plugins.updater` (endpoints + pubkey) is configured, which requires a signed +# release feed. Add it back together with that config. Autostart is likewise +# deferred until there's a settings toggle for it. +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +tauri-plugin-single-instance = "2" +tauri-plugin-window-state = "2" +tauri-plugin-global-shortcut = "2" + +[profile.release] +panic = "abort" +codegen-units = 1 +lto = true +opt-level = "s" +strip = true diff --git a/desktop/src-tauri/build.rs b/desktop/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/desktop/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/desktop/src-tauri/capabilities/default.json b/desktop/src-tauri/capabilities/default.json new file mode 100644 index 0000000..a7e00c6 --- /dev/null +++ b/desktop/src-tauri/capabilities/default.json @@ -0,0 +1,24 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Capabilities for the main jcode window. The window loads the bundled jcode web server over loopback, so the Tauri JS APIs are granted to localhost origins via `remote`.", + "windows": ["main"], + "remote": { + "urls": ["http://127.0.0.1:*"] + }, + "permissions": [ + "core:default", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-focus", + "core:window:allow-minimize", + "core:window:allow-unminimize", + "core:window:allow-start-dragging", + "core:window:allow-close", + "core:app:allow-version", + "core:event:default", + "notification:default", + "opener:default", + "dialog:default" + ] +} diff --git a/desktop/src-tauri/icons/128x128.png b/desktop/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..62e747125d943bead001c64dd2c794ed6a11a41a GIT binary patch literal 2565 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square142x142Logo.png b/desktop/src-tauri/icons/Square142x142Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..618193e810b249f5a58afda8db1d2f0055ecd0a4 GIT binary patch literal 2878 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

T_dv5BJqA)`%4*=F-_i zLqkhMiK?duUE#esO1Bj!;BvY)Ha0xlJ==}0-PCU#Ne53j55xk8#ARrEBP)gKn!B^M z!VXgoK9;?;>d||l9{Yyltxt@?VC@S7WZd`s!7W1goctGBE8Smq$ZJwgg0ABB>vaHH=h7Zug$I*O%ILqr{5 z_c}fsq3YRrE&_-TI6Ia)SSd8NhubUm;&91-q5DfzX;q!;l(x}{lR2ro z*RTZiS_O`j=6Mub{wr>4@R!_>YExz8YApeuSnJC-Pd)#LL)_OWY68mDpE(7W$I^3> zU=*)5ifWTPBkpy=t}G2`5Rde6O2>!YJHvZ{@px_IbYx!PelQr=&0M_rP!vuRa}j63 zVRzrPo!O#wUO^$JnGx>Gur2aOFfHzBao0K2pQ(aw_P(&Hg;$jUk?FUrM1C2QWBF*F zo}{&Xxnb1nhpAVwy*+PpPVW$A+WHT2ad%aPfc`HFag@Uj; zO}ubJB9Shi@aJM33Yt*gjvo88)8Q2j$r=9A-OyF}kUGQY{{F}@AR6u}J=$>|l#R<% z;nhevLUhhDnaJzx`u68X-V4i}8Kos(D*KBFa`*PH`TBfTtTn<J`2gppkOc=R06`eY1S%jd zoH=zQ|A+oTKiRFlsk*W<&GAmpUki2=(xLA@PB{8^{bIUmts_U<`%%wJH{{q>eA!k} za*F}eCo&W1xfmz{L9JY1U-3;?L_K%x)z}T@b93=k7CXy}OI)A}<CH+$Hp3aC3J6&PKQcOdQYvV$^nA*m7;LRl(ReWsnR)Tx8{h_+C02w3~pu6tyI@dLY8Gj2G z=uc-7IXO9N>X$U1G{0FA1PVEsvogKhwbNxT_|M?l2yOeMn*HRvQP2AWwM|iAGgDA| zKrkz&YR=8N>Dn#sIL8@X89 z?rJqsGZV!&YaH87N!`tze$jof98vRT`BYi58vY}VL0nkgXd^K8jJ)CV=5Y&}aO)jr zfN{TX$o$s#=wAZNhV*Ufj CF~27O literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square30x30Logo.png b/desktop/src-tauri/icons/Square30x30Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..ce55f01e554fb0286718939e788c7e2a08b61e8a GIT binary patch literal 726 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square44x44Logo.png b/desktop/src-tauri/icons/Square44x44Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..544fcb35bc12dffdf8b234a7660c260456674e0e GIT binary patch literal 1062 zcmV+>1ljwEP)&4Yp!gn}Tbz4Rn_2nC^_ z$3mnYT52w`g*NG-f3`{eK|@T6q)pQL-rC*nHk)jAleO!EWp`)i&3rTSy*F>(GVq@X zQ|^P??Y0+-#h(5BeZSmyv)No#2i4Z&>*B>OFE76s;BvX3qoYG8+uGWK&CN|+z@otmeNkCF9tZRel6w3G#>h27 zU}tA%OF9@D8iJ{*sk(yg>+6H@@$r)H!C(*)iA0I6x!j251f6)@X~4I6RA|QKbQz`P z9T?zTGq*&Gi;H#V?BwJmEG;d8)oSJ5!{Km6JPaRnAV*{?%+1Zoc6ROj4h&w-fUR*) zO_&0iT{FCnHNnR(Cxtd+v6y0D#P`i;ovpzu8Mt-fx26!0nj*mEX0cM=jMk;4nbInx ztCd2GRzLgVlO5teEnu_!rG(R3fc}fy+;jBnslz3g0UOyktJww=jjr4hC9({>S~#@u z>&R&yFoC53>9Kn<>iK8Vy-d>)sV1;B=qeqxn0m(s^9uFn<0jAL^<88ir_{qJrs>D z5D38C)&=NqT|110&*h-!jY|@B*(^ojdpy;s3ar=b6#{rofW?hZOiTbg!yHCVbW}oc zKF2N>l?elD&y@vS32Z17g3-~@+Ki44e-g>1OB%`M8$<)xgODf7JtwzMNMdCr8i-HF zVo4^G)f*iF_WS+d^ZB^4hwe7;p8h6LywtqIg_z!!U3d`qAyDG}Osgu+Mj{dD?d_Eu zeCdow15sskq!5ipx#uUJ8z6jJs`&KA_malO*cO2%Y7C5W9&{lK$E#&uzau>vbTHOnbPHqLNY06m1SWe2RtTLPNgW+cX^R}h8x zSV^qi+1W{!)nw45;4e&H*(I4D`ln#j3?^ZdJEq%o(Ew+5q`Cyyfl!*BGT(T)aRBaWz$&@O3bIbFi@X6E g*jrUU7L6=_0O74X8HIDCtN;K207*qoM6N<$f;*Z6N&o-= literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/Square71x71Logo.png b/desktop/src-tauri/icons/Square71x71Logo.png new file mode 100644 index 0000000000000000000000000000000000000000..8d22ebc4de665efdbbc27ad98a40247cd947901d GIT binary patch literal 1464 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/StoreLogo.png b/desktop/src-tauri/icons/StoreLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..020e823011634ba3d81fc1fb8620d3fcb70ae1d9 GIT binary patch literal 1131 zcmV-x1eE)UP)<-AcAx=WYvl4hoFrZe;P{mr}?3Qh=7 zxjIx;Rhi@Q_*J!9O(zlw^TBq)7d4^C{vnE@yn4NU$?NrovUOBtYOq?ZO{D1G+uLhW zsrb9e%=c(Cnt|2TRgWMbD1qJG-62x#I)=sO4uSQS2a)=x8)L#48cB31ScP3IM_K$fudHnRJQtD+xsUnpugYY~!cGKqQG6 zeks~YQX(Wy&`Jb>6pO`ZR<$tLDG>x?_-th7i`Ys4k>Z;(Zc(s<$z+1QzCQG3oHm!s z1+%lWl7eV$ZH2C`E)H*feH{h|2O$!P@Qy)c8qDEvKzn=pp~)H=8o*|=Nh*o4v9Uw# zcDo${fdGt*jPQ=37O00Tx!rEb)oE^SF7xIv!x)qh5FH&Iu&}Ton@x0gcXK2$H8quU zjTE!BCQ)7R-a382`tm>EsEa^b)vvtH5$-SNV8UmD_Zw#gYX#r2fD$XhgM7yo0>YLb z2&6XK7T}NwL@bE({!}51@HHh>RqI3o?p_B`{*OEicPQBKt*E+_eg($COHN@fmwGdeKaR|gHc(Q1L z$c{SeBvI0?lGxxyZBY=!>)T(!sQtTt488ap8eddqogBC6a-4Yt0hykj26#o%GoL27 z;PkstPrxjGE8*~K%XZj$WJe{bo~eWeP-2=mpb+I^gw54Cx?eQt;xyB%uWXW zXXBlu=+8FMy}B?wwr?J4L$zAmC;@{m0e5PmtTfOz?!KvDoq>d2YhYz%#mPt{lv|ifcsuDTUGRIo>-y?Org zv4bGx60wcmj44wz8uFq5-=0hqvm~@yZBy1K9O+b3Q-en2B-ag%#bVfK6U)oX9#OA@ x7(&;ax&o$|i$}ShIbpEFIDv64?`KX*qkl-vJJ~%8ec}KB002ovPDHLkV1gbb5R(7^ literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/desktop/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b66c44934150a553eb0640def76fa653c0fb9400 GIT binary patch literal 2056 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 Hu0mjf+NNUW literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..a2d367e849a6c5ab5697c1a645a3b1d6caee7dce GIT binary patch literal 2072 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/desktop/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000000000000000000000000000000000000..4dbc5438192a9ae35879221769de373e259584a1 GIT binary patch literal 1818 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 P00000NkvXXu0mjf1x!|m literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/desktop/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000000000000000000000000000000000000..58aa4c84b343fa89161c7cae6c6c6a5e11fb53ee GIT binary patch literal 4391 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..12a25331339be02406ebb33181f645837d37df2c GIT binary patch literal 5592 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 Hu0mjfn)u7e literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/desktop/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..147ed1e400b7f0816dd0ce68f8526238fb8bd402 GIT binary patch literal 7505 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 + + #fff + \ No newline at end of file diff --git a/desktop/src-tauri/icons/icon.icns b/desktop/src-tauri/icons/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..673b3f40dde379ff0db867eabec54917bbac56b5 GIT binary patch literal 70302 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 UHMH;P7X)E`nWG&j&wu~>KdKY~UjP6A literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.ico b/desktop/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..ec329569324e9d400d5a3e38c899c4ec8ca47bf0 GIT binary patch literal 13041 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%0A9@2>`0H$bQU+UVsC>W(sJ)Y~L z(g(&tV73zmbHo9?lae(#?J0QUr? zJNY>S4zkW9OIme7@1F*qS8!sy)!L{x@iO+n-{t=)Cc_9`fcX!nepjP%+SE%do)Gkl z@;^84gDX#1j3_0tq&YIXq@>PES4W2dw>U5e=S2WMnbQZO!1Mn8NpmJ5_MpJS2?|C9 z6fY0@C+3(8lVTb0)^aR30JKSZFfphSPS_Ug$OcrvU$=D9lzd0NC(d9XZYF{>19vQa z#$y4X*$^HeH37E?@DS}sWE#4$z1^y%Cfc zQH@0=#n1^b1lITf^CJVc7dByTWMpI7G}lFXa8iDq+-ocz}xc z;e-wOR-lyTrwyLu>ia922jb@R`Q#^yN4gllE;H=Od7ga&*0ci;$)oZvtB zw@0uQHfn?}xOXNGHs^QkfzO1w1wQ?!o5F+&3T?p$7yO7upgD+x`BVWLSD3?u`-J@D z$GZ7UvVf?0$NqW@c)cJeyyN>ZbRikD`;JHWVqh)s6p)`VFc%gk2KZKBMmNB`jCL={#uFT4d_eF+X@o43QCoO|$dyF0~TuK-pSNXzSuEbWhk`5;o*+=#TZ zwc*0y3KKfOe_UxjfUUuqrKFBlEG<|O>#Hsb&n?(XvExhiqFes3jnM^M)+NKtpes#- zWeCr)0kr|hKu;dWP8~1Sq|cSmYXSNIX9AQVML3&*QK_q|3&0-Yy1w|EM{<_w?#>|Z z*`7IikGgB?iUK;RBbOoMg2F)antMm$nkWlqs&jL5E4*lQdLKOC1{DUeggOa|W;1L| z#F#jGS67l9QyB;)vujlE2?V1=`66L`F-?PB16VA8`OoSoWQP)PUI!}Y)3fv)LP);v zcWBfADhZE1U>1uUL5KOw^(x}HVaZ@J^V7W|!H#o^wty?;nQ_~-0@&D<-Fv6v%vw6P z5T-9E6CU>c>R%~2Swh8cCz3}_GSXn;WF+K=?a{g)_m_SUb8Wlln>KW}&5f{Lk8ey> zLMhp?o7BK4g=P3lj7Y7PWSq=Q0rVYDo3@&-uEGKPQUY@YV5|RDkwl3jbeOIU&haxq zKKQ{QeyRZCdQ>~94u5ZxMDl=;9#Gi8Bf{YLk17YzCtxz1=P6c({BR+cdiP2MC@DW} z2~akVC#3bgx(lSbTvtAe&x2(JL+xBZQ z8y8ueW0^G`v^lkbm>Pe^vPdyv-LaL=p3(T94KW+jBpH{}+Be5UwY*rCguVAry6U%e z+bh+tn2bdSVH7U2+s+^$b%5y)(3kMf1GkjvTqr6MdTVc#3X$;>5qw z)6=&a8ARG&OGUaPE0khXkZlJ) zlIY(luf;5NL)8;w3YPU(hp|t|veYqJC*;WKjm1&O{E*F7Z-!4Nj?kuhqK^hE?ruYPh>n2q ze@7&8>_EUxxi*RJGu;v!jX!p2o4(=47kp(atj+)9ZJou*lX%O!;xbi9xc#M{DX0Z&cGK$(y@;13b_~Xa(!*bH%EX#1+hki-2Q+_K{xDm@ zkU6m%?_M^gq^)mYfH!ZsBYNj5Idjzcd<;cQdr6!B+6|}Hn3VY#+llnI;!Ad%g&&`? zE^B2>4I73v9S_VI6{yHe6Qx|&=dXEs;!;7v@leU=A-+rvT0Q?^XZi#uVE=kqVge6; z6KU;UceXZTc$S#6hS+u3V6j|2Ykx24m!)42%a|;c3qbrPSL=a4%#UBXYX44_*k<q|3ca}*FKR!}R z`|CH;J1O@U!*2Rqq2Bu@bC0B5Y?%(_y!fpSU8#Lv_sFP59u1aq#bih65Vyh0ZdV>j zY89jC00&$%eQ+$6C=HupAKHrLBb~g4m}p`Mn|MuFQp6WI2o6*GwUV&6ZQ3wBD0yq2 zp%t-kShc@BYz!QJH81s#@2}qjBl%FbVt&WK&-2$i(U2OabN1+uS1uj=ykEuQ@ZrN@ z*rYq3KOb$R(VojF?RwndH?(E@jvd#I?z)+DFzriKl%j>HscBiqBj zzLD0~rZitw>Mk9fC*GrNPyPJ-MEv{seEyA3N`_B+2;l2M<3Ca}lKtL4SxO9@c5r4f zX?YrV96O3}UM)xLOPRU}$OFxC(?f~a&A5TIgqvab7&`v4{#Uho34PP)6C z+FBQnlo5IKtk(tuOP*=uXt*;)9ULq_zc^d^$$#zQP-kmY@6y)_?oU1`qNp7W* zl1gRmWCD4`iWOa}t(VHE%2=6Gv4VhiYwyKq&gT2Q@EL9{+tO^SWVrGIpK}6XTt36_ z_VJP52)+aqE$=y6zn$gG#0DxH2bY4Pecobj4yf>Hmz(+jiro+L$+=o@m6p-*ViR_iEqBT`(yBT!^NeXJt_X9^!L4@3&6-uZf{Q zx*X_xN{QECh6$eC8q1asb)u#fYsgSgEP+>T)``HW?aQ1G49eSC%<=mPwca8hW_5^^ zaz5GCOZGX-#5-_8q(^(@07jL(6E~VS@qufwU&PM@;y5frR5}eg)0nsHj~bZLc00t- z?QeyQQp76=l>`%u!-jmFJ&?yHKc{C!H@1smd=83looGRn}Y5OfD8nqi=t3-T`e4K!wGc&`U8oay3CW~`ie4tG9r-wmB zjU<(V@B@^w4`j}RISVSMXtkx2qpqtQE2+Bih@s;%Qe4O;tDUNki!t2Edjy}%%_0}W|f$to0WWR_n&KY$!yu$VYDytSw@U5Vy)jGZ+P|rvDrd& zB^2`bU`Nr{&q0>fR2lo^qTg=11$e|+=Un$74yl9dKr-Iwh+ejK6}AeIS#M<8r{IcIRV>rg$9|dE?|%k$-$>eXRN_`ONQ*bh2T~k;JmS_% zu^#>M{{flBoX@$bx|(oa-0Ne1M@&8c3ToZrDjkZ&6%;7sYx8fw$}H$)?fzSP5M0OW zl@wSZwTln=*n{Gyr3pV|*6KDWDO8wW8D2HWIA$*TD3q%MEta#sZsuhU7WL$f1p@Jo zrUHF-{k#=~=G51HF>i^}1^(V3pkqkv zt22z{;AW)OtBQ;r;B1EPN`sukKK3io4#Q)C(y;tDvnvu_e^OHDdgLh7?mv_T+J%<= z)Y9@ZteBMgyq3F~RxSgdGClMDkN|Z>u3kxJN9-A$r)Ta4W}c+8)1KJ{98P<-&f<7` zd^tg}_%B20Z*i9{>pheq@uaT~b5qMnt5)^WuApw^zmP4@)8Cf7>mu z!EANk%^1R&dzZ4F1cov1hc0HEDX84g8@NK0b}`q#PRt9K{F#t^POKDME;we{K8msS}o6cwci>ALP;qPlt06uRS7iJdsBG%8t% zB2t6Zzwq4hxx8J#(Z;4GV6&5*Erd~NgT9nasI^Q@tAbbW*nzLSt#KxhjBV%B-t#=BrRb8^!!icT#fq?^aYjtVyn6Z09!ltZ={gNjW^8 z+V|4;Tc|Yh3ajVcsSn)rXcYAh`Mr$)tRQ-(2TIjZY_)frz=>m(PL^Lo?7Gq)vy%~K zYP>nuroWc$in};j)r;x9Ccy83j=p5+diEUXmtnN~&|e!@gQ9*>z~^1Mc(G>jOjGW| z#ZT;=*C+@h}>C?u0x6PUb?K{I`l z)6AUpmLBrk4qM;OuuqrwI#A=;iw;&xT8Td?Dzeh&O*VAsdr8O=WwP1#HOZ5ow9&Zs zS=~>ht!H@h2^IR_XE4z*S#q1)r&B8?nzJ3g_UC(M5@L>I&_Y(El$Dj0dos$yq%M6< z-5Yk}Wtdc7L8kJcOn@4L#DBhzcdp;HyN5nC*AcBH=%v*5k`)W1m4=$rYhl8m0*xr6LQ9!%`U z*emp4J-xGMBT4ky7fel8J3G6uEK`Vf=jzOd_Miv8Np}g{kcnjWfJJt5pZ{~LmNC9$ zGp93~4p1|jfP7_fx>fRtp(#;q(Gf#BT89>-YSCq3hrPS0I&WK)0xrEhc4ne*Iy`hE z-!eC$Nc7d|(}z9J=arJ}!rrWjIfi=dV@T!fT^r9Htld*=JNeM{>He8G6^*dPjE@DP z2da*qF7%4F6>noc^sQ=Gf$u%l`RKxZ%Fs?9ULViXk*GfS^s7tF;#mBJe)2?uUHD?g zvr|`5$r_{VG%vGH?v5rrhfWjS43NjC_pV`TH zs46K}=yTR-^~d-1yg63)k!{$G%eceS=Hn>I_Tra1vX18m<~V@5R7tX!++O{*CWup~ z3*q{;j-LGLd@Oay@_|85hGI@{zT=@i5b~(y*IRZ0TkamAY~t7?g(=fe z$ou*u_7i4=O=A-x?`a{@T5r}>*qNnH*5{b=|0?bh9$aw9R&rp}QiL#Sb_%WTuLKKmZg zZ{DStxj5F~aPzuqPpi9u>fzq&?$EVL3`#O2vm)GPFL}UgxxDd?j*b_%T?{!n>Zi>D zD&mdmGbDYA3Dl^Xlf%rH%2_a(~ei%N#kXt)9EZ-80U6dgj#DgsGm% zX}Roo<;s;mLI2lwk@3DaeS3kmYh}&ZlrDKCN4N>ZTXVGC5{&scCDaj&oYLaht}9A@ zZH+x2QiUPJ$D#$Z!quz1gYWDhFA=d}#`<_W?1JESNh%%Y3oTm~z1y7S}P!)s(I z4^kme`pMH|`i)gz^fKS+TwIr$ zWJZZvrQV}~o2H-K5nanu(XcJnF}JkLv&PqLJpj7k6{EI^JY8caw0dJu>%>jrvqhs% zR?fDh2MP_J(tU_q&bAq1v%AcRLrCmj>&}*I-lGyPt0tJWKdQ zpHQc~WKoc41@ex?pqI4HJxzFIN=!UvVbM6a-$2-2CN=T)W^Q=C($LP1PP{C=Bk79B z{XT%djzf4!Ueft?to;MzC(pqzPw(s5Z>oB@{tco@C-cy&mug4u*NC+$2b9*+>GZW4 zMSEB6QHr4}WFC59WiZ?SNWFpj&D3sl%kMSsJ`zU{ciadfM&2rkv%A38FV!QFG{~O4 z?=KW2pRzN5C5lAID^Uq`SrvHf1-{`*;$4g9C(o;`TUU6{Lqq|v6ZW@I(!h2y6Exzc z=gi-1$?>w;bSuaZ?@n&*OOT_#p!c9LB7bh!bn-?2pD!zxh>LK_c+bksu9LUC>XSZW z)Fpc6t>kc`t(?7Tj7rlpRnkt`zhaM&4B8V9rfp4qQF8ye<^4XCWv#6w zN6RP~P&TFlV9Q(9qNzW81hSH<d2qpo!Vw4GYs5``&;tY2rO{|{0823`OF literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/icon.png b/desktop/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..549d775ce0fd831b2c721836f51bf174816754bc GIT binary patch literal 11784 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 AmjD0& literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..eec3f02766ef2c7fb874c214643c7b4e82b05f88 GIT binary patch literal 690 zcmV;j0!{siP)Hr35I-j`t*RQ3TB)syArev}H9G>fNPG+(x)4@A#L}6eEOcRDK>Pt6KxBadwH?w4 z$y)~me?S<@l-g=lsTpe0ssxGp9~(v#l#-g`ga@7>)yvXBLKEJUje1_Nld zTF~$Jb&D8=F`RF*Df3h+1%*NZB9X|-rg+-x^`KNL**??`pjNNf>yS>TSD@%&CX;au zP?2u8OFiC0rP*vQ+y=yXyWL)>o(2K|*8vroOeXq3avRVvdV|3rG#U+6m+4R=c^klF zG70&7UbU6WWe@~G)%UcEIgW!^ECxQG&lZT4hhDFj29DqF*V%~C=4!P{b5^lMpZe0A5$F|DZQ0S1&%&Ofy}R|j#cKv#G|g0X8RysAD99r5(&s=v$XbF(u*&_a5!8V=F(cs zp!bm{zk$DGZP6=95{yP85JgdM!$&+63a$N7_r+HVfFeu}XkC5&U`ET{$-6`NDVIej z#bZ1kub#Gl7rf#htCVu=4IImW{N;Ukc^+^KsJm174i@rL#Ap{tH~aAV(<3@=t80kI zoq7 zamfJ)ATE=DN^LbD>4lyW&~~9D>$I)yNYa+S&Ayjm*Pk6bj^o%dJcL60?9cxBd+&Si zd*26}A_5mc*jz8z+}xDaF$`nagL}PReL?5v=V5()9adIWARdovg4A2b9k<)<(B9sj zhW7b0-}~j*db&95x(84Ww9HE*Ff9jDW}EQ4cyhI|~B?1EzpzffVW{CMKY# zrzgAVS{V{$XlN+?DTbwb6wS4-ug|iOLh<|klo?Z?ZEbCq@yTquhK2@MUS8(E+k_^2 zULaA2EP+5k3c9hekvcDsoCJfxl*DwgtBWm8=BvXvyS24NkGkg#5+#vH(59<3tt)0o zPC}tj_7u?SkU|}+bW&wyWjd{LI-L?dj%agpbB01<5+KZJIbAdwg|V?Q%KgM`vPzBj z_V!W+aZ>G zN4G(a)iVpZ@!$%47pwwz)e(Q(edQQ#d!F&%zYo0+k!Pif?S+Xxaa^M7NY3hElEUla zhc*a4eV0EjZWAF5Ik=Lv;jP3<&D?zGC>I(r`h?$Zp;l)VET(4m-0;F!? z#R?O2_98&$9bSqBvY_Eb^V>Da|5+pBq?zH0wJKym!;4iRa}F=X;FARpFJkdjAAtAj z0YqMuL5R#o;zu^qLx`^7#ZmSKerov(yw$JYPP)ki`HNqwse)E9LFN=*u-ew&-w&M! zcj42MUAkUWaSY#l_5y|;ejs@$&(+t|xrw6^&5-yE-QC^%@tT^N{KJc+_(dw^2nu80 zW<1|@R#gyR=O}$cK}MdJYJ$XK`qI*pVo^Qk@WP!WSxVf!uTYQ84C%Y}wz?krvs|;e zVkKS)Qdq@T6<&lV;X;zz?G`LDm4ip-Mq&tryhUn7j?z>9*w5ig%|406>*3*H%5;Ug z#l=PKOA^?lgM)*V&fifk@jDdkIb1qlBZX&I)gkzq%tdmb&<-Xl-px`+(ZU<3|ikZCxhcE>i=LT;bV_Z9Oe5Eg)HvAS>8RzKs_Xqr(T%v;x5V6)m2C)lT0x| zVw}Vff}^V(plVRLy>(_T_oFm5HRV?BxhVhO^B*`zw4*QoIp+WX002ovPDHLkV1hfH Bjrafn literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5697fa59d3bb94c3054268bb5ca9b82eccdddca2 GIT binary patch literal 1343 zcmV-F1;F}=P)oq7 zamfJ)ATE=DN^LbD>4lyW&~~9D>$I)yNYa+S&Ayjm*Pk6bj^o%dJcL60?9cxBd+&Si zd*26}A_5mc*jz8z+}xDaF$`nagL}PReL?5v=V5()9adIWARdovg4A2b9k<)<(B9sj zhW7b0-}~j*db&95x(84Ww9HE*Ff9jDW}EQ4cyhI|~B?1EzpzffVW{CMKY# zrzgAVS{V{$XlN+?DTbwb6wS4-ug|iOLh<|klo?Z?ZEbCq@yTquhK2@MUS8(E+k_^2 zULaA2EP+5k3c9hekvcDsoCJfxl*DwgtBWm8=BvXvyS24NkGkg#5+#vH(59<3tt)0o zPC}tj_7u?SkU|}+bW&wyWjd{LI-L?dj%agpbB01<5+KZJIbAdwg|V?Q%KgM`vPzBj z_V!W+aZ>G zN4G(a)iVpZ@!$%47pwwz)e(Q(edQQ#d!F&%zYo0+k!Pif?S+Xxaa^M7NY3hElEUla zhc*a4eV0EjZWAF5Ik=Lv;jP3<&D?zGC>I(r`h?$Zp;l)VET(4m-0;F!? z#R?O2_98&$9bSqBvY_Eb^V>Da|5+pBq?zH0wJKym!;4iRa}F=X;FARpFJkdjAAtAj z0YqMuL5R#o;zu^qLx`^7#ZmSKerov(yw$JYPP)ki`HNqwse)E9LFN=*u-ew&-w&M! zcj42MUAkUWaSY#l_5y|;ejs@$&(+t|xrw6^&5-yE-QC^%@tT^N{KJc+_(dw^2nu80 zW<1|@R#gyR=O}$cK}MdJYJ$XK`qI*pVo^Qk@WP!WSxVf!uTYQ84C%Y}wz?krvs|;e zVkKS)Qdq@T6<&lV;X;zz?G`LDm4ip-Mq&tryhUn7j?z>9*w5ig%|406>*3*H%5;Ug z#l=PKOA^?lgM)*V&fifk@jDdkIb1qlBZX&I)gkzq%tdmb&<-Xl-px`+(ZU<3|ikZCxhcE>i=LT;bV_Z9Oe5Eg)HvAS>8RzKs_Xqr(T%v;x5V6)m2C)lT0x| zVw}Vff}^V(plVRLy>(_T_oFm5HRV?BxhVhO^B*`zw4*QoIp+WX002ovPDHLkV1hfH Bjrafn literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png b/desktop/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e76d9fa2c7418c3b3943bf6c3e23749fc642145b GIT binary patch literal 1775 zcmVNZPNx%kdwaQGsbZ57n3dI0iVG&Vm&t5zy=4Ys`Xq7R+$`2!5Vi@R_TSfGB353iS{1a%vJ%5j#EKFK~z~> zYFt@aQB*gp6zu5esH{#jG&G=T7x!TC70$@Wh^*_Inwla2N2}YWq%rmN^|BwQy1F_` zsDj$sTG@T_csz>gwi)%7mOpF8vyK&Nr(Sj2i~x(PY6AlUmYBSThK4wOYJ}CqY6V+e zT`d{I5YJWtwzsUTOfXhqa&l7Bz+<(JSSwit(PR;_l!{Qwhcz-1M(m!gulk|nrTgG~ z?i7qJ-h&IU^3&5W^!2kbi2&l?&#+iAQLwN13EV@7RdHVWhdZz}gJFP;1S>0ULGPKg za~1ID-W5M1nJb+C5 zgR^rOZ_xC48hG!XPa3eg2jU^r5%+H)V6ou8KYL>j3I&2a?eT#i#Wd$1+Lb~ zYkYiMcH1W=CiHQCMbeBwV`F2;V}x=k4-dd2qj5L_#UAg9^152*A$Q0r2v%sMTj7m!gwEB{ zn7Lba(Z^R!8TY2bCuD?M6>JDx+@7hmg2vS}*dm+iw3MUMbMWCem*Bn4=Pd~~Gc!{- zBVKlPwk{P^lKl>EUHcO6Mm}35QUbWo*vlW-)FQo1|MfU z&;lqXlbD0puR_iNS_1b7R8%J@F9g*}V38l+QxAcQnLvbWS+$M9?MZm>orGNC{%#RV z;po9RNVIS%x7#f~p^?y+|KR>$r zOVaf-6s&T&=H_Nba%}tjeR?f?ZVEoZme16^HR zjOOj_dE&|36P4HC&1^Y-9 zNP$)KZJv&fj=;{&4wLL)i3&ilqN+gkSEVpr|KQ-DsEeHFb}X7(QlnKhT;MG(WAbxM z8p#KpXkZa2y0qwdg*ym_!3BX-(sFYmTH#B6xNJzkYQ7IcKky0k1D{qb{{t!oImbIX R_{#tQ002ovPDHLkV1oEUSv3Fv literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png b/desktop/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..cfae1d9775b50e01d83f20719a25fd4fd977d6a4 GIT binary patch literal 943 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 lOB3r0`)>}w{+mNOWBO!R5%*_2V zbIzGF=Z=CX2{1`m1co3@0C#qFAP@*#yc-F}al|@Js;-=z9B{c@;BYwP_ml(N*w}#8 z)m2zoSpmP_Z~A6Pl}TlLMMXu>+}sR}jg8=TyUl-@W^^FJ8XO#y0n)e`+VEnfsi_G@ zMn+_)M#38vn3gwu@fro;#thTlmJRVPSBM?X3($Z2|ZzkRCb#!#V#Kc7WI*`FT zlkh0if~Y!$r>CbGycmdD(^)Zv(O{s?WAK!+zP_$7EWFy(W$@I?jE;`#PhqsQnPhF_ zt;_56g3ssE2DrGmC^C5ZWh56z)C9P>xydnj`eoME);LvwxTd{q%d=F}b#QPXvr_nh z;eY^xXIv%}3Moh6*47q-XR=J>2;?KEiVU8)x*}N#Bx7)y057(j5YLB7N=h_CJXi*G z!}j*JrQwAGs9I&vNQ8~4nKlKp9Vjm^hlPa&If|<%LqkKV>u4*$Gm5OB1cO27>gtNp zb9!y^W;@W^+iM69Wnf^yveUM|zu)k4PNx&PySq)dDVXg*adEL(1#4NIr`9($iBwZY zZSo{HZBTdM6t=3t7#|fL2Foz>}?L*1zy^{jbzE^?8y*+SIhAAPCWwdqQPq2{aUV@Pr52f^E`R>hUzJ zpFea3{Kxe2m=2DglV3E}$vmxv`n%pd@Wm+hREY0UHuT-grxs)oXhk{Dy?Fbxe9bcZ zy+0|gzdG}Zt(2B3cQp6yzKB2ib$se9t{VdiX)wlsES?S z5nGAa8D6qw1O_h!jx>Xpgd=^~_HC&;(lnH=UA~Bf{6GdT34jbVl_H*YWp`n<04T^8&@@`elFzq;g}_;DyC?{cmYx26S&mE$w6X=&CIu zLJ2$4m`}IAzYp-kU4Tz}bU)vI^B6w*?nTYI%FgpxA|+%Z9VA|OWo2bBH#aAz@mj}` z4*YD`aPhTM-MYemj}-5NYs5$1`1m+ccyZ;%Xr=zcFJC3C%ig>77}QcUNKad{@bFOw z;<+)iv$OIe01CNr3(VxJXj|@pVgzDh(C$$dah*3cH3eppq_;ie5QlGVis#Ao4>seE)Z z_e`(77J_)QN$-4r9hOzT0IRO0(VtWRjD3PJ-pX<0-2OTbKJ>i=-I8VPaRtC3C3|ZL z>N?MTrz$wB2O=U_MIF3TQ&S@z9v*TeJAlE3u(Rx!M%$h=RgIF)tqiZQuuzOVcp~af z@ccZ|9l*S3e0hF!$fM5A@K72W8nhj0>D)YyJ*BQ+roO(O(*%eCf~u-2k-<|hQ&(3f zR#sN(A3lcbKs)k=GrcE*(-}{0l>8qtldLTo3?q1^ z&yl`xilxH`@%%hIh%k1=Vt07B!aye({k&LLg&07H39w@H_WWJ6<5BP!Rl&dju5C+8 lOB3r0`)>}w{+mNOoPktyR)-%XXcz_BPwU+ z&i(e>d+s@B?iD2_K;l>tsS5}&2?17?#l=M!85v1f-{bK(y7Te*3JVME19)_F6nc7k z;L)Q;^3Mwk3kgGVlb+Y>1)t9cMMXv6_xt6allCPt>SUpnUb}V;?%cTplarIKg_KOa z?Cfl)t*wQ_hY!o}Edd+`d;R)#*-{Bo^7Rldg2qpxpc4Xo|Necrc=6&IizP_r=;h?( zz=aDJpuD`?e8873U4pAuuM#8+^lEBqWYDGn#v%Fa*|V|}MV)P(UR6~U1OfrwL$)r! z2p1=O^o*&qt=G%X&xc#LZs~$7=(!nMF2UtTZ)|J~8XFsRT}u-%bEYI3mig1`?CgZ* z=4Q#z5RKk)U0oe@HphB5Z{AdmhQ$Po^DesOv=ri74}&7z-Q6+gCWCAjv0i?Xz|1RWh6`Yjl<8k9J& z2i(ZH_`!n*+7^uKU4m;bxG14eNHJEi1~6OmO4Ql*f*Y3p{(ea_;g89B>TLVLMHw0z z0!_fo%stfE_JbSAE+$}H9#NT%J>iDEY1KIx3*@M??Fl#5FfAslI*|0NhYMy@RT3nA z;40~f1lPH64FM)d{KHKcaP$QW3JT<$WM*cjrY=Uw&=Gk0^r@@hVrAt1{rlxVwRCYc zGdw)3T1BI`t)kI~ICJKV{E1i2ojV72@7{F=bxPcpeKLIS~$hJO&^VTxSP(L_0=(2(AqPryQ=0+M3LEif(`lE`{rgcaz)< z*EZQCg6qI+lAGX~sV5R#XIFTN1lQT+yCT7Lc5SvuMd{9dMptK(-0%e!2`*)m2(FF& z5s~0hHi_U;Hi_UmGMi+p4s#(~$|g}ZiQrN;iQocdlL#(QHi_T@Ws?Z5d$UOd*TLB& z+GyOt*(8F?aW=`v-ud;icYdc<*30h!6C|+zyGaPOVb462=dQq;`&_Jp&Y#oem#Xz{ z-9T_5xqEbg;f=%Q;rmZQYv7_}r2Q>#D)t@wfBX^FDLyk__p*} z(mQQzF3pwMBq3EBg{f`%rv$lb*K%B)re*a@Yzm-kQW76vVq)U|zqGy!un^z%t9C8P zbF$uhdNnp^Sxf+Mvf$$B+?Ow3mX98CUN&jwF@yVS-1jKl_D5XzjQ;&->%}433b+{= z8F1yw6~l*f@S9C~_BbVLWHrEjzi+Z^SVpi`!A(szDFN$^8!nsU)d0t{msPUoT(8$_ zX<*3EPIrBMePHHhV|pK=;e$EV|L@mn!DcvAMo(NB7#J|rPQl0n;E^%S&CR9&4vuct z^?QD{rdG4wuJH_AffKX5&IIxUkY_~b;-x-U>~7Jt>-W;Je<-f+@xib(2^rh%8b;*{Uty8 zRM1?;M9ViH&%l?%TcPi_O$t%3*YG)Rr|+9ry++$;CQL4iT^k`c7+vq{l&Z6dG!(mqJB3s<1XF>y+bg7GtKWa&ss%ROl&BC=)sP@NU#N53 zOK?-H=aHC1{zG0~o~aGNBtp}N&yx9V)16|Hgu-1PKxDSFpNQ7LwcYcMnChtTqRs2|wIJmptV$=0&y zVP`Y1S+5K;mPH*uVIU>iir07~KA#V=va%$CYcD-yrZhw=x|~Oj93i+i z(>r$TnBv9O4AJ3WcS%Xff8I2~p2N_NnSD=RA{>TJIB4jw!xF~FJ^jyVTM&&So()zXtEPdwDw*6XqL zuGHS%?$J}RsjKJ?FLe9%ZI9pYm#DL?w_Lq&bz5+xG&VL$ZEbBH>TGM~Ts_0GA;2iT zy}dH%>FH_mvQxAaLT_2ip-LLy@Vp!h2BjM}Zou;LvWFmnDG@3RVqh3D(KjR#1{?{x zqoYHHtrZj@xcU=kBp4GVc7&%y%Ga6L72}iEiLsZ zWrer^tRx8Aik)}tAozTV%&S}hCINTu_)bEANs=J{0M{`?5sjXjtN;K207*qoM6N<$ Ef~W|KkN^Mx literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-512@2x.png b/desktop/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..98983adf53926d776ce43edb59bde0d98989dce6 GIT binary patch literal 26898 zcmbq*2Ut^C*X|Bgr8*W+R4k)7w%}Mm5rUN&QPHuWQi7m@1p=Z{QxrQQIu=w!6hy{? zpddvc2_m2(AfVDoDAFOJlX~ttNkHen_y3>!-TOU!j(GOjd$0bkwe~(2w{Ef08Z&te zLP+b+Ki2()kP7@&1&vaJ|Inw-4?@VT;?H%fclcIz)aYETJwfGm+c=N`0evaAFn3Qy|41`TsCUl^I+!tu5nV;_qDy$rPoL^eoZe*6p5(x__9ssNbbBo zKa9yDMK%wL%oB4BlweoyD;F3N;J^Cte}wRTA_o3Da>D=k@BaD!cARY1|BLy4TK#`q zcX*uf!ra_k=c;6Pk<`!4ZSDput8hB$_uqdBUDflRhVj(Y)B=5HFL%k+G(63031%k! zX=77F!zAgo{V#5tP#k-C&4m#LH8nLNscS3GY~{*8U(dOsPu*&U%HmCNyMiOke7e#T z=$+(b{>yHbBSaY(D_;F z;d+wRdd>%RVOub>#oB^esykgvxv40UGez?eofd7T%|<9n%xQ@Y7WWxZgCjkPZRjU@ zPBDd1MB!gG%vO(64WT5apls=sKj)@7?aq6|>dJChHIZgD=GhaJLH>p8c%H3ZjtUK@I+ zD#28gW5rk;g?eNSDX*D(hS~CrD2$?Pdz4PtV%E7d8)>2kntGBg7WV=lzyqRivKqXz zIL%d97H4PM5==8PgiXrmu3DkFn;Dqq0|PAgi@d3=1(K9n|MnY_9g#ufWLytsuD|6t zCG}{ks_j`)}4Es zk;TaxP3@NvG4}&;^gW+izn8w<^-WH&%?nW$Rag#@_VPGqz|3?7utdfKsr|Ixt zz=*jw`Zi&?AqL@;ULVgjvd8N=-}b&aerUm-SC0gR$QU{8h0V?O$mq^3$@o%~^#PX^ z&W{`7m{YCYjP=121;#Mya5YWT=~b|-Y`oM+Jmuc7kT%gbdVc?SlV*;Tk^PXF=|h>A zTaM_0wxsfXJdc4of!yIAfT|sJZ#g3i#IJ@SR|RKTLDv;hA_u{aN0FM#44>_ayY%qk z3}6pVIJs|O8tD1ZmspGM&B}`r*R{k%Vr%cU{%CDR1#kyf13l`J(yA(hFfz@rw-Bb$ zMDq?GJ-PrVD|Mjq$kcmsffbv0>pA-Fd%pYS%#~ni`f6xh&i!7S6jA7vD=SSO%ETQ- zS^p>)J3f^wfeoH%7XNrR!w{&9`jR3d`_!Cxd9riw$OdD6Gymo4G%fyapz==ns{_{d zf!#SbQ)@U`f6AFouD@!iG4xiC`8jaUbXe7o)o-IGO=H1`VU828^TO4TEt&c{odA*x zKhgee8K;XJLy=6qr{KeZW7N_dy8o|Ef}ZadQK!>QkT-ln=KajAl>`@3Jswa zhmGm$_W)AI%%oOfFGH!%j}-~cOj|*W@RR?gf3qG&miB~2$HwNJcSG#sLw&KNsYl9Q zyzW(~82RxdivJ4kfiQ=r?4`)*siu}z&U!tEFXCqrdNS=I3V**zO@H&|Zm7y}A0LrZ zwlz8w9b#aT`|3cxy*OGf2V!Z7kFT$3;mY;}#8Jo9QRfX>EibvsBXMMzF~vR~{+Z<>0gJ0%xMKc=zb_-9Ttl=9EJWS`Sg@>OA&zO$3s#_ioH zX#OFbm_TZ20AUeJd@NK&A94L)8w%_GkQqOybjAvqCLAy3R8_Ut|4@sKH71#Q+`dv9 zA9ojInUR$*E$NlM-dj+ZKcYDsIX?UM{|a1qu0LhU_z#GYx!j|=hW@5G{l7ai;7 z{WR!IpFUgA9Le*7Z5RDmHJ-WcS{_03Ut4K=7SB8f_#$9T)K zP6t?!{SbnD=z;uOf}dCU?Ray9&juf#6rGvX1moEy`BIC4m!bb2a`M|AS3g3!8FKze z>xxfFF**GDZb)9CwZcX9MW_KogVVd-%$GUGI&DU>gP7>>=1UGMB1|Fl7PF8I8#dH~ zgfIJNc4ZqQx6qx>cBOrN_hXucjJmqI)Si#?sk&+?F6CrrCEuHV(ovzUQIzp*&4jXe zGB1d)&EV$(D}hxqZkmXH2bb%WP$U5(H3lHnnW1podQ^?MnVNj2sH^ucHh&cR(W5YF z8VIXI=K8_=2hY#cG|n8atIQZ}mv-#Hce2p6Ic8X2HuyP?Y0>skW~@!|YRLV_>MQzb zv)98t-W=NCzj%5r!Si!TinKL`zLVL{`7t8oZ>A3S2w8MDOldNja<8_wb|Xhf`7!&g zJ+`*Cfei*6{%WvsZMUyacQ!1N%7YU8nJq%qgsbsVskABXhcyH^-H29Q{?gu`+W5D1An=%!2s|#&@Jk%= zu2GbOLZR@jjk3SWgC`Ip!1?d`K*2hHXyrfeqY*?g+fU&p#A?FUeW1C=yF-VCY%opL z7+uJqNNgz5AI9BRP{n3jJ}0wkXQ8@(5mRSNcsuo9|MrYnw+$&X{;Ggr_`UXLe)U`@ ztNuY>rKIa2`7t&1LpsG@gr=CH`A55!DMlLYA}6-;I@Iyp-RskGySWlqDj^i zF**6@ogehZGrjT`rmVBE2_;xw@ec@aadUfLs4;A#ww#u(t}84|#L@{x`*9QdA)s<2 zhglw#NgrVQIp9+j-s&0gj>(G>4Z?gcb#{nJt!dXt=qygzOVxSo1s>>}mO*X};p(Zq zaT7HrE=U;hfqh>GwQ9VJ%okH?Gd$HTFFJVTOjzzQObA)KB{5QR)K_KtM@b?*+=P<+ zf;{1i8KgP<<-=MBmY} zi18eTcm8|0P}p=*K0H-1T=hZ+Pw9P=Hsc;s@&hbFRF3xA;zQUE%)hnJZUhj-Gku## zm0o(5WO5|Ia$B`;L&!*Y0Sup;|gVnhq3N8uX?%2(u zNm9+w<1D%H{>xw4l9zgsW$ddL|{B^={{bburE6`44n(ZD*P(E zhC{30;ASErkb6oUH_PwfeMgA4W>QP@k%@cde)QQRrRHS#h?p6NPUKDX(ML(!A?=h( z@$5tfzp35ZJ$Gl99`^J!2G_5NDi3s4zHV54RPnS}FFRtUj2Q&WaL6SNoKtA5EEQPq zIauRU1kPkY{LR^4g{|Nq?0wDHM^>oh^;3dn@V~?Y%IhH_{+%07xNLydo$$a?dXJ5O z+ou&Nn?ebnj*D>Zl<^C!fb(9um~jUNdPU3Ak6@;51xd}gv=eu@IGgaF`-0F8L*?78 z19-%d=dGZHvqX95;2Q}C_9Y)CT(ck3XFrd^)H`^*$bCP;f0WlXYO@*V4%pM5f22tD zzAqnRkUVu5&(`0;OxX?Xt+>-0Eo`PsU1f1iv5@sXCnq+VK1Vu$c^R1C3*nZiaUcOe zz84Q&7;R&7uv~_VWCXa`s|5uGEdgXR&R;;jeC0}|%)Vra@v2_(n0|kA97}0`1&E&~ z50dhvdEs>?lj#+T)~%pPi*nr+ilU)ANjk-81`p{Q7=+)7#Joq+vf!$>kUpX8-~Zv_$WOBV1VN6 zl@|{PDKrlN<&S`j7c&bR5*ES3v?Sqqx{d;!;4q0Jp5y(;*>9K{r;CP zUxq+?&AlqQ0(U-g)p@@r{e_qJBy?GZNpX<`Gu`e5ouB6KRt=Wzw$eE}xnVe`>C5BY|GJsB(? zeiC-5<=M++o058)krr}10tFzTWLPD$Bc4BsOgBLnzDboBT2&(nZn4Rw!I^9?BH?IV z)mZoH5rfv)_9;?HQ<>pgGYc*0$WUbFV7Va~(+Icsq0gtAXCD1A94Zpap*mGd;6g}K zSdi*k#$hd_-LHo_G9b5CfU1yeBH;!F@wq`dpnnSflf_@a_jV~W{4*_ZN2D;CC`zMX z6dU}I^h>gP6~3H>ib60mtj`O}1HLIUUcOxXQ}9^du)ti{60L>;w6wGe682S%_%2~n zi)C5HO#_50e(FycF!75o&p{u(GvG*nXprjk9>D~LBolr9kxA3WXdf{(0u!nmi+-}v zDB{=2sQl*{FRn5qwD5R_DPBLal6AE#72PhB@RQf+3+bSbo=z2*XU{@%GnM(2zX#ES zUC*94DaZby+U z(#xJ&s(wo#AOIuJ;&oQzVSedjC2A}c882Uc@L9?}MScuJ!$rsmkj_MBT;1i)`okL@ zpU>9}MZ??DxGIpQImVwSpy9Grv8{_SVqcNNBV4X7%IIi09{y#%aAS_X&< z;6NegzpgRWfCa4z4VTplmLc{Jv>c;E)dJoP1M`2_LU=rq%l5@6Y5w$-`ot2J9?6wd z=OvueLY|((FNZv_B*WuWIc4zYyw7C_{Ybr_PO%Inc!_is1K@p{7CKt3z;MC2XCevj z6l8+Qf3+QS zL{KmrTiEZ&`oO=*$VnlYuX=2RTB@Kd&AWLyW_~>doEv!fBT@;V^xk;86c2Z4z;l^iL|o|V>e`*; zwyl>J;O{>W!HS_5HGeI_{ktFvonM}D|h@m_~dd8SL?$uV) z{Py=+dQ~2ZT}Bj_l9u#CVip}AA1^S{QX9}7oj}8}+~Dhy9O_ECU52k$uZ__4vExdz zHM#K-X^=x_^`>Z+@drAqw9tE(>@9l2`MaK9xH;eU(hkcq2OkGKojCPQh%0Y{t;0}c zKv$Yq-C4a#G{&vq?lP~^%_b}8`EHWQke=V5W!DDOMM{T~Nn_}?mXq;xpz{1a353)7 zx1?J{a#xjfuGE2WXw=nngd24EJN^w@QUvt2?U5xf_n+*lo@DFjcK}dI4ipsd(k~_F zk)4>B-tnph!WH4h9{_!ds9oL#dCGHnpt_!QyDLKiAf@xF^=rPk-MRf+TyRlf(-MnO zGFejR&vY1yrAU5@*D_eXRGpxYHoe;v9-?J7$1@+V^E!xKr!UK>J7-WWY-Yo9){XRh zER5MVTMwJ`ht~G|r;)4*D+5%X;BvUDe5i^gse&Z2amMRSn`=TE@E7c2m^%;O^XO-N zzhyTdS+L|{CXq|J7|K;P=?w|FabFL2Y5XB1j*xMQE5fH|EHr6dZ|5b5dQw%p|1 zO^33;n<`=tbv2hF`9@jB(Q%TTvqAt1mv_0lCjc#|srS6; zD+ip61ztWE7JNiM0*&N{}U;oy~4PS3Kstrj8SsZn!1jU2l zP;Ru};rErW<}s(~W2fy4`_>@OV^6?i!y6{` z4q{u|!}Kgwz0^)aA7pBpGlMR|Lb0LHTA?9rvT=>-RhqM!Q5bHR8|qjRR`dOUjIkEt zrA}`&9z1WRq{Wl*x7KfCNqo9(D%A?jc&iN0T(`z;H+l+3nm%OYu3SqvWd@}J?bawB z8S5v>^v(EW)%G!XfG;ohY8!r7~oxQ(;4`!o z4i-Y+h-#opx8E<^uS=WyR_5tBB`s}jJu`G`8_p0ILF1gjK#{2$J~1uRueWLRd)kTP z$9J=EH{CAQYNmVxZ<}_sy49VhOW|vbm2FG7VFqVFd)w!{fH?4#>PBpthafZfOBmW) zwpy-TyH-AI;^+78FX#|R{pazek_yYqZ}-b^oa&BeUjs+&Wl>9?T@aKw$UE zUTvVSFY{Xu8RyQQr;f5rRc3!scaq=;&|q4ra(ftXE5ty17bKdB*^*AQ?=n=95$kc` z7Bw-!y=r;83@An4W_{TnUq*!v2khVs1&bq#T>Dh>4kv`-2B9U0gj8{$s3n*)S|^mi z=noD6_gv=4xjeShtWcXX&s(Xf-f!`$|8u>bj<#9Z9B zn3`t2vzhZP&dwEHv>bi3JRvI|dCAO9Yg{5tBK!+dc}XRy;m%)uA(ynfN_Ek5nXSE@ z@#O1aubLBOdt^*E`2I!%Dy;9+PxS}m(M!A5)uMNjf|@`dg1z7On--(k5VuFyKcGlB zuMPyuxv;G;QYEAHad@CE7!NKL(*(#j@bbUAoRo_(>8ND7Y}KRH31pB`w-LR^NG_^_ z+`p|CU979yXQ-m!W10Dqq3%$6#tw=kh1MAfCbWY8QZh)#1|>_UI|1%c>3&L)l7_N$ zatkBO;$)#LB!l*~d99)Fsu_X*iS%_EIL{da=DnC1tJh3k)iG(9!QSH<@jU|VX6^8X zfO)QbFd5^BGCAH=B$-_=o2JfhhpsX#uq~6K1SR*{fl$cDwxyBP{|I6Lh%;0hCj%zG8|&7J41|e)OC~WW$ip-QEME55w4J=zD9ZGi0oL4w1LD!@er?bd%ov4Kv|x@Z-1LP&o8v!ZhBP;1T@`lQhwDPMbD5xWahb zY@>Ugr**RA_Er0cB0N1q^5pbXvy;HR+yw?ExcTB-Ym@1UJlq4A0p-B|&bF9LU5pa; zDT9n<2~@5=NQBTXPZv}*0o{RV;qOD|75FVzg(%|Iiv+;CawibXv}{raT<5817HaIb z0FbC;WC&y<{E?9%x1JzJNACjxoT?5Z*;Aj?(Md;cqw@dukidXPTP)Z)%J|w*#H#{8 zsT#vAU8OrZKWz+umNojGJ-DfXV%b&}dDhGOmOpO=cJr@&BG{&jF|h3?`_q2ngKGzG2|o5hs$;jt+j(ryU+((qDA0T+l@qU-;*%tKh*D zIzdi(){zs266Xb`1A?Z=U`?5|77xyxgg zMk_ITU6p}*^NAW)0%q*`WO#(upyQ;^Kek}S%UW^bp#ZDum(kJPj;Ce?8ed^S2b9|w zo#D@s*gyB>-L4W7YT31`E&X3|v7^;vopiZF8hCQ!l(%!GUc}PLdXlzMi%|^IR6UKy zq%sn}aChlYPy=yywi;qgwY^cNN;g?Rdz&IfDgvRBkZr+vyHv44FOIU%DrdR9T?`naC%vc>w(A$YKJ=&Kh>VqELPT~}55mSWZx znvC1xqEWRy;i1cFbrSNs^%7{1*)gt%u5l2PdkyK0L3sAPm;E(XoFEtsPcG^{g$g6H zr$XSvZUe}45GyAY9Wb}q)#Qcpps<5H>&LFA)p2yFy2zb5j%yt&wQGnEShYmc^!#!J zJujQkzGQllSoz(JjB(tj+wXgdRiE#JoqfprlHOu#mDy=H{qaBlx zgM+vDSE>_?a~8R2qGOYoZ#oSX6phB2t3zlo75%>ss94;`@ctk&?5sNXJ> z@Lv|)^e4fi!bmO1n&SDLB&6Pad0e}N3R{o}QmSWBSDHC`WSnW+W2nMjYS(k$zm;}2 zX9uZd2SDqm2_)cp(`g3Ju;aG}W{-~46YI|q>2>D}TNt}b02<{H`x1bk#Necg-s6K~! zF-TnT8%d(Rb*Y0@=Sg4;$5R~;r?9Q+_Zll!%UX27*+T6zwa+7uJe*AKb;uYR^{EBIl75k))PQjGVkd^t{_c8j902_3v7fo2o?39gY+mO|3M_h0m zzBc4%&>uyN2I5u$48xN=Py#710dm1si%KFw<7+a0^S1}xx<`UMvta}0azR99-g%_E z9-GYw1G{f8LwGe5pG1Zngi46v?Iz0$^G~}(4$eU1?aQ{MPo+c5kY%+R=$b^d*YTtn zawRO4~HlkcAS|I#m%;Fg>N`@QL(|jQ1_BXevE|{7{}t zA7H6XGkXiF;hAkyfYU%7O(Y}%5sTBzQ0?!nDb~uLEoP-_oBfmiTILn)r$iC!!Vvm+ zgzM#VxNeFyKp8w|&O_*xiFnT1y`%|EvE0vLAPv|{{%RdGZ5cvypJwH8+J@T2WALy9 z(-%iUQx245{8sAVCzHTUAd%?+WI?c_LkVczSDI9(c{iTY1EDMCQgv7but#IN!9>cy zrv%rx#d^@ihUp(wOEXh|2T{9IFiK@@9lnAwM5#68y+=CE-tiI*tiVm8C7`i$adK7b z0@L$2_zPhsW%XmtQ!P}$T6)=LQ-dO09^!!cXkUyu^+^975w&(a zor#90029KJ`yDW@z2PYK()5PdBB_~lB@0R?P%(lhn@Q@XEk*>w;{t92rN8t!cgkb{ z0n3HIFQb-Gy!DXfriMh!!{DTyDbx#aO7Hy5q>_BY^^}x0!TFIl%BUI6G0Ex{cV01{ z87QMrCFoGZg+xm1dTXM=D&%{3!V`6;(`Ot`KPdQ&Dv$0j+voKmlf(=1)Z8GKMSfr3 z)PQ{l@3H#z0zHxY4vzT}DVJ_Iu3wK+pUvMivOJTy)~4`^O|8h=!jiFE&>|N*0nPQHhNfivd@q)o~q&udE2*nEu$1G^Qs+< zbmRP*u;-Mi&Gnqc`ZD+tP0Z;C_t>_1En;aS-Ru5U*m>;5l5>$OtIyZn@$AciATUGt zS2eRWhk<>6;ZiTm1y}3o_H7lva43jkIL;+K;_*r;C>-R)9(^%WsJ13*Xsu1bl?lCh zQO)Je)E1;}JTznn2e*_%%>J2j8b3b(3L4X1H5in#3|j#Hc~^$df{Bs*=Lp3ofIzE@ zeGBQ_*w8I2)XdayvEoAiLM^@ME-bC+{>%P&N$JbRcMi^I+xHaF;4(Od#2G4UOPoT- zEgs})BK(`oIUXe<^8{%_pV-@8CM{NJajzcB?M4sg=}FdxgbCfLL+%A8B392NdL9|v zqs*E=BvbsiafRwo6Z*BrisxZ5=c@GhGAYx$&^n5+Z6LDSz*l@h-$T4|qd$`***Jzf zMMI`Ne+iha?@T2XX1afbj(N0?Jzt&B%E|1BEPdbvK3++A@l;|*)qLu8lK4`vsBarYT@^ytYECaJ`Evo0li+tpu53gNqkazm^s9D2> zJOV^e+4^#n3FT1$kc$2w)YIV5a4v*mW2)!gQYuV ztI4Zk;Dc}B!oV8 z#z`4QwJp|wT@6hBkH2c*P*U9pe-Vk?`eyeMzyS?!Yw4VS{W=|f8^+52eh+^#^*=+= zgd_S?8Em%azWd){GsK#oF*O3X4d~i_mHZ^+zhLu!!PE#K^^|*T$U2mq47G3s-=JhP z4Rqqa!seELVQK|Ep2sdSQMr{ej>}Wo7v*OViCgO;2`a;w{WJtxWQrehR7H6*Sf?yJ zz3eg*q{|qmIEngC`DDOb@WxMNbZ+BXT#JyU>Xj@QDlc6W>&Yt9FjTQaRlEp+4~1GX znwLPwr?GF-07cNHspXi-1-J+H^ceso;UBg|C4c~^G~ro>J`a+|IFf!c~{E(phJsGm-#CuRb9?QCp2-UeFuWkmUx}cOY91c!N z{u)kL0^VAHA_s#ihXd=$_|kY$9gn2K(@+J#Q4tPu`ekI{bSM^9WlJk$fJQO5JnAdv z9-cwzTRbi=Q#fh~d;@GOtGR2)4Ny@8$M-CQJPBUy_8vpN$;2h#HnVi0Y@w%&tw2v1`=h}KHNs}4Skd8 z6YK{XE5qjx6iTH~2b@rxEi!%h3QUj(U>WVQq6Y5pfp#crsLIGspeZ`1IQd#~^;N8< zCMU624@YaL9rp|!(ZdxIJj?_u0u}5>RwTitgYN_1u96zK0!BX=1o#kKh*_tG&o|@D z48jdS_Orts2vaOYz@0P~Zg&(uEDnRJ$MU1Nm~b8y-~QLHa?d^-6y^R}UiRrz{oN3P zAJhY7^Z(03|EPKpKqc|8^`DkwfD8X|AVH9mEX4~mS2XVV{wL7~xcFZplTc}bbdsf_ z3Ka#;0ne0m*uo!S1Xm(O20ao;p!xH2Ady7}<>zE@zVhHBtG$EAmX%VrSb$c-&jTg! zy-=f<9m7Er$|3cGJ1iQb9sqVnM$>Q+L+R_3Jd(-#?DAm1{%{_AlZK`0KoPz>>z_P# zLT}Fuxlfg?g*yju(LfYV2-Lz5+QAi9K%{`flDOdIfep{d{+qx94jW#YCrej@*Mgpm zyd{q>Bd;B;mNoH59s^WDXe$Ad11TsM?p(6)?(VStYv3VN;hI(P2r7T_Egp*RIG1Ft zP~08ksju8?<537F0|1{bcw<=%k@Y@h@d%rU%@>YHvK6y%9)qKg!q}a#tz4qpi&A?B}?gK@&wQF!!x6S zA~}XeQ-X^1fcBAS4m%oQzD$8-ZXcgd$q75EkeF;k!_FM%zQTpcbO0Bdd?oNdFF#LI zY)D4z&+J!_6eq(Mpuq94*DB~x$=>?*Ly7*)B8a+*AcCV3$r719fZaOvt+!J|wVwhM z1nD1201b3tIl@Ial1@N0RB%P+=YVkv1^ZV_0nXAPdB@dyIF87M4Ij{vZ3&-)W6GcCT6h^;w%#S_c=zS#ED1yOo zSd$mvn(LPpr9p*2& zx0|CI_~ymbFI{DlqUJ)H-~{HQ9Gcm`6I=D_$gVGU>Ds~9gwLUne)4kX{Z{3Z&KGgY zWZ{3)>-#Q|6vviW*2$9@<0HHs@9{A^jVsiy2hzcF_gAh`Gc=15l!**UGY|LgS);g1 z_uwgx{@gHCEXy$=W$)lN;!>>RW|ywM|FM%tsg0dpgV_}CFsO4Wp`DWe!!W+>{hV4( zYIx^R9Pl<#lzD>wCMI3GBob=$7fokJ@#<0x)XGV+13Fhlcsd?+ps1Ud*%JB5T~azj zOUZGCW~yJy`&^Og72U7WNqCSxWse7a-Gzc=?DU|yxqHfB$`Spmj>f**U33#(d)b2Z z0*UB`FCOHtCqC+Srwgy=pQU#+s|K~&_>Ko_lKdFPotZXW0r<+8d{7 zU>Y|5Z;q!3@_wz=T&xIR{E{>D}bJZeKPmSP_C8Lynt?O zW9t@104=~3?p$@~LIV`is4=F{A31&dbEkArdUD~erHlLpclt`t(Wz|>A!al0yNM;! z3`e1TY-QaXUlj=S+7dgAWzm1Wg2^TBT|WJZm2E@u1=7#}aYLL%zPdAZ8OKO)Gz&#- zBDIGx`+d3%)g>MbGt%!R;%YpDgvVR^@!&e!t4?(PN<5YSvT0%0yBTmf%Td?2OT5@n ztzvS;viuxnxatvFBGH`Ay@j1j&1kkNFjHET#%rePFVaoiUUdR0>(If1iz65Q#xgi% z7DK5NN%RSx>K~efd%JN&sIkKfA{rx@z7|gtW&hwL@mELVKbPd%h|`8BJYDYFK}os# z>g8?eu5Wx1T!m3XTXTc4XC(faU~GB%gT-if;M+v)Eg?bIHQx0Y__ulLVPIBDi=SDb!*@CzO`?Zs9%=`;CH2T66@8>c|yQ&y)#B8WuLqVeo5GO#Aa zw@}f#TpkRy1lVo4x^MaYwZN%TsRg6&na2I!VxFJ7e=jZ`pdOyfp>6y~VY(2?ZmY+$ zcgC)&vzv{FAJtcm5PXe8^e|dmyruaS{~5Q63{QAQ9Y1HlLJ#7vTZ3zXZ9Zt4#x#=p z7wb5Pt%|3q`o1aNAF6ZuuSvaPQYr&IcK`+|VdCo0DDT2xiwkpWV{w#WQGs$6R zNXN=g_5GWEQ&4>27R%>(cRkBBP*?SJqj`Tzhwq21C;Q)Ashs+CD*E)wGXnZ^IE=6y z&rieh1Mc^&Ue`^Z0Ql;dLG2`{d3JhZXVTG>2YX+94E>^MA>9@HZS))=eizlZEApRj z4rnL!@$uSwVMmjE=S-1fkjEUn&|XbX)qn#3x^PJJIb)(_i*UI+bZzNeD~mIpdZ@G1 z`;#|h&ID9urQY;4D|LvH2!>S|hs}OHtp0PsD$;e`kHa!QAVL#h6Y=>F!@Dp(UA=Yc<8pf zGG3i$3$;SQ=#vaNm~=3<3rjqY&8UBdeKhcY)aoD75F#s%>?Ht&o`h&U3sC}^iO{o8j3>Dox za$m&IS**e}a%$wzJEL{)WN5J9OtsJ1qUnhmpZcQGTBT{r+I_Gq;k|#&#sbQGSUEOm zrS$D1qPS>9^Ayg23x>C3ayM_PufphQ70p-gcB z%o69b;9g6=T`@J$3=*xRl3p9Dz-Ir*wT(kF*{*RMrMo80Ieak*cwnG@A|GoeVS=U< z%t^A#2kU@7k4an~oh7|K16^ydDcIbfQt*v3q>4fzBCOHC@6IrLZpNn$sf%3k!yAX5 z@TfJeR|PpupRX@^^R^c|i`R_Z0dclJSR`dh`{M(O4O1<~;A1aN55fKP7S$;al3<8$l5PC*NkO%V!K(wi93W%ti;6aa7PG7XGsOK24O1iIzg1q zj8zx~p>?ZS5|XGRk*b?&@fIM~W;;oh4^?gFinP0#dBT1%JypU1s-hooq@oWb`7CFu zu#0cS;~(@J+{Yr%h_uiYda=m>)`=Nn-G;#zguiM#Z+O@1NZz zZ`&gRdp=mG$YGERLVhb6%=B<_^?j)SFir%t>+@VI7!;lQLFf8?of2u_k@{;z`$ zfS0+@o`}b->C#2n65#_nh{j#=&(*GI#{^o9)bKv$BVNy$*$PLUdK6ntswrJ0eMtjm z)CuVAdT%>197Qui%mPI^sS=iI7p<{#IaFI-lc!FddfHjOdZ)BA-9j0BhluVg!ds!e^Whi{&X&sKzp#|5oyzfgLcqkElQ+7UecK=u$%|YvAclMs$$DA`uple0le%CTYgkMcA#y=fS|>q?d(VCprOaAll9FQoR*01HJMWn z7Xe<;a3INgxi@aks3|SZ9Um$z&is~~kzN^ES^d)GMF@V$d<}df!QK##|0j5r&I_$> zSdu_H6kWK2xKsF@Ue}4oJy;n|Vdd7$fkQ)Y8A03k+4GsP^CTQPNHfF=RiAK{MDb2( z>F8(_(Q4?J)Z#e0^@xFw`wXK#?xL+z8;>T^GdecCXY4cJ?(^g%rMbl?irDSgJ zbbnh3my1k|*FU6<<_g>6^>iFvhW_+HNmca#NTECaOx*05cSYME2s)XASBp5(dZP5L z`s(eK6E6uJiANv-&7l+AWe3UXCbXq<>J2J><8y3iCCT5)za_)H7^vrOVZ*(ujsuLP zDW~ECzt5K+_%nU<+p)sZ!%?`{fyrr;7?j}7Pb9@Ncd_|C9PKVg_zm57mEAIh^gJPi%!Ep7Eh0=pA21M36F zzpI4Pw@#OiZMqhp(X(;e!+q^=B&73qpMm>BhCJ+{PE?&V*JiVJdeyrnbuDHedQ(VP zFKKY~jxXTB{mQIx2^zhKk1vWw5S=+)0?dsUht*hRa)U;3pq&~;X{Y`2IU0SveqTha zELy!6;Ewi|**ChB%HN!L_P%Z`<7v;DT3vG$}hf+xdJL+MvpL&pebK{K~B~4zo?`r$>gLtF2q>3GLYA zFOkyL$lSF$=8Zw`?GFdG#)Q3*Il`$6srNg?y+wni3_xbA`Xv{tDIZRDa~9b~^sJ>w zLL;^A28wv#k~&T4qHL0QElKOPz(y)5Hk538SXeAOBN@Bt%i-5UpU+z~ZeiH2jOq@l zsk0JJd292^%k9>^-Jv-=|6KL40pNgt&pm1OtC@67()T#Nq2Rq$^=Mr`S}#@{N=w?x z65=ik&Ut3fVBc}Q-x`7H7BS}O)7tp@{v`$#cJAGyi&FbZj%IG)^*jm1wKLc}X&2Am z$g;8j6AlGB>kT1{_-v|ZK(wiBG<_aFZb95vI2@ImgN?JeP-ZU{-5d>Zj<>^Hn_EVh z8Jr?X#}5-43}O_X)VX~DPbC!Fa!ze!`M&83=^f+8M7>?EcWAcJmf4?@E^hW|?fCW5 z*)wYl1F?-gR-w-Kke6i_WEGz1*?r z)_Ch7DS0jIyBje>G|9X+?*6c8hqF*)<)-FA-?o(E)-7F_5EsMKWai!e~xwLs3ofe(3%eX@{nFvMvr6} zV}+kr^o$NRjiqhBE?Qa~dfiAAR_9e!%bJMhrGpC@`0nzCc9lY@^_18-ewmA9 z(diN!TmKoGq}lP91zz4NPPwFk3gJZ15@qeoZa?B0(;=Pf*zG3wIbD*$NzaLuQN)SJ zzWPSrifrx6&(6IrTRFO-kv?ES~0t^ zSJsVKfKpSRY~>V}QM2Ns%>1R4cIm*14NZ?d9JQ^+ zPN}5PWb1L;2V~mH_?9vf70dT7HhA&u4w|XI{qCDhw2?Kcr}8?L)Oys z{(cL|Lw>^V!Fx3b2Qr$?IuDlje071N$^;id)Ms~AOWztfqr-Go1P&$O1nz7PPu{}n zi@;`W35iLZuASk(?{Xtu&u{Sq*Y1Fq;#WS-+5MC>7U^Ku7mOvT2LogJ+G7`K>dp>m2nETw~mxMjN zqY3LaPZNX{`Xjz%jNHl|Q%mDd9nwR(H))OJl|%7eu!Iw}d-(AH)u*}Wtc@!HLVIl$ zeUNCY9|;CptcROIpLyNG;+!>WcJMy4mI~r~sTlZiD0kqL8&De>TnUe?n1rw0{Bpg$ zHgNS(G3f@5j?KglCB{d6+JSA-*c$V?mf&H97SinfuZU_V+vq=-r)_A)>VHpaSOSG8 zp)*8+5N2dc-P&ODiMkst1Xv+)Wk?ZYaQIR8lTg)7HmWGop@2Gx1d zb6weB5x7@c?z?`WhpMS(;~G7}`0`Am7N5%pZ3t18Ms{^@$cEz zJB)q1udQdq5$)T~GFIFca|VL9R6&CZ*cP^;xT4|^fon|`{yjg7n`6}>*%`hb-Cln_ zG=g(iY-o}EdFV1;KwjrS6Iv!=8&=>oJ*9*1`SXJhn+x(>jUlM?#=^P$ZNQlc^LM3IYB}D^S{ohg60iKx zX~P_Bkm-9UjxI3S(2grkzBr@^zW7!*5otOGK8?c$a=vlamMzcJ*LC$S!D76)@6YDq z71X|%Z`6gzi9fG+re$x9>dQJ|N`}7iW*15zv;Q`$>`J6-pjrmKIIMEeeLwEL+?I#G zJl^`3?SI`-G*ng}9N2T#2LJ<=F^qSi-XYv7kqIbc3tM;_-~YvD3Yuq^90su8k+_w$o~_ z5mf>Ug9)+aOW<2b+ohX9nz-Ko^@!>2?tVv=as86#Tk{saKTywZGXX%`rBS^*7Vj6Mm{0-69dRz_tmzglO=|eM9$(GmVK8ky&;i# z8xH85ofa9kV9zhpl=Tho5x5Tf_%jP7i%+RLW*Se+kJROQT0Zp{y15><-!Aqg!8@P~ zH)1JFsu{@8av)5YxRH+HG^AO{B}!;sdKR*6ip$gIV>6#O{OYI1wMeq|8l0 zNT;zyG_=6L{~yX&#y%` zaQWj}+C5Fjcb9o;zHZ08st0g(dbW z#+-ku)tvaw15^AZr-#CuS2nbU5qq4s`W|}OX2xo{T-8I;7G%p*`9K&Eol#|Y>ORN{ zz8zYF1dl!v4zD|9*ghEI8hzaDKs% zUHewj;GpRAMJAZ=WUB>aW`;sRwG2%g7emMO)}Rzyvj~~0v$^ZjM0+(aY@E^GLN*3br0(L~%Q9l};U8Oud zt?BL&` z$jZ@L>|&<;ci--;t{h(SMCFnw%fZ<^|7*@&DgCjmh-w|KXo9fKEYUw8pj4;RgK+yw zJYCX$jfhg$XgTgkrL0_GA^G$CsN4@zm8iBEE~}j;&JkS~vL zw3^|!o0&XFMs42*KfUUTmWCJXeslcE{IA@zxrWz0rnVds4V%$^qk2*4fZMh^H4Hwr}uzG^Web1U=-D&D<2;}e~w5z=rx zMf)<7wmPuyD$b1W!317YFoEtN;XFR6nKV&rZGZVNf{ybje}@0Xy*jm>Dkzz@|% zs1ZYC$j=>4j}N*ZIWoU*R;LDa)$whH5#0I>E;7xS2pYOENy6f5Adc``O2%4%G@Z|0 z;&A{^Z*Dx}!Dae~#H2NbMw!kzV{Bh#!ROLc!K*^Pv7T&(3Rk0Y2oq0KHpbp{*Mf<{ksH$q=LUHPJ*#8TQu#1fN9oE1Nv>JudQ?8Xvey zsHK6L|AAY6J?`W003a>ackOR*AFLr`=xQcAv57C&AC zoygQ;bHCsJ3kME4I1n46<|jy4OW{cdJQ?{M+FxSkfeI=4t}5f73U{OiM=)@xbgXY- z{s8vZLiFYPaugJnE88+;Mt;Iu2SoHI6H%L|0V6Bl{0A-YFRg7 z3FLn8^%h9xz=agPPEJlfdi3b?<9Y{LPokuhlsqo?L9hXGS<21Cp9jN7>u&bEcHEjv zZ-MVlF1N3ClL0|D{NZ`{Bvj@;g4310zP@$R=Q*EpLIO78nuKS_oU|KFQG&<>WPF|} zs^P(#uqR4{s*G1t)ZF;5 z;>P3pi)SuRHgLqn^td#)LuBlgxf*}R_3PJ*IUHU+j=ANZ#PRtSa{2N)D=VvVg0ap@ zGqch$o0{sOMe7F#q9ZoL9o52*s{}-ek|i)KW$7?I5G{n~tgxK{ga`5QMEFF){j^js zo8Ry}>o$IyIh&!=Y2qWc3z_LaJc#RS3}R2Y4k2&dtfz*ukPqpiAOA;K5n^!H>Bl z%$<^c($hD}(+~RUF|wWs!4B|soO|HNw7~>vKdZzH{T)tWt_Z(<+btD8v?kjixlghb z_o;lo=K*ZC-i{TuI`pBn*XamdR|kzbl*VI<=1GWqT2&cvUs_*ZAJoF_IcPf$$c6Vm zz$aW2;e(Dl)BjgvS02^WmBw!p(%6Xu%t3;TwhqTeU@-Se2!b zrAG=;2no1=G88&k#3EqpLM=r=RM6BUqLtzn5DaU4prEn@5+IO}kj!@zu=bChGn|~q zcHdpT<#)e(-*St5jV0ig2%gO>*CuCvp}g)+LyAz zX2V;qiRurtg_K+;3(3r7ko82CVNfb%WSd*v(L3-YrX+C1%wK&2XW^DSu&HgpzUjqN zG(IQdKg?g;)!@gd>)4~7B1!6P8A~7xpO=^p^RA#Q7djsp#dc$e0YRL5A^_S)j=sk9 zc*tJ1{6Vs)CewA<0cV*d5fVUp*64#Eqa2t*w&o?6&wS&9o^~aRGW|w#>!)C+mNNeD z1_{tn6)U@>IbgCdGt2;Z^Ah?>ule*2N;Q40Ji-Snp6zY^6o7nU|)1Mx21(5 zOMX8LRKPo3QnJX0ujVciPGnvmKxx^PWK0W^78(NHVO9CK;x-U$@qF~9T3gY3K>cwK zs82GY-oV-7`18238cQRrVE=iAQ@qLvf##mI5vHplBn*ak!U6S*qd$)CGXoo7)9*p% z(yz_buZ5J@xVY<%NakcO16i=DSSEcCCq)LOlZzEWU_bGk+qLG<5q)YaHQTRaoVy5Ow#vb$ryOA~3L^qXYa z2r$k32AHiPFR>V8DaMcB0)xqKNbZa}eIcU9_2Des*#m}3By)@K&I9->>=uf%ERt-+ z|Bjw9EHNLX*&XQOR8c!lou$8o;f{PmR#R<$jR&dFd*OivQ2e4Lo{7d0%=QS@xepzaMLB!O9!;Q?rfc$-U;V}zZ*tTH?&() z$+IA>HscCbIFV~!WghKs+0`ediduHqROz4RRh$XTJkSIt3`yf!0ID^_{yJElsoE;# zUF>!;EKU}3O_hfo#*X9BrGx$c7=3@@s2Okf*GlK2ePOY&EO;{c;_wpwBzJbYWM4R+ z-~5AcCxD$co8%%G31-?LF2*3Az^l&3!&WqxKyS8(dccifRujpvu`2XIbcSw zMg(-`JZ}$=&T5v(QpAG%^P~j3ts*Sb!F+2G3cg%JF<}OXv-HN|omN@HCYE4>Nrk5Q zSR4UM7d{KjFrwx+xd0Y;a|*ySTgW^@Md=7hMCgea44{@BMOv7p(P*|+M(xNU7I zvA|@~C79Fuue~VX#m2_+J*sU*kFe3bC=^P(eMr86pJAW;mkWT&Gk4f&(N|LD&6yq! zjj`5-;00Qw!L1)n3nL>Pnfyd-=}$AsK8`#Slg#2J=^E${l(6&}Jj7y=wNfey<^6*j z6I1t)Zn11_N&e&Ft$9W<^>oqz^ky%gU$t`*SD=-?rLW}Yr)t0E*>;7u^_JCVaA2~f z<@|=cOU6tr-y15Wc-hq`<(HG_33q=GVHj1Fhd!9aJVZtRg_yl+_@PgQaUJRJlamyL zM+*3b`3?-P*@gF42DGC@WQi} zWw_QzMxDzBDgHFHi`;Z^N1ijtL2ZIg)GzaW4hw~!2QL&A90T^J+g8-TKA2Mt>~3E` zkn{O8Iy#kMRh06St8F+MCShQ}_R(1e{M3i1?HW_B~ z6u*4su?@`&)o&L8TTs}Ys5jM}UALwVN<8!2s|QpR8z#Uy?qT}8T{l7J!@*Py5g^D*z<|L52YbE};?!I>fk2?C zrZZVU&FTpv2FSoAeHyl0+JLQ70+ri4afiJlcwIm1Vmv92tw=$ zG7A1yiP$DhW`$qEsP6I~#TMzem@$|nX)95HV|Oa$&MiCksk#1%&2A^AH1cI@C2ZaS zL9xNNRSs3134G)FA{lK4XcTNk+tXK{#6a$7wIG@fi?{e3_99u)$*G$&K3Gij7Iuf2 zBFCVG{0$p7UX7!`&Vp|D@TB};<{XSQj}!}&nhTq8MfMMA>YG;C7DrdfUBSpE9@3hx z?F)+=c^O>D_dkUL2?)tDhWr%KKxI8JTth|-a^fVhe=qN8CKZ+0cToNY^FgFmJO8ES zFYY$mrm3;@7MENF3_8kYKq0UPuoVlC8Ou??<9Ti3enBEM%ax{RxR36>|SPY!gN2?G|9<0W=J2?tNTbf!7t^vK2v_+|j1#5aMeZ zHwW$MVI*T6y`$}>SYe2$XjCY2N_L37A2Ch(9H9ra@8OmAK%pW9u_t2Vbn2ajKT8tv z<_s9@OP>w;Qe#P^3BNR9bTsZQD82tbnVB^MN^u2+X4~x8aWB=fE`sB^;~Yc7A;Mb- z!u;gG0hwA|wu&Mk;fMmSgE!9UJpnI@_#On1cDK+mD~{SY5u73sS>2+s7&eMHD}Ezx zOn~$C^3ww(o~LutNw5BYM$>qfZ9YdZp!xtMgOW(nz>uKw3vrz6BzGhW+ddum>C)f# zTPJg}U_dE>zwr7IE%#bPqe<);^)W9@}Cqg zFv1ciW27=+yc{7Bt{L?L-(6tCK-`P4bE4~J3Y{Uq z>CvPFHwI20CqJc6vus{Gl=K9HYYQ+rlGh_brBac(B=lo|kIf@zvYt3xwFkm9PCy+z zR)!UFe(R$`a|FDxAeEzo4g^a)Z9Y%ZcPkbrUtW@BGHh3_=%A-D=V2IJ?_oG!V(clH zVATgn2gXK5s#5U)9iP0XZsnDpQ<6WGEVsuQfng(>gA$RBoC1Jy=f02_SuSCf2RRrM z9}$r7IC9UQfkZlPO~7X}KN1d^+grV|9?tqmJnj5GpJb+s#CkMpU(>S4wxG=R5McQ0 zYceLL5Z`wdv6Jd)(4uE~0{3n$HKo7tw}TDq%;HP_)%Kjlc)oi#R3kC4zGCOIj&d&T zUf0KdkvizLae~l0y1GMJEkZ?K6`a7|t-mqwkBE*hA~-(;M;`Dydk}~VzRK_gZL?he sk0P(t_*b>OR^;_YUvKCyif}`XZ04qxJWAc_c-*0NYc{UFxRRUlKMg|*YXATM literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png b/desktop/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..271f1fc425ae9e7af1bf4a71c839692bf1e76084 GIT binary patch literal 2386 zcmV-Y39a^tP)oPktyR)-%XXcz_BPwU+ z&i(e>d+s@B?iD2_K;l>tsS5}&2?17?#l=M!85v1f-{bK(y7Te*3JVME19)_F6nc7k z;L)Q;^3Mwk3kgGVlb+Y>1)t9cMMXv6_xt6allCPt>SUpnUb}V;?%cTplarIKg_KOa z?Cfl)t*wQ_hY!o}Edd+`d;R)#*-{Bo^7Rldg2qpxpc4Xo|Necrc=6&IizP_r=;h?( zz=aDJpuD`?e8873U4pAuuM#8+^lEBqWYDGn#v%Fa*|V|}MV)P(UR6~U1OfrwL$)r! z2p1=O^o*&qt=G%X&xc#LZs~$7=(!nMF2UtTZ)|J~8XFsRT}u-%bEYI3mig1`?CgZ* z=4Q#z5RKk)U0oe@HphB5Z{AdmhQ$Po^DesOv=ri74}&7z-Q6+gCWCAjv0i?Xz|1RWh6`Yjl<8k9J& z2i(ZH_`!n*+7^uKU4m;bxG14eNHJEi1~6OmO4Ql*f*Y3p{(ea_;g89B>TLVLMHw0z z0!_fo%stfE_JbSAE+$}H9#NT%J>iDEY1KIx3*@M??Fl#5FfAslI*|0NhYMy@RT3nA z;40~f1lPH64FM)d{KHKcaP$QW3JT<$WM*cjrY=Uw&=Gk0^r@@hVrAt1{rlxVwRCYc zGdw)3T1BI`t)kI~ICJKV{E1i2ojV72@7{F=bxPcpeKLIS~$hJO&^VTxSP(L_0=(2(AqPryQ=0+M3LEif(`lE`{rgcaz)< z*EZQCg6qI+lAGX~sV5R#XIFTN1lQT+yCT7Lc5SvuMd{9dMptK(-0%e!2`*)m2(FF& z5s~0hHi_U;Hi_UmGMi+p4s#(~$|g}ZiQrN;iQocdlL#(QHi_T@Ws?Z5d$UOd*TLB& z+GyOt*(8F?aW=`v-ud;icYdc<*30h!6C|+zyGaPOVb462=dQq;`&_Jp&Y#oem#Xz{ z-9T_5xqEbg;f=%Q;rmZQYv7_}r2Q>#D)t@wfBX^FDLyk__p*} z(mQQzF3pwMBq3EBg{f`%rv$lb*K%B)re*a@Yzm-kQW76vVq)U|zqGy!un^z%t9C8P zbF$uhdNnp^Sxf+Mvf$$B+?Ow3mX98CUN&jwF@yVS-1jKl_D5XzjQ;&->%}433b+{= z8F1yw6~l*f@S9C~_BbVLWHrEjzi+Z^SVpi`!A(szDFN$^8!nsU)d0t{msPUoT(8$_ zX<*3EPIrBMePHHhV|pK=;e$EV|L@mn!DcvAMo(NB7#J|rPQl0n;E^%S&CR9&4vuct z^?QD{rdG4wuJH_AffKX5&IIxUkY_~b;-x-U>~7Jt>-W;Je<-f+@xib(2^rh%8b;*{Uty8 zRM1?;M9ViH&%l?%TcPi_O$t%3*YG)Rr|+9ry++$;CQL4iT^k`c7+vq{l&Z6dG!(mqJB3s<1XF>y+bg7GtKWa&ss%ROl&BC=)sP@NU#N53 zOK?-H=aHC1{zG0~o~aGNBtp}N&yx9V)16|Hgu-1PKxDSFpNQ7LwcYcMnChtTqRs2|wIJmptV$=0&y zVP`Y1S+5K;mPH*uVIU>iir07~KA#V=va%$CYcD-yrZhw=x|~Oj93i+i z(>r$TnBv9O4AJ3WcS%Xff8I2~p2N_NnSD=RA{>TJIB4jw!xF~FJ^jyVTM&&So()zXtEPdwDw*6XqL zuGHS%?$J}RsjKJ?FLe9%ZI9pYm#DL?w_Lq&bz5+xG&VL$ZEbBH>TGM~Ts_0GA;2iT zy}dH%>FH_mvQxAaLT_2ip-LLy@Vp!h2BjM}Zou;LvWFmnDG@3RVqh3D(KjR#1{?{x zqoYHHtrZj@xcU=kBp4GVc7&%y%Ga6L72}iEiLsZ zWrer^tRx8Aik)}tAozTV%&S}hCINTu_)bEANs=J{0M{`?5sjXjtN;K207*qoM6N<$ Ef~W|KkN^Mx literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png b/desktop/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8cae339e0310f5ec20298b254231aaacf6c0679c GIT binary patch literal 3592 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png b/desktop/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000000000000000000000000000000000000..84853ce9ae91efc3a0a06a91fb6f5aab9ffa264e GIT binary patch literal 1617 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/desktop/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e584e98371ef0c9acaa5ba94f7c4645d3dd5f9 GIT binary patch literal 3415 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 literal 0 HcmV?d00001 diff --git a/desktop/src-tauri/src/main.rs b/desktop/src-tauri/src/main.rs new file mode 100644 index 0000000..fc30cb7 --- /dev/null +++ b/desktop/src-tauri/src/main.rs @@ -0,0 +1,164 @@ +// Prevents a stray console window on Windows in release builds. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +mod sidecar; +mod tray; + +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; + +use tauri::{AppHandle, Manager, RunEvent, WindowEvent}; +use tauri_plugin_shell::process::CommandChild; + +/// Holds the running jcode sidecar so we can terminate it explicitly on exit. +/// Tauri already best-effort kills spawned children, but a background of +/// loopback servers is exactly where a leaked process is most annoying, so we +/// own the lifecycle outright. +#[derive(Default)] +pub struct SidecarHandle(pub Mutex>); + +/// Cross-cutting desktop state. `tray` records whether the menu-bar tray was +/// actually created — close-to-tray must only swallow the window's close when +/// there is a tray to reopen from, or the user would be stranded (e.g. on a +/// Linux desktop with no StatusNotifier host). +#[derive(Default)] +pub struct DesktopState { + pub tray: AtomicBool, +} + +/// Bring the main window to the foreground (used by the tray and the +/// single-instance guard when a second launch is attempted). +pub fn show_main(app: &AppHandle) { + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + let _ = w.unminimize(); + let _ = w.set_focus(); + } +} + +/// Toggle window visibility — the tray icon's left-click behaviour. +pub fn toggle_main(app: &AppHandle) { + if let Some(w) = app.get_webview_window("main") { + if w.is_visible().unwrap_or(false) { + let _ = w.hide(); + } else { + let _ = w.show(); + let _ = w.set_focus(); + } + } +} + +fn kill_sidecar(app: &AppHandle) { + if let Some(state) = app.try_state::() { + if let Ok(mut guard) = state.0.lock() { + if let Some(child) = guard.take() { + let _ = child.kill(); + } + } + } +} + +fn main() { + let mut builder = tauri::Builder::default(); + + // Single-instance must be the FIRST plugin so a second launch is short- + // circuited before any window/sidecar work happens. + #[cfg(desktop)] + { + builder = builder.plugin(tauri_plugin_single_instance::init(|app, _argv, _cwd| { + show_main(app); + })); + } + + builder = builder + .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_notification::init()) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()); + + #[cfg(desktop)] + { + builder = builder + .plugin(tauri_plugin_window_state::Builder::default().build()) + // Registered without a shortcut here; the accelerator is bound in + // setup() so a hotkey conflict logs instead of crashing the app. + .plugin(tauri_plugin_global_shortcut::Builder::new().build()); + } + + let app = builder + .manage(SidecarHandle::default()) + .manage(DesktopState::default()) + .setup(|app| { + // Start the backend FIRST so a (possibly cosmetic) tray failure can + // never prevent the server — and thus the whole app — from coming up. + if let Err(e) = sidecar::start(app.handle()) { + eprintln!("[jcode] failed to start sidecar: {e}"); + use tauri_plugin_dialog::DialogExt; + app.handle() + .dialog() + .message(format!("jcode could not start its local server:\n{e}")) + .title("jcode") + .blocking_show(); + app.handle().exit(1); + return Ok(()); + } + + // The tray is best-effort. On a Linux desktop without a tray host it + // may fail; we log and fall back to "closing the window quits". + match tray::create(app.handle()) { + Ok(()) => { + if let Some(state) = app.try_state::() { + state.tray.store(true, Ordering::Relaxed); + } + } + Err(e) => eprintln!("[jcode] tray unavailable, close will quit: {e}"), + } + + // Global hotkey is a convenience; a conflict must not crash the app. + #[cfg(desktop)] + { + use tauri_plugin_global_shortcut::{ + Code, GlobalShortcutExt, Modifiers, Shortcut, ShortcutState, + }; + // ⌘/⊞ + Shift + J toggles the window from anywhere. + let toggle = Shortcut::new(Some(Modifiers::SUPER | Modifiers::SHIFT), Code::KeyJ); + if let Err(e) = app.global_shortcut().on_shortcut(toggle, |app, _sc, event| { + if event.state == ShortcutState::Pressed { + toggle_main(app); + } + }) { + eprintln!("[jcode] global shortcut not registered: {e}"); + } + } + + Ok(()) + }) + .on_window_event(|window, event| { + // Close-to-tray: when a tray icon exists, the main window's close + // button hides it instead of quitting, so a long-running agent keeps + // working in the background — reopen from the tray, the global + // hotkey, or relaunching. With no tray, closing quits normally. A + // true exit is always available via the tray "Quit" item, the macOS + // app menu (Cmd+Q), or Alt+F4 / closing when there is no tray. + if let WindowEvent::CloseRequested { api, .. } = event { + if window.label() == "main" { + let tray_active = window + .app_handle() + .try_state::() + .map(|s| s.tray.load(Ordering::Relaxed)) + .unwrap_or(false); + if tray_active { + let _ = window.hide(); + api.prevent_close(); + } + } + } + }) + .build(tauri::generate_context!()) + .expect("error while building jcode desktop"); + + app.run(|app_handle, event| match event { + RunEvent::ExitRequested { .. } | RunEvent::Exit => kill_sidecar(app_handle), + _ => {} + }); +} diff --git a/desktop/src-tauri/src/sidecar.rs b/desktop/src-tauri/src/sidecar.rs new file mode 100644 index 0000000..4c22585 --- /dev/null +++ b/desktop/src-tauri/src/sidecar.rs @@ -0,0 +1,145 @@ +//! Launches the bundled `jcode` binary as a Tauri sidecar and points the main +//! window at it once it is accepting connections. +//! +//! The Go binary already embeds the entire web UI (frontend + REST + WebSocket), +//! so the desktop app reuses that server verbatim: Rust just picks a free +//! loopback port, spawns `jcode web` on it, waits until the port is live, then +//! navigates the (initially hidden, splash-showing) window to the server. + +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::time::Duration; + +use tauri::{AppHandle, Manager, Url}; +use tauri_plugin_shell::process::CommandEvent; +use tauri_plugin_shell::ShellExt; + +use crate::SidecarHandle; + +/// Ask the OS for an unused loopback port. There is a tiny TOCTOU window +/// between dropping this listener and the sidecar binding it, which is +/// acceptable for a local developer tool; the health poll below tolerates a +/// slow or failed bind. +fn pick_free_port() -> u16 { + TcpListener::bind("127.0.0.1:0") + .and_then(|l| l.local_addr()) + .map(|a| a.port()) + .unwrap_or(8799) +} + +pub fn start(app: &AppHandle) -> Result<(), Box> { + let port = pick_free_port(); + let url = format!("http://127.0.0.1:{port}"); + + // Run the server from the user's home directory; the in-app workspace + // picker takes over project selection from there. + let workdir = app + .path() + .home_dir() + .unwrap_or_else(|_| std::env::temp_dir()); + + let (mut rx, child) = app + .shell() + .sidecar("jcode")? + .args([ + "web", + "--port", + &port.to_string(), + "--host", + "127.0.0.1", + "--open=false", + ]) + .current_dir(workdir) + .spawn()?; + + if let Some(state) = app.try_state::() { + if let Ok(mut guard) = state.0.lock() { + *guard = Some(child); + } + } + + // Pump the sidecar's stdout/stderr into the desktop log so `jcode web` + // diagnostics are still reachable when running headless inside the app. + tauri::async_runtime::spawn(async move { + while let Some(event) = rx.recv().await { + match event { + CommandEvent::Stdout(bytes) => { + eprintln!("[jcode] {}", String::from_utf8_lossy(&bytes).trim_end()); + } + CommandEvent::Stderr(bytes) => { + eprintln!("[jcode] {}", String::from_utf8_lossy(&bytes).trim_end()); + } + CommandEvent::Terminated(payload) => { + eprintln!("[jcode] sidecar exited: {payload:?}"); + } + _ => {} + } + } + }); + + // Health-poll the port on a background thread, then reveal the window. We + // verify the /api/health response (not just a bare TCP connect) so that if + // another process grabbed the port in the moment between pick_free_port and + // the sidecar binding it, we don't navigate the window to a foreign server. + let app = app.clone(); + let addr: SocketAddr = ([127, 0, 0, 1], port).into(); + std::thread::spawn(move || { + for _ in 0..400 { + if health_ok(&addr, port) { + if let Some(w) = app.get_webview_window("main") { + if let Ok(parsed) = Url::parse(&url) { + let _ = w.navigate(parsed); + } + let _ = w.show(); + let _ = w.set_focus(); + } + return; + } + std::thread::sleep(Duration::from_millis(150)); + } + // Give up waiting after ~60s but still show the window (splash) so the + // user sees the failure instead of an app that never appears. + if let Some(w) = app.get_webview_window("main") { + let _ = w.show(); + } + }); + + Ok(()) +} + +/// Probe GET /api/health and confirm it's actually our jcode server: a 200 +/// response whose body carries the health JSON ("status" field). A foreign +/// listener that happened to grab the port won't satisfy both. +fn health_ok(addr: &SocketAddr, port: u16) -> bool { + use std::io::{Read, Write}; + + let Ok(mut stream) = TcpStream::connect_timeout(addr, Duration::from_millis(300)) else { + return false; + }; + let _ = stream.set_read_timeout(Some(Duration::from_millis(600))); + let _ = stream.set_write_timeout(Some(Duration::from_millis(600))); + + let req = format!( + "GET /api/health HTTP/1.0\r\nHost: 127.0.0.1:{port}\r\nConnection: close\r\n\r\n" + ); + if stream.write_all(req.as_bytes()).is_err() { + return false; + } + + let mut buf = Vec::with_capacity(2048); + let mut chunk = [0u8; 2048]; + loop { + match stream.read(&mut chunk) { + Ok(0) => break, + Ok(n) => { + buf.extend_from_slice(&chunk[..n]); + if buf.len() > 8192 { + break; + } + } + Err(_) => break, + } + } + + let resp = String::from_utf8_lossy(&buf); + resp.starts_with("HTTP/1.") && resp.contains(" 200 ") && resp.contains("\"status\"") +} diff --git a/desktop/src-tauri/src/tray.rs b/desktop/src-tauri/src/tray.rs new file mode 100644 index 0000000..6ee047b --- /dev/null +++ b/desktop/src-tauri/src/tray.rs @@ -0,0 +1,51 @@ +//! System-tray icon: left-click toggles the window, right-click opens a menu +//! with show / hide / quit. The tray is what keeps jcode reachable after the +//! window is "closed" (hidden) to the background. + +use tauri::menu::{Menu, MenuItem, PredefinedMenuItem}; +use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}; +use tauri::{AppHandle, Manager}; + +use crate::{show_main, toggle_main}; + +pub fn create(app: &AppHandle) -> Result<(), Box> { + let show = MenuItem::with_id(app, "show", "显示 jcode", true, None::<&str>)?; + let hide = MenuItem::with_id(app, "hide", "隐藏窗口", true, None::<&str>)?; + let sep = PredefinedMenuItem::separator(app)?; + let quit = MenuItem::with_id(app, "quit", "退出 jcode", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &hide, &sep, &quit])?; + + let icon = app + .default_window_icon() + .cloned() + .ok_or("default window icon missing from bundle config")?; + + TrayIconBuilder::with_id("main") + .icon(icon) + .tooltip("jcode") + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(|app, event| match event.id.as_ref() { + "show" => show_main(app), + "hide" => { + if let Some(w) = app.get_webview_window("main") { + let _ = w.hide(); + } + } + "quit" => app.exit(0), + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let TrayIconEvent::Click { + button: MouseButton::Left, + button_state: MouseButtonState::Up, + .. + } = event + { + toggle_main(tray.app_handle()); + } + }) + .build(app)?; + + Ok(()) +} diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json new file mode 100644 index 0000000..c3bb98b --- /dev/null +++ b/desktop/src-tauri/tauri.conf.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "jcode", + "version": "0.5.3", + "identifier": "com.cnjack.jcode", + "build": { + "frontendDist": "../splash" + }, + "app": { + "withGlobalTauri": true, + "windows": [ + { + "label": "main", + "title": "jcode", + "url": "index.html", + "width": 1280, + "height": 840, + "minWidth": 760, + "minHeight": 480, + "titleBarStyle": "Overlay", + "hiddenTitle": true, + "visible": false, + "center": true + } + ], + "security": { + "csp": null, + "capabilities": ["default"] + } + }, + "bundle": { + "active": true, + "targets": "all", + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.ico" + ], + "externalBin": ["binaries/jcode"], + "category": "DeveloperTool", + "shortDescription": "jcode desktop", + "longDescription": "Native desktop shell for jcode — the embedded Go backend runs as a sidecar and Tauri renders the web UI with native system integration.", + "copyright": "© cnjack" + }, + "plugins": {} +} diff --git a/docs/commands.md b/docs/commands.md index bbcbb58..43788b9 100644 --- a/docs/commands.md +++ b/docs/commands.md @@ -1,6 +1,6 @@ --- title: Commands & Shortcuts -nav_order: 7 +nav_order: 8 --- # Commands & Shortcuts diff --git a/docs/configuration.md b/docs/configuration.md index be30b0f..6bd946f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1,6 +1,6 @@ --- title: Configuration -nav_order: 6 +nav_order: 7 --- # Configuration diff --git a/docs/desktop.md b/docs/desktop.md new file mode 100644 index 0000000..d2636c6 --- /dev/null +++ b/docs/desktop.md @@ -0,0 +1,154 @@ +--- +title: Desktop App +nav_order: 6 +--- + +# Desktop App + +jcode ships a native desktop application built with [Tauri](https://tauri.app). It +wraps the exact same web UI you get from `jcode web` in a real OS window and adds +native system integration — notifications, a menu-bar tray, single-instance +focus, window-state memory, and a native folder picker. + +It is **not** a second implementation: the Go backend (with the web UI already +embedded) runs as a bundled *sidecar* process, and Tauri renders it. Everything +you can do in the browser UI works identically here. + +## Architecture + +``` +┌─────────────────────────── jcode.app (Tauri) ───────────────────────────┐ +│ │ +│ Rust shell (src-tauri) │ +│ ├─ picks a free loopback port │ +│ ├─ spawns the jcode sidecar: jcode web --port N --host 127.0.0.1 │ +│ ├─ health-polls the port, then navigates the window to it │ +│ ├─ system tray · single-instance · window-state · close-to-tray │ +│ └─ kills the sidecar on exit │ +│ │ +│ WebView ──HTTP/WS──▶ http://127.0.0.1:N (the jcode Go server) │ +│ ▲ ├─ embedded Vue web UI │ +│ │ ├─ REST /api/* │ +│ native APIs └─ WebSocket /ws │ +│ (notification, opener, │ +│ dialog) via Tauri IPC │ +└──────────────────────────────────────────────────────────────────────────┘ +``` + +Why a sidecar instead of rewriting the backend in Rust: + +- **Zero divergence.** The desktop app and `jcode web` serve byte-for-byte the + same frontend, REST API, and WebSocket stream. A fix in one is a fix in both. +- **Same-origin simplicity.** The Vue app's relative `/api` and `/ws` URLs just + work because the WebView loads the server's own origin. +- **Native where it counts.** The frontend feature-detects Tauri and routes + notifications, external links, and the folder picker through native plugins, + falling back to web APIs in a plain browser. + +The localhost origin is granted the Tauri JS APIs through a capability `remote` +entry (`http://localhost:*`, `http://127.0.0.1:*`) in +`desktop/src-tauri/capabilities/default.json`. + +## Native capabilities + +| Capability | What it does | +| --- | --- | +| **Notifications** | Native OS notifications on task-finished / approval-needed, fired only when the window isn't focused. Falls back to the web Notification API in the browser. | +| **System tray** | Menu-bar icon: left-click toggles the window; right-click → Show / Hide / Quit. | +| **Close-to-tray** | Closing the window hides it (the agent keeps running in the background). Quit from the tray or ⌘Q to exit for real. | +| **Global shortcut** | ⌘/⊞ + Shift + J toggles the window from anywhere. | +| **Single instance** | Launching jcode again focuses the existing window instead of starting a second copy. | +| **Window state** | Window size and position are remembered across launches. | +| **Native folder picker** | "Open folder" in the workspace switcher uses the OS dialog on desktop. | +| **External links** | Links open in the system browser via the opener plugin. | +| **Overlay title bar** | On macOS the traffic-light buttons sit in a slim draggable strip above the shell. | + +## Security model + +The desktop app keeps the jcode server running in the background (close-to-tray), +so the loopback server's exposure matters. The server drives an agent with shell +and file tools, so reaching its API equals running commands as you. + +- **Loopback only.** The sidecar binds `127.0.0.1` — never LAN-reachable. +- **Random per-launch port.** Not a security control, but raises the bar. +- **Cross-origin gate.** The WebSocket handshake and the REST CORS layer reject + requests whose `Origin` is a foreign website. Same-origin, loopback origins + (covering the Vite dev proxy), and empty-Origin clients are allowed; a page on + `https://evil.com` cannot open `ws://127.0.0.1:` or drive the agent. See + `isAllowedWebOrigin` in `internal/web/server.go` (unit-tested in `cors_test.go`). +- **Native APIs scoped to 127.0.0.1.** The Tauri capability grants notification / + opener / dialog only to the loopback origin the app actually loads. + +Hardening still worth doing (tracked as follow-ups): a per-launch **bearer token** +the sidecar hands the UI (to also stop *local* processes on a shared machine from +reaching the port), and a **Content-Security-Policy** response header from the Go +server as defense-in-depth around the markdown sink (already DOMPurify-sanitized). + +## Building & running + +Prerequisites: the [Rust toolchain](https://rustup.rs) plus the usual jcode web +toolchain (Go, Node, pnpm). On Linux you also need the Tauri system +dependencies (WebKitGTK etc. — see the [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/)). + +```bash +# Develop — opens the app with a hot window (rebuilds the sidecar first) +make desktop-dev + +# Build a distributable bundle (.app/.dmg on macOS, .msi on Windows, …) +make desktop-build + +# Regenerate app icons from the brand mark (web/public/icon.svg) +make desktop-icons +``` + +Under the hood `make desktop-sidecar` compiles the Go binary with the host +target-triple suffix Tauri expects +(`desktop/src-tauri/binaries/jcode-`, plus `.exe` on Windows) — +the web UI is embedded into that binary, so the bundle is self-contained. The +finished artifacts land in `desktop/src-tauri/target/release/bundle/`. + +Always build through `make` — it rebuilds the sidecar first. Invoking +`pnpm tauri build` / `pnpm tauri dev` directly does **not** rebuild the sidecar +and will bundle whatever stale binary is in `binaries/`; run `make desktop-sidecar` +first if you must use the Tauri CLI directly. (In this headless build environment +the DMG step fails because it needs Finder/AppleScript; the `.app` itself builds +fine, and the DMG bundles normally on a desktop session.) + +For CI/CD — how tagged releases build and publish these bundles for every platform, +and which macOS signing certificates/secrets are required — see [Release & CI](release.html). + +## Layout + +``` +desktop/ +├─ package.json # drives the Tauri CLI +├─ splash/index.html # shown while the sidecar boots +└─ src-tauri/ + ├─ tauri.conf.json # window, bundle, externalBin (the sidecar) + ├─ capabilities/default.json # permissions + remote localhost grant + ├─ binaries/ # jcode- sidecar (built by make) + ├─ icons/ # generated from web/public/icon.svg + └─ src/ + ├─ main.rs # plugins, tray, close-to-tray, sidecar cleanup + ├─ sidecar.rs # port pick · spawn · health-poll · navigate + └─ tray.rs # menu-bar icon + menu +``` + +The desktop bridge on the frontend side lives in +`web/src/composables/useDesktop.ts` — every export is feature-detected, so the +same web bundle runs unchanged in a browser and inside the desktop shell. + +## Troubleshooting + +- **Window never appears.** The shell waits for the sidecar to accept + connections (up to ~60s) before showing the window; after that it shows the + splash anyway. Check the sidecar's output — it is forwarded to the desktop log + with a `[jcode]` prefix. +- **Native notifications don't fire.** Confirm OS notification permission was + granted, and that the localhost origin is listed under `remote.urls` in the + capability file (this is what enables Tauri JS APIs on the loopback origin). +- **Auto-update.** The updater plugin is intentionally not bundled yet: it + panics at startup unless `plugins.updater` (endpoints + pubkey) is configured, + which needs a signed release feed. To enable it, re-add `tauri-plugin-updater` + (Cargo + `main.rs` + the `updater:default` capability), set + `bundle.createUpdaterArtifacts: true`, and add the `plugins.updater` config. diff --git a/docs/goal.md b/docs/goal.md index 772d1ad..c1bcc1c 100644 --- a/docs/goal.md +++ b/docs/goal.md @@ -1,6 +1,6 @@ --- title: Goals -nav_order: 8 +nav_order: 9 --- # Goals diff --git a/docs/themes.md b/docs/themes.md index 1d27a6b..04b764f 100644 --- a/docs/themes.md +++ b/docs/themes.md @@ -1,6 +1,6 @@ --- title: Themes -nav_order: 9 +nav_order: 10 --- # Themes diff --git a/docs/web-interface.md b/docs/web-interface.md index 88a39e0..d64851a 100644 --- a/docs/web-interface.md +++ b/docs/web-interface.md @@ -7,6 +7,8 @@ nav_order: 5 jcode includes a browser-based interface for users who prefer a visual UI over the terminal. The web UI provides chat, file browsing, terminal access, and full agent control. +> Prefer a native window with OS notifications and a menu-bar tray? The same UI ships as a [Desktop App](desktop). + ![jcode Web UI](asset/web-screenshot.png) ## Starting the Web Server diff --git a/internal/command/ssh.go b/internal/command/ssh.go index 34d11ab..9fd6795 100644 --- a/internal/command/ssh.go +++ b/internal/command/ssh.go @@ -10,11 +10,15 @@ import ( einomodel "github.com/cloudwego/eino/components/model" "github.com/cnjack/jcode/internal/prompts" + "github.com/cnjack/jcode/internal/remote" "github.com/cnjack/jcode/internal/tools" "github.com/cnjack/jcode/internal/tui" ) -// handleSSHConnect connects to a remote machine via SSH and reconfigures the env. +// HandleSSHConnect connects to a remote machine via SSH and reconfigures the +// env. The pure connection + directory-listing logic lives in internal/remote +// so the web server can reuse it; this function keeps only the TUI glue +// (p.Send of status/dir messages). func HandleSSHConnect( ctx context.Context, env *tools.Env, @@ -33,23 +37,18 @@ func HandleSSHConnect( host = parts[1] } - executor, err := tools.NewSSHExecutor(host, user, tools.BuildSSHAuthMethods()) + executor, err := remote.Connect(remote.SSHOptions{Host: host, User: user}) if err != nil { p.Send(tui.SSHStatusMsg{Success: false, Err: err}) return } - // Temporarily set the executor so handleSSHListDir can use it during + // Temporarily set the executor so HandleSSHListDir can use it during // interactive path selection. env.SetSSH(executor, "/root") if path == "?" { - remotePwd := "/root" - if stdout, _, execErr := executor.Exec(ctx, "pwd", "", 5*1e9); execErr == nil { - if trimmed := strings.TrimSpace(stdout); trimmed != "" { - remotePwd = trimmed - } - } + remotePwd := remote.DiscoverPwd(ctx, executor, "/root") HandleSSHListDir(ctx, env, remotePwd, p) return // Do not initialize agent yet } @@ -58,11 +57,7 @@ func HandleSSHConnect( if path != "" { remotePwd = path } else { - if stdout, _, execErr := executor.Exec(ctx, "pwd", "", 5*1e9); execErr == nil { - if trimmed := strings.TrimSpace(stdout); trimmed != "" { - remotePwd = trimmed - } - } + remotePwd = remote.DiscoverPwd(ctx, executor, "/root") } env.SetSSH(executor, remotePwd) @@ -76,41 +71,17 @@ func HandleSSHConnect( p.Send(tui.SSHStatusMsg{ Success: true, - Label: fmt.Sprintf("%s@%s (pwd: %s)", user, host, remotePwd), + Label: envLabel, }) } -// handleSSHListDir runs `ls` on the remote host and sends the results to the +// HandleSSHListDir runs `ls` on the remote host and sends the results to the // TUI directory picker. func HandleSSHListDir(ctx context.Context, env *tools.Env, path string, p *tea.Program) { - cmd := fmt.Sprintf("ls -F -1 %s", tools.ShellQuote(path)) - stdout, stderr, err := env.Exec.Exec(ctx, cmd, "", 10*1e9) + dirs, err := remote.ListDirs(ctx, env.Exec, path) if err != nil { - p.Send(tui.SSHDirResultsMsg{Err: fmt.Errorf("ls failed: %v\nstderr: %s", err, truncate(stderr, 100))}) + p.Send(tui.SSHDirResultsMsg{Err: err}) return } - - lines := strings.Split(strings.TrimSpace(stdout), "\n") - var dirs []string - if path != "/" { - dirs = append(dirs, "..") - } - for _, line := range lines { - line = strings.TrimSpace(line) - if line == "" { - continue - } - if strings.HasSuffix(line, "/") { - dirs = append(dirs, line[:len(line)-1]) - } - } - p.Send(tui.SSHDirResultsMsg{Path: path, Items: dirs}) } - -func truncate(s string, l int) string { - if len(s) > l { - return s[:l] + "..." - } - return s -} diff --git a/internal/command/web.go b/internal/command/web.go index ed6ac44..2a597e4 100644 --- a/internal/command/web.go +++ b/internal/command/web.go @@ -23,6 +23,7 @@ import ( internalmodel "github.com/cnjack/jcode/internal/model" weixin "github.com/cnjack/jcode/internal/pkg/weixin" "github.com/cnjack/jcode/internal/prompts" + "github.com/cnjack/jcode/internal/remote" "github.com/cnjack/jcode/internal/runner" "github.com/cnjack/jcode/internal/session" "github.com/cnjack/jcode/internal/skills" @@ -337,6 +338,11 @@ func runWebServer(port int, host string, openBrowser bool) error { } switchProject := func(newPwd string) (*adk.ChatModelAgent, *session.Recorder, error) { + // 0. Close any live remote SSH connection we're switching away from. + if prev, ok := env.Exec.(*tools.SSHExecutor); ok { + defer func() { _ = prev.Close() }() + } + // 1. Update env working directory (all tools share the same *Env). env.ResetToLocal(newPwd, platform) @@ -371,6 +377,50 @@ func runWebServer(port int, host string, openBrowser bool) error { return newAg, newRec, nil } + // switchToRemote mirrors switchProject but binds the shared env to a remote + // SSH executor instead of a local path. It reuses the SAME agent/recorder + // rebuild sequence so the agent, system prompt and session recorder stay + // consistent with the local switch path. + switchToRemote := func(executor *tools.SSHExecutor, remotePwd string) (*adk.ChatModelAgent, *session.Recorder, error) { + // 0. Close the previous live remote SSH connection (if switching + // remote→remote); switching from local has nothing to close. + if prev, ok := env.Exec.(*tools.SSHExecutor); ok && prev != executor { + defer func() { _ = prev.Close() }() + } + + // 1. Point the shared env at the remote SSH executor. + env.SetSSH(executor, remotePwd) + remotePlatform := executor.Platform() + + // 2. Approval state now governs the remote working directory. + approvalState.SetWorkpath(remotePwd) + + // 3. Re-render the system prompt with the remote env label + platform. + // Project skills are scanned from the LOCAL fs, so keep the existing + // descriptions rather than rescanning against the remote path. + envLabel := fmt.Sprintf("%s (pwd: %s)", executor.Label(), remotePwd) + systemPrompt = prompts.GetSystemPrompt(remotePlatform, remotePwd, envLabel, nil, skillLoader.Descriptions()) + + // 4. Update the captured pwd (env already points at the remote target). + pwd = remotePwd + + // 5. Recorder scoped to a host-qualified project key so a remote path + // does not collide with a local path of the same name in the tree. + projectKey := remote.ProjectLabel(executor, remotePwd) + if rec != nil { + rec.Close() + } + newRec, _ := session.NewRecorder(projectKey, providerName, modelName) + rec = newRec + + // 6. Rebuild the agent with the updated remote prompt. + newAg, err := createAgent(providerName, modelName) + if err != nil { + return nil, nil, err + } + return newAg, newRec, nil + } + srv := web.NewServer(&web.ServerConfig{ Port: port, Host: host, @@ -382,6 +432,7 @@ func runWebServer(port int, host string, openBrowser bool) error { RebuildForMode: rebuildForMode, InitialMode: startupMode.String(), SwitchProject: switchProject, + SwitchToRemote: switchToRemote, TodoStore: env.TodoStore, Recorder: rec, Tracer: langfuseTracer, diff --git a/internal/remote/ssh.go b/internal/remote/ssh.go new file mode 100644 index 0000000..89e393d --- /dev/null +++ b/internal/remote/ssh.go @@ -0,0 +1,182 @@ +// Package remote provides UI-agnostic helpers for connecting to and inspecting +// remote execution targets (currently SSH). The connection + directory-listing +// logic here was extracted from the TUI command layer so that both the TUI and +// the web server can drive the same flow without depending on bubbletea. +package remote + +import ( + "context" + "fmt" + "os" + "path/filepath" + "strings" + "time" + + "golang.org/x/crypto/ssh" + + "github.com/cnjack/jcode/internal/tools" +) + +// SSHOptions describes how to reach and authenticate with a remote host. +// +// Host may be "host", "host:port" or "user@host"; User and Port fill in the +// pieces a form provides separately. When neither Password nor KeyPath is set, +// authentication falls back to the SSH agent + the default ~/.ssh keys (the same +// behavior as tools.BuildSSHAuthMethods, used by the TUI). +type SSHOptions struct { + Host string + Port int + User string + Password string // password auth + KeyPath string // explicit private key file (~ is expanded) + Passphrase string // passphrase for an encrypted private key +} + +// resolveTarget splits Host into a dial address ("host:port") and a username, +// honoring an embedded "user@" prefix and an explicit Port. +func resolveTarget(opts SSHOptions) (addr, user string) { + user = strings.TrimSpace(opts.User) + host := strings.TrimSpace(opts.Host) + if at := strings.SplitN(host, "@", 2); len(at) == 2 { + if user == "" { + user = at[0] + } + host = at[1] + } + if user == "" { + user = "root" + } + // Apply an explicit port only when the host doesn't already carry one. + if opts.Port > 0 && !strings.Contains(host, ":") { + host = fmt.Sprintf("%s:%d", host, opts.Port) + } + return host, user +} + +// BuildAuthMethods assembles the SSH auth methods for the given options. It +// returns an error if an explicit key cannot be read or parsed, and falls back +// to the agent + default keys when no explicit credentials are supplied. +func BuildAuthMethods(opts SSHOptions) ([]ssh.AuthMethod, error) { + var methods []ssh.AuthMethod + + if opts.Password != "" { + methods = append(methods, ssh.Password(opts.Password)) + } + + if kp := strings.TrimSpace(opts.KeyPath); kp != "" { + key, err := os.ReadFile(expandHome(kp)) + if err != nil { + return nil, fmt.Errorf("read private key: %w", err) + } + var signer ssh.Signer + if opts.Passphrase != "" { + signer, err = ssh.ParsePrivateKeyWithPassphrase(key, []byte(opts.Passphrase)) + } else { + signer, err = ssh.ParsePrivateKey(key) + } + if err != nil { + return nil, fmt.Errorf("parse private key %s: %w", kp, err) + } + methods = append(methods, ssh.PublicKeys(signer)) + } + + // Fall back to agent + default keys when nothing explicit was provided. + if len(methods) == 0 { + methods = tools.BuildSSHAuthMethods() + } + if len(methods) == 0 { + return nil, fmt.Errorf("no SSH credentials available: provide a password or key, or load keys into the SSH agent") + } + return methods, nil +} + +// Connect dials the remote host described by opts and returns a live executor. +func Connect(opts SSHOptions) (*tools.SSHExecutor, error) { + addr, user := resolveTarget(opts) + methods, err := BuildAuthMethods(opts) + if err != nil { + return nil, err + } + return tools.NewSSHExecutor(addr, user, methods) +} + +// DiscoverPwd returns the remote default working directory (best effort), +// falling back to the provided default when `pwd` cannot be determined. +func DiscoverPwd(ctx context.Context, exec tools.Executor, fallback string) string { + if fallback == "" { + fallback = "/root" + } + if stdout, _, err := exec.Exec(ctx, "pwd", "", 5*time.Second); err == nil { + if trimmed := strings.TrimSpace(stdout); trimmed != "" { + return trimmed + } + } + return fallback +} + +// ListDirs lists the sub-directories of path on the remote target using the +// executor. ".." is prepended (unless already at the filesystem root) so callers +// can render an "up" entry in a directory picker. +func ListDirs(ctx context.Context, exec tools.Executor, path string) ([]string, error) { + cmd := fmt.Sprintf("ls -F -1 %s", tools.ShellQuote(path)) + stdout, stderr, err := exec.Exec(ctx, cmd, "", 10*time.Second) + if err != nil { + return nil, fmt.Errorf("ls %s failed: %v: %s", path, err, truncate(stderr, 100)) + } + + var dirs []string + if path != "/" { + dirs = append(dirs, "..") + } + for _, line := range strings.Split(strings.TrimSpace(stdout), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + if strings.HasSuffix(line, "/") { + dirs = append(dirs, strings.TrimSuffix(line, "/")) + } + } + return dirs, nil +} + +// ProjectLabel returns a stable, host-qualified identifier for a remote working +// directory. It is used as the session "project" key so remote sessions are +// grouped separately from local paths of the same name. Form: +// ssh://user@host:port/remote/path +func ProjectLabel(exec *tools.SSHExecutor, remotePwd string) string { + return fmt.Sprintf("ssh://%s@%s%s", exec.User(), exec.Host(), normalizeRemotePath(remotePwd)) +} + +// normalizeRemotePath ensures the path is absolute (leading slash) for use in a +// project label. +func normalizeRemotePath(p string) string { + if !strings.HasPrefix(p, "/") { + return "/" + p + } + return p +} + +// expandHome expands a leading ~ or ~/ to the current user's home directory. +func expandHome(path string) string { + if path == "~" { + if home, err := os.UserHomeDir(); err == nil { + return home + } + return path + } + if strings.HasPrefix(path, "~/") { + if home, err := os.UserHomeDir(); err == nil { + return filepath.Join(home, path[2:]) + } + } + return path +} + +func truncate(s string, n int) string { + s = strings.TrimSpace(s) + if len(s) > n { + return s[:n] + "..." + } + return s +} diff --git a/internal/remote/ssh_test.go b/internal/remote/ssh_test.go new file mode 100644 index 0000000..2a934b4 --- /dev/null +++ b/internal/remote/ssh_test.go @@ -0,0 +1,106 @@ +package remote + +import ( + "context" + "os" + "testing" + "time" + + "github.com/cnjack/jcode/internal/tools" +) + +func TestResolveTarget(t *testing.T) { + cases := []struct { + name string + opts SSHOptions + wantAddr string + wantUser string + }{ + {"host+user+port", SSHOptions{Host: "1.2.3.4", User: "root", Port: 2222}, "1.2.3.4:2222", "root"}, + {"host only defaults user", SSHOptions{Host: "1.2.3.4"}, "1.2.3.4", "root"}, + {"user@host embedded", SSHOptions{Host: "deploy@example.com", Port: 22}, "example.com:22", "deploy"}, + {"explicit user beats embedded", SSHOptions{Host: "deploy@example.com", User: "root"}, "example.com", "root"}, + {"host already has port ignores port opt", SSHOptions{Host: "1.2.3.4:2200", Port: 22, User: "u"}, "1.2.3.4:2200", "u"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + addr, user := resolveTarget(c.opts) + if addr != c.wantAddr || user != c.wantUser { + t.Fatalf("resolveTarget(%+v) = (%q,%q), want (%q,%q)", c.opts, addr, user, c.wantUser, c.wantAddr) + } + }) + } +} + +// fakeExecutor implements tools.Executor for ListDirs / DiscoverPwd tests. +type fakeExecutor struct { + out string + err error +} + +func (f *fakeExecutor) ReadFile(context.Context, string) ([]byte, error) { return nil, nil } +func (f *fakeExecutor) WriteFile(context.Context, string, []byte, os.FileMode) error { + return nil +} +func (f *fakeExecutor) MkdirAll(context.Context, string, os.FileMode) error { return nil } +func (f *fakeExecutor) Stat(context.Context, string) (*tools.FileInfo, error) { + return &tools.FileInfo{Exists: true, IsDir: true}, nil +} +func (f *fakeExecutor) Exec(context.Context, string, string, time.Duration) (string, string, error) { + return f.out, "", f.err +} +func (f *fakeExecutor) Platform() string { return "linux/amd64" } +func (f *fakeExecutor) Label() string { return "fake" } + +func TestListDirs(t *testing.T) { + // `ls -F -1` output: dirs end with "/", executables with "*", files plain. + exec := &fakeExecutor{out: "bin/\nREADME.md\nsrc/\nrun.sh*\n.git/\n"} + dirs, err := ListDirs(context.Background(), exec, "/home/app") + if err != nil { + t.Fatalf("ListDirs error: %v", err) + } + want := []string{"..", "bin", "src", ".git"} + if len(dirs) != len(want) { + t.Fatalf("ListDirs = %v, want %v", dirs, want) + } + for i := range want { + if dirs[i] != want[i] { + t.Fatalf("ListDirs[%d] = %q, want %q (full: %v)", i, dirs[i], want[i], dirs) + } + } +} + +func TestListDirsRootHasNoParent(t *testing.T) { + exec := &fakeExecutor{out: "etc/\nusr/\n"} + dirs, err := ListDirs(context.Background(), exec, "/") + if err != nil { + t.Fatalf("ListDirs error: %v", err) + } + if len(dirs) == 0 || dirs[0] == ".." { + t.Fatalf("root listing must not include '..': %v", dirs) + } +} + +func TestDiscoverPwdFallback(t *testing.T) { + exec := &fakeExecutor{out: " /opt/work \n"} + if got := DiscoverPwd(context.Background(), exec, "/root"); got != "/opt/work" { + t.Fatalf("DiscoverPwd = %q, want /opt/work", got) + } + bad := &fakeExecutor{out: "", err: context.DeadlineExceeded} + if got := DiscoverPwd(context.Background(), bad, "/root"); got != "/root" { + t.Fatalf("DiscoverPwd fallback = %q, want /root", got) + } +} + +func TestProjectLabel(t *testing.T) { + // ProjectLabel needs a *tools.SSHExecutor; build a zero executor isn't + // possible (unexported fields), so assert the format via a helper on a + // constructed value would require reflection. Instead, verify the path + // normalization branch indirectly through label-shaped expectations. + if got := normalizeRemotePath("home/app"); got != "/home/app" { + t.Fatalf("normalizeRemotePath = %q, want /home/app", got) + } + if got := normalizeRemotePath("/srv"); got != "/srv" { + t.Fatalf("normalizeRemotePath = %q, want /srv", got) + } +} diff --git a/internal/session/index_test.go b/internal/session/index_test.go new file mode 100644 index 0000000..d094a0f --- /dev/null +++ b/internal/session/index_test.go @@ -0,0 +1,41 @@ +package session + +import "testing" + +// TestRecorderIndexingRequiresContent locks the contract the web server's +// todo/goal OnUpdate guard relies on: a recorder that has written nothing is +// NOT listed (so ambient todo/goal updates, which the server now skips while +// HasRecording() is false, can never create a phantom empty session), and the +// session only appears once a real user message has been recorded. +func TestRecorderIndexingRequiresContent(t *testing.T) { + t.Setenv("HOME", t.TempDir()) + + const project = "/proj/elves" + + rec, err := NewRecorder(project, "zhipu", "glm-5.2") + if err != nil { + t.Fatalf("NewRecorder: %v", err) + } + + // A fresh recorder has no file and must not be indexed. + if rec.HasRecording() { + t.Fatal("fresh recorder should report HasRecording() == false") + } + if metas, _ := ListSessions(project); len(metas) != 0 { + t.Fatalf("a recorder that wrote nothing must not be indexed; got %d sessions", len(metas)) + } + + // The first user message creates the file and indexes the session. + rec.RecordUser("hello") + if !rec.HasRecording() { + t.Fatal("recorder should report HasRecording() == true after a user message") + } + metas, err := ListSessions(project) + if err != nil { + t.Fatalf("ListSessions: %v", err) + } + if len(metas) != 1 || metas[0].UUID != rec.UUID() { + t.Fatalf("expected the session indexed after a user message, got %+v", metas) + } + rec.Close() +} diff --git a/internal/tools/env.go b/internal/tools/env.go index f24556b..2edea14 100644 --- a/internal/tools/env.go +++ b/internal/tools/env.go @@ -361,6 +361,12 @@ func (s *SSHExecutor) Exec(ctx context.Context, command, workDir string, timeout func (s *SSHExecutor) Platform() string { return s.platform } +// User returns the SSH username. +func (s *SSHExecutor) User() string { return s.user } + +// Host returns the dialed host (includes the port, e.g. "1.2.3.4:22"). +func (s *SSHExecutor) Host() string { return s.host } + func (s *SSHExecutor) Label() string { return fmt.Sprintf("%s@%s", s.user, s.host) } diff --git a/internal/web/cors_test.go b/internal/web/cors_test.go new file mode 100644 index 0000000..4370d3f --- /dev/null +++ b/internal/web/cors_test.go @@ -0,0 +1,43 @@ +package web + +import ( + "net/http/httptest" + "testing" +) + +// TestIsAllowedWebOrigin locks the cross-origin gate that protects the +// loopback agent-control API + WebSocket. The legitimate flows (same-origin, +// loopback, the Vite dev proxy, LAN access) must pass; a cross-origin website +// must be rejected. +func TestIsAllowedWebOrigin(t *testing.T) { + cases := []struct { + name string + host string // the server's own Host (request target) + origin string // the browser-set Origin header + want bool + }{ + {"empty origin (curl / native client)", "127.0.0.1:8080", "", true}, + {"same-origin loopback (desktop webview / local browser)", "127.0.0.1:53913", "http://127.0.0.1:53913", true}, + {"same-origin LAN (--host 0.0.0.0)", "192.168.1.5:8080", "http://192.168.1.5:8080", true}, + {"vite dev proxy localhost:5173 -> 127.0.0.1:8091", "127.0.0.1:8091", "http://localhost:5173", true}, + {"loopback origin, different port", "127.0.0.1:8080", "http://127.0.0.1:9999", true}, + {"localhost origin to 127.0.0.1 backend", "127.0.0.1:8080", "http://localhost:8080", true}, + {"cross-origin website", "127.0.0.1:53913", "https://evil.com", false}, + {"cross-origin website targeting LAN ip", "192.168.1.5:8080", "https://evil.example", false}, + {"non-loopback private ip, different origin", "127.0.0.1:8080", "http://10.0.0.9:8080", false}, + {"malformed origin", "127.0.0.1:8080", "://nonsense", false}, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + r := httptest.NewRequest("GET", "/api/health", nil) + r.Host = c.host + if c.origin != "" { + r.Header.Set("Origin", c.origin) + } + if got := isAllowedWebOrigin(r); got != c.want { + t.Errorf("isAllowedWebOrigin(host=%q, origin=%q) = %v, want %v", c.host, c.origin, got, c.want) + } + }) + } +} diff --git a/internal/web/git.go b/internal/web/git.go new file mode 100644 index 0000000..0fe9b85 --- /dev/null +++ b/internal/web/git.go @@ -0,0 +1,108 @@ +package web + +import ( + "encoding/json" + "io" + "net/http" + "os/exec" + "strings" +) + +// handleGitBranches lists local branch names (most-recently-committed first) +// plus the current branch, for the composer's branch picker. A non-git +// directory or any git error yields an empty list rather than an error, +// mirroring handleWorkspace so the UI degrades gracefully. +func (s *Server) handleGitBranches(w http.ResponseWriter, r *http.Request) { + listCmd := exec.CommandContext(r.Context(), "git", "for-each-ref", + "--format=%(refname:short)", "--sort=-committerdate", "refs/heads") + listCmd.Dir = s.pwd + out, err := listCmd.Output() + if err != nil { + writeJSON(w, http.StatusOK, map[string]any{"current": "", "branches": []string{}}) + return + } + + // `branch --show-current` reports the unborn branch of a fresh repo (e.g. + // "main"), where `rev-parse --abbrev-ref HEAD` would just say "HEAD". + curCmd := exec.CommandContext(r.Context(), "git", "branch", "--show-current") + curCmd.Dir = s.pwd + curOut, _ := curCmd.Output() + current := strings.TrimSpace(string(curOut)) + + branches := make([]string, 0, 16) + for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") { + if b := strings.TrimSpace(line); b != "" { + branches = append(branches, b) + } + } + // A freshly initialised repo's current branch has no ref yet, so + // for-each-ref omits it — surface it anyway so the picker shows the branch + // you're actually on (with its checkmark). + if current != "" { + found := false + for _, b := range branches { + if b == current { + found = true + break + } + } + if !found { + branches = append([]string{current}, branches...) + } + } + writeJSON(w, http.StatusOK, map[string]any{ + "current": current, + "branches": branches, + }) +} + +// handleGitCheckout switches to an existing branch, or creates and checks out a +// new branch when create=true. It refuses while the agent is running (a branch +// switch rewrites the working tree under a live task) and surfaces git's own +// error verbatim (e.g. "Your local changes would be overwritten") rather than +// forcing a destructive checkout — the user decides how to resolve a dirty tree. +func (s *Server) handleGitCheckout(w http.ResponseWriter, r *http.Request) { + if s.running.Load() { + writeJSON(w, http.StatusConflict, map[string]string{ + "error": "agent is running — stop it before switching branch", + }) + return + } + + var req struct { + Branch string `json:"branch"` + Create bool `json:"create"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + branch := strings.TrimSpace(req.Branch) + if branch == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "branch is required"}) + return + } + + args := []string{"checkout"} + if req.Create { + args = append(args, "-b") + } + args = append(args, branch) + + cmd := exec.CommandContext(r.Context(), "git", args...) + cmd.Dir = s.pwd + out, err := cmd.CombinedOutput() + if err != nil { + msg := strings.TrimSpace(string(out)) + if msg == "" { + msg = err.Error() + } + writeJSON(w, http.StatusConflict, map[string]string{"error": msg}) + return + } + + curCmd := exec.CommandContext(r.Context(), "git", "branch", "--show-current") + curCmd.Dir = s.pwd + curOut, _ := curCmd.Output() + writeJSON(w, http.StatusOK, map[string]any{"branch": strings.TrimSpace(string(curOut))}) +} diff --git a/internal/web/remote.go b/internal/web/remote.go new file mode 100644 index 0000000..6c8dcd9 --- /dev/null +++ b/internal/web/remote.go @@ -0,0 +1,314 @@ +package web + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + pathpkg "path" + "strings" + "sync" + "time" + + "github.com/google/uuid" + + "github.com/cnjack/jcode/internal/config" + "github.com/cnjack/jcode/internal/remote" + "github.com/cnjack/jcode/internal/tools" +) + +// pendingConnTTL bounds how long an established-but-unbound SSH connection is +// kept alive while the user works through the wizard's directory picker. +const pendingConnTTL = 10 * time.Minute + +// pendingConn is an SSH connection created by the remote-connect wizard that has +// not yet been bound to the live env. +type pendingConn struct { + exec *tools.SSHExecutor + host string // host:port as dialed + user string + port int // originally requested port (for reconnect prefill) + createdAt time.Time +} + +// remoteConnRegistry holds pending SSH connections keyed by connection id. +type remoteConnRegistry struct { + mu sync.Mutex + conns map[string]*pendingConn +} + +func newRemoteConnRegistry() *remoteConnRegistry { + return &remoteConnRegistry{conns: make(map[string]*pendingConn)} +} + +func (rg *remoteConnRegistry) add(pc *pendingConn) string { + id := uuid.New().String() + rg.mu.Lock() + defer rg.mu.Unlock() + rg.sweepLocked() + rg.conns[id] = pc + return id +} + +func (rg *remoteConnRegistry) get(id string) *pendingConn { + rg.mu.Lock() + defer rg.mu.Unlock() + return rg.conns[id] +} + +// take removes a connection WITHOUT closing it: ownership transfers to the +// caller (e.g. the live env after a successful bind). +func (rg *remoteConnRegistry) take(id string) *pendingConn { + rg.mu.Lock() + defer rg.mu.Unlock() + pc := rg.conns[id] + delete(rg.conns, id) + return pc +} + +// drop removes and closes a pending connection (cancel / abandon). +func (rg *remoteConnRegistry) drop(id string) { + rg.mu.Lock() + pc := rg.conns[id] + delete(rg.conns, id) + rg.mu.Unlock() + if pc != nil && pc.exec != nil { + _ = pc.exec.Close() + } +} + +// sweepLocked closes and removes connections older than the TTL. Caller holds mu. +func (rg *remoteConnRegistry) sweepLocked() { + now := time.Now() + for id, pc := range rg.conns { + if now.Sub(pc.createdAt) > pendingConnTTL { + if pc.exec != nil { + _ = pc.exec.Close() + } + delete(rg.conns, id) + } + } +} + +// handleRemoteConnect establishes an SSH connection from the wizard's config +// step and parks it in the pending registry, returning a connection id the +// client uses to browse remote directories and ultimately bind. +func (s *Server) handleRemoteConnect(w http.ResponseWriter, r *http.Request) { + if s.needsSetup { + writeJSON(w, http.StatusServiceUnavailable, map[string]string{"error": "setup required: please configure a provider first"}) + return + } + var req struct { + Type string `json:"type"` // "ssh" (docker reserved for later) + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + AuthMethod string `json:"auth_method"` // "password" | "key" + Password string `json:"password"` + KeyPath string `json:"key_path"` + Passphrase string `json:"passphrase"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + if req.Type != "" && req.Type != "ssh" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "only ssh connections are supported"}) + return + } + if strings.TrimSpace(req.Host) == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "host is required"}) + return + } + + opts := remote.SSHOptions{Host: req.Host, Port: req.Port, User: req.User} + if req.AuthMethod == "password" { + opts.Password = req.Password + } else { + opts.KeyPath = req.KeyPath + opts.Passphrase = req.Passphrase + } + + exec, err := remote.Connect(opts) + if err != nil { + writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) + return + } + + remotePwd := remote.DiscoverPwd(r.Context(), exec, "/root") + id := s.remoteConns.add(&pendingConn{ + exec: exec, + host: exec.Host(), + user: exec.User(), + port: req.Port, + createdAt: time.Now(), + }) + + writeJSON(w, http.StatusOK, map[string]any{ + "connection_id": id, + "remote_pwd": remotePwd, + "platform": exec.Platform(), + "user": exec.User(), + "host": exec.Host(), + }) +} + +// handleRemoteListDir lists sub-directories of a path on a pending connection, +// driving the wizard's remote directory picker. +func (s *Server) handleRemoteListDir(w http.ResponseWriter, r *http.Request) { + var req struct { + ConnectionID string `json:"connection_id"` + Path string `json:"path"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + pc := s.remoteConns.get(req.ConnectionID) + if pc == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "connection expired or not found"}) + return + } + path := strings.TrimSpace(req.Path) + if path == "" { + path = remote.DiscoverPwd(r.Context(), pc.exec, "/root") + } + dirs, err := remote.ListDirs(r.Context(), pc.exec, path) + if err != nil { + writeJSON(w, http.StatusBadGateway, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]any{"path": path, "dirs": dirs}) +} + +// handleRemoteBind commits a pending connection: it binds the shared env to the +// remote executor at the chosen directory and rebuilds the agent (same path as +// a local project switch). +func (s *Server) handleRemoteBind(w http.ResponseWriter, r *http.Request) { + if s.running.Load() { + writeJSON(w, http.StatusConflict, map[string]string{"error": "agent is running, cannot switch workspace"}) + return + } + if s.switchToRemote == nil { + writeJSON(w, http.StatusNotImplemented, map[string]string{"error": "remote workspaces are not supported"}) + return + } + var req struct { + ConnectionID string `json:"connection_id"` + Path string `json:"path"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + pc := s.remoteConns.get(req.ConnectionID) + if pc == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "connection expired or not found"}) + return + } + remotePwd := strings.TrimSpace(req.Path) + if remotePwd == "" { + remotePwd = remote.DiscoverPwd(r.Context(), pc.exec, "/root") + } + + // Tear down local PTYs (they belonged to the previous workspace). + s.ptyMgr.closeAll() + + ag, rec, err := s.switchToRemote(pc.exec, remotePwd) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": fmt.Sprintf("failed to bind remote workspace: %v", err)}) + return + } + + label := remote.ProjectLabel(pc.exec, remotePwd) + + s.mu.Lock() + s.pwd = remotePwd + s.agent = ag + s.recorder = rec + s.history = nil + s.mu.Unlock() + + s.todoStore.Update(nil) + + // Ownership of the executor has transferred to the live env; remove the + // pending entry WITHOUT closing it. + s.remoteConns.take(req.ConnectionID) + + s.wsBroker.Broadcast(WSEvent{ + Type: "project_switched", + Data: map[string]string{"pwd": remotePwd, "label": label}, + }) + + writeJSON(w, http.StatusOK, map[string]any{ + "status": "ok", + "pwd": remotePwd, + "label": label, + "name": pathpkg.Base(remotePwd), + "host": pc.host, + "user": pc.user, + "port": pc.port, + "remote_path": remotePwd, + }) +} + +// handleRemoteCancel closes and discards a pending connection the user +// abandoned mid-wizard. +func (s *Server) handleRemoteCancel(w http.ResponseWriter, r *http.Request) { + var req struct { + ConnectionID string `json:"connection_id"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + s.remoteConns.drop(req.ConnectionID) + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} + +// handleRemoteSaveAlias upserts a saved SSH alias (name/addr/path) into config so +// it appears in GET /api/ssh for one-click reconnects. Secrets are never stored. +func (s *Server) handleRemoteSaveAlias(w http.ResponseWriter, r *http.Request) { + var req struct { + Name string `json:"name"` + Addr string `json:"addr"` // user@host + Path string `json:"path"` + } + if err := json.NewDecoder(io.LimitReader(r.Body, 1<<16)).Decode(&req); err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"}) + return + } + if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Addr) == "" { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "name and addr are required"}) + return + } + + // Mutate + persist under the lock (the config save must be atomic with the + // in-memory edit), but release it before writing the HTTP response so a slow + // client cannot stall other handlers — mirrors handleCreateMCP/handleToggleSkill. + s.mu.Lock() + if s.cfg == nil { + s.mu.Unlock() + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "config unavailable"}) + return + } + updated := false + for i := range s.cfg.SSHAliases { + if s.cfg.SSHAliases[i].Name == req.Name { + s.cfg.SSHAliases[i].Addr = req.Addr + s.cfg.SSHAliases[i].Path = req.Path + updated = true + break + } + } + if !updated { + s.cfg.SSHAliases = append(s.cfg.SSHAliases, config.SSHAlias{Name: req.Name, Addr: req.Addr, Path: req.Path}) + } + err := config.SaveConfig(s.cfg) + s.mu.Unlock() + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) +} diff --git a/internal/web/server.go b/internal/web/server.go index ebd57f8..82101df 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -10,6 +10,7 @@ import ( "io" "net" "net/http" + "net/url" "os" "os/exec" "path/filepath" @@ -80,6 +81,14 @@ type Server struct { // switchProject changes the working directory and rebuilds the agent. switchProject func(newPwd string) (*adk.ChatModelAgent, *session.Recorder, error) + // switchToRemote binds the shared env to a remote SSH executor and rebuilds + // the agent (mirrors switchProject for remote targets). + switchToRemote func(executor *tools.SSHExecutor, remotePwd string) (*adk.ChatModelAgent, *session.Recorder, error) + + // remoteConns holds SSH connections established by the remote-connect wizard + // that have not yet been bound to the live env (keyed by connection id). + remoteConns *remoteConnRegistry + // PTY manager for terminal sessions. ptyMgr *ptyManager @@ -134,6 +143,7 @@ type ServerConfig struct { RebuildForMode func(planMode bool) (*adk.ChatModelAgent, error) InitialMode string // unified startup mode string ("ask"/"plan"/"autopilot") SwitchProject func(newPwd string) (*adk.ChatModelAgent, *session.Recorder, error) + SwitchToRemote func(executor *tools.SSHExecutor, remotePwd string) (*adk.ChatModelAgent, *session.Recorder, error) TodoStore *tools.TodoStore Recorder *session.Recorder Tracer *telemetry.LangfuseTracer @@ -175,6 +185,8 @@ func NewServer(cfg *ServerConfig) *Server { createAgent: cfg.CreateAgent, rebuildForMode: cfg.RebuildForMode, switchProject: cfg.SwitchProject, + switchToRemote: cfg.SwitchToRemote, + remoteConns: newRemoteConnRegistry(), todoStore: cfg.TodoStore, recorder: cfg.Recorder, tracer: cfg.Tracer, @@ -210,7 +222,11 @@ func NewServer(cfg *ServerConfig) *Server { s.mu.RLock() r := s.recorder s.mu.RUnlock() - if r != nil { + // Only record into a session that already has real content (a user + // message created the file). Otherwise an ambient todo reset — e.g. + // clearing the previous session's todos when starting fresh — would + // be the first write, creating + indexing a phantom empty session. + if r != nil && r.HasRecording() { snapItems := make([]session.TodoSnapshotItem, len(items)) for i, it := range items { snapItems[i] = session.TodoSnapshotItem{ @@ -228,7 +244,13 @@ func NewServer(cfg *ServerConfig) *Server { s.mu.RLock() r := s.recorder s.mu.RUnlock() - tools.GoalRecorderHook(r)(g) + // Same guard as the todo hook: a goal change must not be the first + // write that creates + indexes an otherwise-empty session (e.g. + // clearing the previous session's goal on reset). Always emit to the + // UI, but only persist once the session has real content. + if r != nil && r.HasRecording() { + tools.GoalRecorderHook(r)(g) + } if s.handler != nil { s.handler.Emit("goal_update", g) } @@ -269,6 +291,8 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("GET /api/files/content", s.handleReadFile) mux.HandleFunc("GET /api/status", s.handleStatus) mux.HandleFunc("GET /api/workspace", s.handleWorkspace) + mux.HandleFunc("GET /api/git/branches", s.handleGitBranches) + mux.HandleFunc("POST /api/git/checkout", s.handleGitCheckout) mux.HandleFunc("GET /api/tasks", s.handleListAllTasks) mux.HandleFunc("PATCH /api/tasks/{id}", s.handleUpdateTask) mux.HandleFunc("GET /api/models", s.handleListModels) @@ -284,6 +308,11 @@ func (s *Server) Start(ctx context.Context) error { mux.HandleFunc("POST /api/mcp/{name}/login", s.handleMCPLogin) mux.HandleFunc("GET /api/mcp/{name}/login/status", s.handleMCPLoginStatus) mux.HandleFunc("GET /api/ssh", s.handleListSSH) + mux.HandleFunc("POST /api/remote/connect", s.handleRemoteConnect) + mux.HandleFunc("POST /api/remote/list-dir", s.handleRemoteListDir) + mux.HandleFunc("POST /api/remote/bind", s.handleRemoteBind) + mux.HandleFunc("POST /api/remote/cancel", s.handleRemoteCancel) + mux.HandleFunc("POST /api/remote/save-alias", s.handleRemoteSaveAlias) mux.HandleFunc("GET /api/skills", s.handleListSkills) mux.HandleFunc("POST /api/skills/{name}/toggle", s.handleToggleSkill) mux.HandleFunc("GET /api/slash-commands", s.handleSlashCommands) @@ -450,7 +479,10 @@ func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { func (s *Server) handleWorkspace(w http.ResponseWriter, r *http.Request) { // Use the request context so the git commands are cancelled if the client // disconnects (CodeRabbit review feedback on PR #82). - branchCmd := exec.CommandContext(r.Context(), "git", "rev-parse", "--abbrev-ref", "HEAD") + // `branch --show-current` (not `rev-parse --abbrev-ref HEAD`) so a freshly + // initialised repo with no commits still reports its unborn branch (e.g. + // "main") instead of the literal "HEAD". + branchCmd := exec.CommandContext(r.Context(), "git", "branch", "--show-current") branchCmd.Dir = s.pwd branchOut, _ := branchCmd.Output() branch := strings.TrimSpace(string(branchOut)) @@ -2202,8 +2234,11 @@ func (s *Server) handleSetApprovalMode(w http.ResponseWriter, r *http.Request) { // --- WebSocket handler --- +// CheckOrigin rejects cross-origin WebSocket handshakes from untrusted web +// pages (see isAllowedWebOrigin); without this any website could open a socket +// to the loopback server and read the agent's live event stream. var wsUpgrader = websocket.Upgrader{ - CheckOrigin: func(r *http.Request) bool { return true }, + CheckOrigin: isAllowedWebOrigin, } func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) { @@ -2884,11 +2919,50 @@ func writeJSON(w http.ResponseWriter, status int, data any) { _ = json.NewEncoder(w).Encode(data) } +// isAllowedWebOrigin decides whether a browser request's Origin is trusted. +// +// The server (especially the always-on desktop sidecar) exposes agent control — +// i.e. shell/file tools — over loopback with no auth token, so an unconditional +// `Access-Control-Allow-Origin: *` plus a WebSocket CheckOrigin that returns +// true would let any website the user visits drive the agent or read its live +// event stream via ws://127.0.0.1:. We gate on Origin instead: +// +// - empty Origin (curl, native client, same-origin navigations) → allow +// - Origin equal to the request's own Host (same-origin) → allow; this covers +// local-browser, the desktop webview, and LAN access via `--host 0.0.0.0` +// - Origin whose host is loopback → allow; this covers the Vite dev proxy +// (localhost:5173 → 127.0.0.1:) +// +// A page on https://evil.com cannot forge its Origin, so it falls through to +// false and is blocked. This is intentionally not a full auth solution (a local +// process can still reach the port); it closes the cross-origin *website* vector. +func isAllowedWebOrigin(r *http.Request) bool { + origin := r.Header.Get("Origin") + if origin == "" { + return true + } + u, err := url.Parse(origin) + if err != nil || u.Host == "" { + return false + } + if u.Host == r.Host { + return true + } + switch u.Hostname() { + case "127.0.0.1", "localhost", "::1": + return true + } + return false +} + func corsMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") - if origin != "" { + // Only reflect CORS headers for trusted origins; a disallowed cross-origin + // request gets none, so the browser blocks the response (and its preflight). + if origin != "" && isAllowedWebOrigin(r) { w.Header().Set("Access-Control-Allow-Origin", origin) + w.Header().Set("Vary", "Origin") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") w.Header().Set("Access-Control-Allow-Credentials", "true") diff --git a/web/package.json b/web/package.json index fa240be..fcd1e75 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,10 @@ "@headlessui/vue": "^1.7.23", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.2.2", + "@tauri-apps/api": "^2.9.0", + "@tauri-apps/plugin-dialog": "^2.4.0", + "@tauri-apps/plugin-notification": "^2.3.1", + "@tauri-apps/plugin-opener": "^2.5.0", "@types/dompurify": "^3.2.0", "@types/qrcode": "^1.5.6", "@xterm/addon-fit": "^0.11.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index ee67f2c..18abec7 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -20,6 +20,18 @@ importers: '@tailwindcss/vite': specifier: ^4.2.2 version: 4.2.2(vite@8.0.8(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3)) + '@tauri-apps/api': + specifier: ^2.9.0 + version: 2.11.1 + '@tauri-apps/plugin-dialog': + specifier: ^2.4.0 + version: 2.7.1 + '@tauri-apps/plugin-notification': + specifier: ^2.3.1 + version: 2.3.3 + '@tauri-apps/plugin-opener': + specifier: ^2.5.0 + version: 2.5.4 '@types/dompurify': specifier: ^3.2.0 version: 3.2.0 @@ -420,48 +432,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.42.0': resolution: {integrity: sha512-+JA0YMlSdDqmacygGi2REp57c3fN+tzARD8nwsukx9pkCHK+6DkbAA9ojS4lNKsiBjIW8WWa0pBrBWhdZEqfuw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.42.0': resolution: {integrity: sha512-VfnET0j4Y5mdfCzh5gBt0NK28lgn5DKx+8WgSMLYYeSooHhohdbzwAStLki9pNuGy51y4I7IoW8bqwAaCMiJQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.42.0': resolution: {integrity: sha512-gVlCbmBkB0fxBWbhBj9rcxezPydsQHf4MFKeHoTSPicOQ+8oGeTQgQ8EeesSybWeiFPVRx3bgdt4IJnH6nOjAA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.42.0': resolution: {integrity: sha512-zN5OfstL0avgt/IgvRu0zjQzVh/EPkcLzs33E9LMAzpqlLWiPWeMDZyMGFlSRGOdDjuNmlZBCgj0pFnK5u32TQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.42.0': resolution: {integrity: sha512-9X6+H2L0qMc2sCAgO9HS03bkGLMKvOFjmEdchaFlany3vNZOjnVui//D8k/xZAtQv2vaCs1reD5KAgPoIU4msA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.42.0': resolution: {integrity: sha512-BajxJ6KQvMMdpXGPWhBGyjb2Jvx4uec0w+wi6TJZ6Tv7+MzPwe0pO8g5h1U0jyFgoaF7mDl6yKPW3ykWcbUJRw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.42.0': resolution: {integrity: sha512-0wV284I6vc5f0AqAhgAbHU2935B4bVpncPoe5n/WzVZY/KnHgqxC8iSFGeSyLWEgstFboIcWkOPck7tqbdHkzA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.42.0': resolution: {integrity: sha512-p4BG6HpGnhfgHk1rzZfyR6zcWkE7iLrWxyehHfXUy4Qa5j3e0roglFOdP/Nj5cJJ58MA3isQ5dlfkW2nNEpolw==} @@ -534,48 +554,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.57.0': resolution: {integrity: sha512-i66WyEPVEvq9bxRUCJ/MP5EBfnTDN3nhwEdFZFTO5MmLLvzngfWEG3NSdXQzTT3vk5B9i6C2XSIYBh+aG6uqyg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.57.0': resolution: {integrity: sha512-oMZDCwz4NobclZU3pH+V1/upVlJZiZvne4jQP+zhJwt+lmio4XXr4qG47CehvrW1Lx2YZiIHuxM2D4YpkG3KVA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.57.0': resolution: {integrity: sha512-uoBnjJ3MMEBbfnWC1jSFr7/nSCkcQYa72NYoNtLl1imshDnWSolYCjzb8LVCwYCCfLJXD+0gBLD7fyC14c0+0g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.57.0': resolution: {integrity: sha512-BdrwD7haPZ8a9KrZhKJRSj6jwCor+Z8tHFZ3PT89Y3Jq5v3LfMfEePeAmD0LOTWpiTmzSzdmyw9ijneapiVHKQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.57.0': resolution: {integrity: sha512-BNs+7ZNsRstVg2tpNxAXfMX/Iv5oZh204dVyb8Z37+/gCh+yZqNTlg6YwCLIMPSk5wLWIGOaQjT0GUOahKYImw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.57.0': resolution: {integrity: sha512-AghS18w+XcENcAX0+BQGLiqjpqpaxKJa4cWWP0OWNLacs27vHBxu7TYkv9LUSGe5w8lOJHeMxcYfZNOAPqw2bg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.57.0': resolution: {integrity: sha512-E/FV3GB8phu/Rpkhz5T96hAiJlGzn91qX5yj5gU754P5cmVGXY1Jw/VSjDSlZBCY3VHjsVLdzgdkJaomEmcNOg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.57.0': resolution: {integrity: sha512-xvZ2yZt0nUVfU14iuGv3V25jpr9pov5N0Wr28RXnHFxHCRxNDMtYPHV61gGLhN9IlXM96gI4pyYpLSJC5ClLCQ==} @@ -639,36 +667,42 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.15': resolution: {integrity: sha512-ZvRYMGrAklV9PEkgt4LQM6MjQX2P58HPAuecwYObY2DhS2t35R0I810bKi0wmaYORt6m/2Sm+Z+nFgb0WhXNcQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-VDpgGBzgfg5hLg+uBpCLoFG5kVvEyafmfxGUV0UHLcL5irxAK7PKNeC2MwClgk6ZAiNhmo9FLhRYgvMmedLtnQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.15': resolution: {integrity: sha512-y1uXY3qQWCzcPgRJATPSOUP4tCemh4uBdY7e3EZbVwCJTY3gLJWnQABgeUetvED+bt1FQ01OeZwvhLS2bpNrAQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.15': resolution: {integrity: sha512-023bTPBod7J3Y/4fzAN6QtpkSABR0rigtrwaP+qSEabUh5zf6ELr9Nc7GujaROuPY3uwdSIXWrvhn1KxOvurWA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.15': resolution: {integrity: sha512-witB2O0/hU4CgfOOKUoeFgQ4GktPi1eEbAhaLAIpgD6+ZnhcPkUtPsoKKHRzmOoWPZue46IThdSgdo4XneOLYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.15': resolution: {integrity: sha512-UCL68NJ0Ud5zRipXZE9dF5PmirzJE4E4BCIOOssEnM7wLDsxjc6Qb0sGDxTNRTP53I6MZpygyCpY8Aa8sPfKPg==} @@ -737,24 +771,28 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.2': resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.2': resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.2': resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.2': resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} @@ -802,6 +840,18 @@ packages: peerDependencies: vue: ^2.7.0 || ^3.0.0 + '@tauri-apps/api@2.11.1': + resolution: {integrity: sha512-M2FPuYND2m+wh5hfW9ZpSdxMPdEJovPBWwoHJmwUpysTYNHaOkVFN419m/K0LIgjb/7KU2vBgsUepJWugQCvAA==} + + '@tauri-apps/plugin-dialog@2.7.1': + resolution: {integrity: sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==} + + '@tauri-apps/plugin-notification@2.3.3': + resolution: {integrity: sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg==} + + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} + '@tsconfig/node24@24.0.4': resolution: {integrity: sha512-2A933l5P5oCbv6qSxHs7ckKwobs8BDAe9SJ/Xr2Hy+nDlwmLE1GhFh/g/vXGRZWgxBg9nX/5piDtHR9Dkw/XuA==} @@ -1513,24 +1563,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.32.0: resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} @@ -1565,6 +1619,7 @@ packages: lucide-vue-next@1.0.0: resolution: {integrity: sha512-V6SPvx1IHTj/UY+FrIYWV5faISsPSb8BnWSFDxAtezWKvWc9ZZ40PDrdu1/Qb5vg4lHWr1hs1BAMGVGm6V1Xdg==} + deprecated: Package deprecated. Please use @lucide/vue instead. peerDependencies: vue: '>=3.0.1' @@ -2649,6 +2704,20 @@ snapshots: '@tanstack/virtual-core': 3.13.23 vue: 3.5.32(typescript@6.0.2) + '@tauri-apps/api@2.11.1': {} + + '@tauri-apps/plugin-dialog@2.7.1': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@tauri-apps/plugin-notification@2.3.3': + dependencies: + '@tauri-apps/api': 2.11.1 + + '@tauri-apps/plugin-opener@2.5.4': + dependencies: + '@tauri-apps/api': 2.11.1 + '@tsconfig/node24@24.0.4': {} '@tybys/wasm-util@0.10.1': diff --git a/web/src/App.vue b/web/src/App.vue index 5e69623..9f0a8fe 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,10 +1,12 @@ + + + + diff --git a/web/src/components/ChatInput.vue b/web/src/components/ChatInput.vue index 590eb41..3f6b760 100644 --- a/web/src/components/ChatInput.vue +++ b/web/src/components/ChatInput.vue @@ -3,12 +3,23 @@ import { ref, nextTick, watch, computed, onMounted, onUnmounted } from 'vue' import { useChatStore } from '@/stores/chat' import { api } from '@/composables/api' import type { SlashCommandInfo, ChatImage } from '@/types/api' +import WorkspacePicker from '@/components/WorkspacePicker.vue' +import BranchPicker from '@/components/BranchPicker.vue' +import { MessageSquare, ClipboardList, Zap, Target, Plus, Paperclip, Slash, X } from 'lucide-vue-next' + +// Which way the workspace/branch pickers open. The docked composer opens them +// upward (default); the centered welcome composer has more empty room below, so +// it opens them downward to avoid clipping against the top of the canvas. +withDefaults(defineProps<{ pickerPlacement?: 'top' | 'bottom' }>(), { + pickerPlacement: 'top', +}) const store = useChatStore() const input = ref('') const textarea = ref(null) const showModelPicker = ref(false) const showModePicker = ref(false) +const showAddMenu = ref(false) const showManageModels = ref(false) const modelFilter = ref('') const containerRef = ref(null) @@ -24,9 +35,9 @@ const pendingImagePreviews = ref([]) const fileInput = ref(null) const modes = [ - { value: 'ask' as const, label: 'Ask', icon: '💬' }, - { value: 'plan' as const, label: 'Plan', icon: '📋' }, - { value: 'autopilot' as const, label: 'Autopilot', icon: '🚀' }, + { value: 'ask' as const, label: 'Ask', icon: MessageSquare }, + { value: 'plan' as const, label: 'Plan', icon: ClipboardList }, + { value: 'autopilot' as const, label: 'Autopilot', icon: Zap }, ] const filteredSlashCommands = computed(() => { @@ -189,6 +200,22 @@ function triggerImageUpload() { fileInput.value?.click() } +// Insert a trigger character at the cursor from the "+" menu. For "/" at the +// start of an empty box this also opens the slash-command menu via handleInput. +function insertToken(char: string) { + showAddMenu.value = false + const el = textarea.value + const start = el ? el.selectionStart : input.value.length + const end = el ? el.selectionEnd : input.value.length + input.value = input.value.slice(0, start) + char + input.value.slice(end) + nextTick(() => { + el?.focus() + const pos = start + char.length + el?.setSelectionRange(pos, pos) + handleInput() + }) +} + function handleImageSelect(e: Event) { const target = e.target as HTMLInputElement const files = target.files @@ -230,6 +257,7 @@ function handleClickOutside(e: MouseEvent) { if (containerRef.value && !containerRef.value.contains(e.target as Node)) { showModelPicker.value = false showModePicker.value = false + showAddMenu.value = false showSlashMenu.value = false if (showManageModels.value) { showManageModels.value = false @@ -279,7 +307,7 @@ watch(() => store.isRunning, (running) => {